Skip to content

Commit

Permalink
Merge pull request #751 from Iterable/feature/JWTImprovement
Browse files Browse the repository at this point in the history
[MOB - 8993] - JWT part 2 with improvements
  • Loading branch information
Ayyanchira authored Jul 5, 2024
2 parents 7ae0cde + 63db963 commit 7e99a06
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.iterable.iterableapi;

/**
* Represents an auth failure object.
*/
public class AuthFailure {

/** userId or email of the signed-in user */
public final String userKey;

/** the authToken which caused the failure */
public final String failedAuthToken;

/** the timestamp of the failed request */
public final long failedRequestTime;

/** indicates a reason for failure */
public final AuthFailureReason failureReason;

public AuthFailure(String userKey,
String failedAuthToken,
long failedRequestTime,
AuthFailureReason failureReason) {
this.userKey = userKey;
this.failedAuthToken = failedAuthToken;
this.failedRequestTime = failedRequestTime;
this.failureReason = failureReason;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.iterable.iterableapi;
public enum AuthFailureReason {
AUTH_TOKEN_EXPIRED,
AUTH_TOKEN_GENERIC_ERROR,
AUTH_TOKEN_EXPIRATION_INVALID,
AUTH_TOKEN_SIGNATURE_INVALID,
AUTH_TOKEN_FORMAT_INVALID,
AUTH_TOKEN_INVALIDATED,
AUTH_TOKEN_PAYLOAD_INVALID,
AUTH_TOKEN_USER_KEY_INVALID,
AUTH_TOKEN_NULL,
AUTH_TOKEN_GENERATION_ERROR,
AUTH_TOKEN_MISSING,
}
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ private void retrieveEmailAndUserId() {
if (_authToken != null) {
getAuthManager().queueExpirationRefresh(_authToken);
} else {
IterableLogger.d(TAG, "Auth token found as null. Scheduling token refresh in 10 seconds...");
IterableLogger.d(TAG, "Auth token found as null. Rescheduling auth token refresh");
getAuthManager().scheduleAuthTokenRefresh(authManager.getNextRetryInterval(), true, null);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
public interface IterableAuthHandler {
String onAuthTokenRequested();
void onTokenRegistrationSuccessful(String authToken);
void onTokenRegistrationFailed(Throwable object);
void onAuthFailure(AuthFailure authFailure);
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public synchronized void requestNewAuthToken(
boolean hasFailedPriorAuth,
final IterableHelper.SuccessHandler successCallback,
boolean shouldIgnoreRetryPolicy) {
if ((!shouldIgnoreRetryPolicy && pauseAuthRetry) || (retryCount >= authRetryPolicy.maxRetry && !shouldIgnoreRetryPolicy)) {
if (!shouldIgnoreRetryPolicy && (pauseAuthRetry || (retryCount >= authRetryPolicy.maxRetry))) {
return;
}

Expand Down Expand Up @@ -123,20 +123,20 @@ private void handleAuthTokenSuccess(String authToken, IterableHelper.SuccessHand
}
queueExpirationRefresh(authToken);
} else {
IterableLogger.w(TAG, "Auth token received as null. Calling the handler in 10 seconds");
//TODO: Make this time configurable and in sync with SDK initialization flow for auth null scenario
handleAuthFailure(authToken, AuthFailureReason.AUTH_TOKEN_NULL);
IterableApi.getInstance().setAuthToken(authToken);
scheduleAuthTokenRefresh(getNextRetryInterval(), false, null);
authHandler.onTokenRegistrationFailed(new Throwable("Auth token null"));
return;
}
IterableApi.getInstance().setAuthToken(authToken);
reSyncAuth();
authHandler.onTokenRegistrationSuccessful(authToken);
}

// This method is called when there is an error receiving an the auth token.
private void handleAuthTokenFailure(Throwable throwable) {
IterableLogger.e(TAG, "Error while requesting Auth Token", throwable);
authHandler.onTokenRegistrationFailed(throwable);
handleAuthFailure(null, AuthFailureReason.AUTH_TOKEN_GENERATION_ERROR);
pendingAuth = false;
scheduleAuthTokenRefresh(getNextRetryInterval(), false, null);
}
Expand All @@ -153,8 +153,8 @@ public void queueExpirationRefresh(String encodedJWT) {
}
} catch (Exception e) {
IterableLogger.e(TAG, "Error while parsing JWT for the expiration", e);
authHandler.onTokenRegistrationFailed(new Throwable("Auth token decode failure. Scheduling auth token refresh in 10 seconds..."));
//TODO: Sync with configured time duration once feature is available.
isLastAuthTokenValid = false;
handleAuthFailure(encodedJWT, AuthFailureReason.AUTH_TOKEN_PAYLOAD_INVALID);
scheduleAuthTokenRefresh(getNextRetryInterval(), false, null);
}
}
Expand All @@ -170,6 +170,14 @@ void reSyncAuth() {
}
}

// This method is called is used to call the authHandler.onAuthFailure method with appropriate AuthFailureReason
void handleAuthFailure(String authToken, AuthFailureReason failureReason) {
if (authHandler != null) {
authHandler.onAuthFailure(new AuthFailure(getEmailOrUserId(), authToken, IterableUtil.currentTimeMillis(), failureReason));
}
}


long getNextRetryInterval() {
long nextRetryInterval = authRetryPolicy.retryInterval;
if (authRetryPolicy.retryBackoff == RetryPolicy.Type.EXPONENTIAL) {
Expand All @@ -187,6 +195,7 @@ void scheduleAuthTokenRefresh(long timeDuration, boolean isScheduledRefresh, fin
if (timer == null) {
timer = new Timer(true);
}

try {
timer.schedule(new TimerTask() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ static IterableApiResponse executeApiRequest(IterableApiRequest iterableApiReque
if (responseCode == 401) {
if (matchesJWTErrorCodes(jsonResponse)) {
apiResponse = IterableApiResponse.failure(responseCode, requestResult, jsonResponse, "JWT Authorization header error");
IterableApi.getInstance().getAuthManager().handleAuthFailure(iterableApiRequest.authToken, getMappedErrorCodeForMessage(jsonResponse));
// We handle the JWT Retry for both online and offline here rather than handling online request in onPostExecute
requestNewAuthTokenAndRetry(iterableApiRequest);
} else {
Expand Down Expand Up @@ -265,6 +266,40 @@ private static boolean matchesErrorCode(JSONObject jsonResponse, String errorCod
}
}

private static AuthFailureReason getMappedErrorCodeForMessage(JSONObject jsonResponse) {
try {
if (jsonResponse == null || !jsonResponse.has("msg")) {
return null;
}

String errorMessage = jsonResponse.getString("msg");

switch (errorMessage.toLowerCase()) {
case "exp must be less than 1 year from iat":
return AuthFailureReason.AUTH_TOKEN_EXPIRATION_INVALID;
case "jwt format is invalid":
return AuthFailureReason.AUTH_TOKEN_FORMAT_INVALID;
case "jwt token is expired":
return AuthFailureReason.AUTH_TOKEN_EXPIRED;
case "jwt is invalid":
return AuthFailureReason.AUTH_TOKEN_SIGNATURE_INVALID;
case "jwt payload requires a value for userid or email":
case "email could not be found":
return AuthFailureReason.AUTH_TOKEN_USER_KEY_INVALID;
case "jwt token has been invalidated":
return AuthFailureReason.AUTH_TOKEN_INVALIDATED;
case "invalid payload":
return AuthFailureReason.AUTH_TOKEN_PAYLOAD_INVALID;
case "jwt authorization header is not set":
return AuthFailureReason.AUTH_TOKEN_MISSING;
default:
return AuthFailureReason.AUTH_TOKEN_GENERIC_ERROR;
}
} catch (JSONException e) {
return null;
}
}

private static boolean matchesJWTErrorCodes(JSONObject jsonResponse) {
return matchesErrorCode(jsonResponse, ERROR_CODE_INVALID_JWT_PAYLOAD) || matchesErrorCode(jsonResponse, ERROR_CODE_MISSING_JWT_PAYLOAD) || matchesErrorCode(jsonResponse, ERROR_CODE_JWT_USER_IDENTIFIERS_MISMATCHED);
}
Expand Down

0 comments on commit 7e99a06

Please sign in to comment.