Skip to content

Commit

Permalink
Merge pull request #733 from Iterable/feature/JWTImprovement
Browse files Browse the repository at this point in the history
[MOB-8517] - Part 1 JWT
  • Loading branch information
Ayyanchira authored May 14, 2024
2 parents 20a9821 + ce5ee4d commit fbfc6fb
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Context getMainActivityContext() {
@NonNull
IterableAuthManager getAuthManager() {
if (authManager == null) {
authManager = new IterableAuthManager(this, config.authHandler, config.expiringAuthTokenRefreshPeriod);
authManager = new IterableAuthManager(this, config.authHandler, config.retryPolicy, config.expiringAuthTokenRefreshPeriod);
}
return authManager;
}
Expand Down Expand Up @@ -344,7 +344,7 @@ private void logoutPreviousUser() {

getInAppManager().reset();
getEmbeddedManager().reset();
getAuthManager().clearRefreshTimer();
getAuthManager().reset();

apiClient.onLogout();
}
Expand All @@ -355,6 +355,7 @@ private void onLogin(@Nullable String authToken) {
return;
}

getAuthManager().pauseAuthRetries(false);
if (authToken != null) {
setAuthToken(authToken);
} else {
Expand Down Expand Up @@ -457,7 +458,7 @@ private void retrieveEmailAndUserId() {
getAuthManager().queueExpirationRefresh(_authToken);
} else {
IterableLogger.d(TAG, "Auth token found as null. Scheduling token refresh in 10 seconds...");
getAuthManager().scheduleAuthTokenRefresh(10000);
getAuthManager().scheduleAuthTokenRefresh(authManager.getNextRetryInterval(), true, null);
}
}
}
Expand Down Expand Up @@ -697,6 +698,17 @@ public IterableAttributionInfo getAttributionInfo() {
);
}

/**
* // This method gets called from developer end only.
* @param pauseRetry to pause/unpause auth retries
*/
public void pauseAuthRetries(boolean pauseRetry) {
getAuthManager().pauseAuthRetries(pauseRetry);
if (!pauseRetry) { // request new auth token as soon as unpause
getAuthManager().requestNewAuthToken(false);
}
}

public void setEmail(@Nullable String email) {
setEmail(email, null, null, null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.iterable.iterableapi;

import android.util.Base64;

import androidx.annotation.VisibleForTesting;

import org.json.JSONException;
Expand All @@ -20,23 +19,46 @@ public class IterableAuthManager {
private final IterableApi api;
private final IterableAuthHandler authHandler;
private final long expiringAuthTokenRefreshPeriod;
private final long scheduledRefreshPeriod = 10000;
@VisibleForTesting
Timer timer;
private boolean hasFailedPriorAuth;
private boolean pendingAuth;
private boolean requiresAuthRefresh;
RetryPolicy authRetryPolicy;
boolean pauseAuthRetry;
int retryCount;
private boolean isLastAuthTokenValid;
private boolean isTimerScheduled;

private final ExecutorService executor = Executors.newSingleThreadExecutor();

IterableAuthManager(IterableApi api, IterableAuthHandler authHandler, long expiringAuthTokenRefreshPeriod) {
IterableAuthManager(IterableApi api, IterableAuthHandler authHandler, RetryPolicy authRetryPolicy, long expiringAuthTokenRefreshPeriod) {
this.api = api;
this.authHandler = authHandler;
this.authRetryPolicy = authRetryPolicy;
this.expiringAuthTokenRefreshPeriod = expiringAuthTokenRefreshPeriod;
}

public synchronized void requestNewAuthToken(boolean hasFailedPriorAuth) {
requestNewAuthToken(hasFailedPriorAuth, null);
requestNewAuthToken(hasFailedPriorAuth, null, true);
}

public void pauseAuthRetries(boolean pauseRetry) {
pauseAuthRetry = pauseRetry;
resetRetryCount();
}

void reset() {
clearRefreshTimer();
setIsLastAuthTokenValid(false);
}

void setIsLastAuthTokenValid(boolean isValid) {
isLastAuthTokenValid = isValid;
}

void resetRetryCount() {
retryCount = 0;
}

private void handleSuccessForAuthToken(String authToken, IterableHelper.SuccessHandler successCallback) {
Expand All @@ -51,7 +73,12 @@ private void handleSuccessForAuthToken(String authToken, IterableHelper.SuccessH

public synchronized void requestNewAuthToken(
boolean hasFailedPriorAuth,
final IterableHelper.SuccessHandler successCallback) {
final IterableHelper.SuccessHandler successCallback,
boolean shouldIgnoreRetryPolicy) {
if ((!shouldIgnoreRetryPolicy && pauseAuthRetry) || (retryCount >= authRetryPolicy.maxRetry && !shouldIgnoreRetryPolicy)) {
return;
}

if (authHandler != null) {
if (!pendingAuth) {
if (!(this.hasFailedPriorAuth && hasFailedPriorAuth)) {
Expand All @@ -62,9 +89,17 @@ public synchronized void requestNewAuthToken(
@Override
public void run() {
try {
if (isLastAuthTokenValid && !shouldIgnoreRetryPolicy) {
// if some JWT retry had valid token it will not fetch the auth token again from developer function
handleAuthTokenSuccess(IterableApi.getInstance().getAuthToken(), successCallback);
return;
}
final String authToken = authHandler.onAuthTokenRequested();
pendingAuth = false;
retryCount++;
handleAuthTokenSuccess(authToken, successCallback);
} catch (final Exception e) {
retryCount++;
handleAuthTokenFailure(e);
}
}
Expand All @@ -89,12 +124,11 @@ private void handleAuthTokenSuccess(String authToken, IterableHelper.SuccessHand
} 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
scheduleAuthTokenRefresh(scheduledRefreshPeriod);
scheduleAuthTokenRefresh(getNextRetryInterval(), false, null);
authHandler.onTokenRegistrationFailed(new Throwable("Auth token null"));
return;
}
IterableApi.getInstance().setAuthToken(authToken);
pendingAuth = false;
reSyncAuth();
authHandler.onTokenRegistrationSuccessful(authToken);
}
Expand All @@ -103,7 +137,7 @@ private void handleAuthTokenFailure(Throwable throwable) {
IterableLogger.e(TAG, "Error while requesting Auth Token", throwable);
authHandler.onTokenRegistrationFailed(throwable);
pendingAuth = false;
reSyncAuth();
scheduleAuthTokenRefresh(getNextRetryInterval(), false, null);
}

public void queueExpirationRefresh(String encodedJWT) {
Expand All @@ -112,15 +146,15 @@ public void queueExpirationRefresh(String encodedJWT) {
long expirationTimeSeconds = decodedExpiration(encodedJWT);
long triggerExpirationRefreshTime = expirationTimeSeconds * 1000L - expiringAuthTokenRefreshPeriod - IterableUtil.currentTimeMillis();
if (triggerExpirationRefreshTime > 0) {
scheduleAuthTokenRefresh(triggerExpirationRefreshTime);
scheduleAuthTokenRefresh(triggerExpirationRefreshTime, true, null);
} else {
IterableLogger.w(TAG, "The expiringAuthTokenRefreshPeriod has already passed for the current JWT");
}
} 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.
scheduleAuthTokenRefresh(scheduledRefreshPeriod);
scheduleAuthTokenRefresh(getNextRetryInterval(), false, null);
}
}

Expand All @@ -131,28 +165,57 @@ void resetFailedAuth() {
void reSyncAuth() {
if (requiresAuthRefresh) {
requiresAuthRefresh = false;
requestNewAuthToken(false);
scheduleAuthTokenRefresh(getNextRetryInterval(), false, null);
}
}

void scheduleAuthTokenRefresh(long timeDuration) {
timer = new Timer(true);
long getNextRetryInterval() {
long nextRetryInterval = authRetryPolicy.retryInterval;
if (authRetryPolicy.retryBackoff == RetryPolicy.Type.EXPONENTIAL) {
nextRetryInterval *= Math.pow(IterableConstants.EXPONENTIAL_FACTOR, retryCount - 1); // Exponential backoff
}

return nextRetryInterval;
}

void scheduleAuthTokenRefresh(long timeDuration, boolean isScheduledRefresh, final IterableHelper.SuccessHandler successCallback) {
if ((pauseAuthRetry && !isScheduledRefresh) || isTimerScheduled) {
// we only stop schedule token refresh if it is called from retry (in case of failure). The normal auth token refresh schedule would work
return;
}
if (timer == null) {
timer = new Timer(true);
}
try {
timer.schedule(new TimerTask() {
@Override
public void run() {
if (api.getEmail() != null || api.getUserId() != null) {
api.getAuthManager().requestNewAuthToken(false);
api.getAuthManager().requestNewAuthToken(false, successCallback, isScheduledRefresh);
} else {
IterableLogger.w(TAG, "Email or userId is not available. Skipping token refresh");
}
isTimerScheduled = false;
}
}, timeDuration);
isTimerScheduled = true;
} catch (Exception e) {
IterableLogger.e(TAG, "timer exception: " + timer, e);
}
}

private String getEmailOrUserId() {
String email = api.getEmail();
String userId = api.getUserId();

if (email != null) {
return email;
} else if (userId != null) {
return userId;
}
return null;
}

private static long decodedExpiration(String encodedJWT) throws Exception {
long exp = 0;
String[] split = encodedJWT.split("\\.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public class IterableConfig {
*/
final long expiringAuthTokenRefreshPeriod;

/**
* Retry policy for JWT Refresh.
*/
final RetryPolicy retryPolicy;

/**
* By default, the SDK allows navigation/calls to URLs with the `https` protocol (e.g. deep links or external links)
* If you'd like to allow other protocols like `http`, `tel`, etc., add them to the `allowedProtocols` array
Expand Down Expand Up @@ -100,6 +105,7 @@ private IterableConfig(Builder builder) {
inAppDisplayInterval = builder.inAppDisplayInterval;
authHandler = builder.authHandler;
expiringAuthTokenRefreshPeriod = builder.expiringAuthTokenRefreshPeriod;
retryPolicy = builder.retryPolicy;
allowedProtocols = builder.allowedProtocols;
dataRegion = builder.dataRegion;
useInMemoryStorageForInApps = builder.useInMemoryStorageForInApps;
Expand All @@ -118,6 +124,7 @@ public static class Builder {
private double inAppDisplayInterval = 30.0;
private IterableAuthHandler authHandler;
private long expiringAuthTokenRefreshPeriod = 60000L;
private RetryPolicy retryPolicy = new RetryPolicy(10, 6L, RetryPolicy.Type.LINEAR);
private String[] allowedProtocols = new String[0];
private IterableDataRegion dataRegion = IterableDataRegion.US;
private boolean useInMemoryStorageForInApps = false;
Expand Down Expand Up @@ -224,6 +231,16 @@ public Builder setAuthHandler(@NonNull IterableAuthHandler authHandler) {
return this;
}

/**
* Set retry policy for JWT Refresh
* @param retryPolicy
*/
@NonNull
public Builder setAuthRetryPolicy(@NonNull RetryPolicy retryPolicy) {
this.retryPolicy = retryPolicy;
return this;
}

/**
* Set a custom period before an auth token expires to automatically retrieve a new token
* @param period in seconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ public final class IterableConstants {
public static final int ITERABLE_IN_APP_ANIMATION_DURATION = 500;
public static final int ITERABLE_IN_APP_BACKGROUND_ANIMATION_DURATION = 300;

public static final int EXPONENTIAL_FACTOR = 2;

public static final double ITERABLE_IN_APP_PRIORITY_LEVEL_LOW = 400.0;
public static final double ITERABLE_IN_APP_PRIORITY_LEVEL_MEDIUM = 300.0;
public static final double ITERABLE_IN_APP_PRIORITY_LEVEL_HIGH = 200.0;
Expand Down
Loading

0 comments on commit fbfc6fb

Please sign in to comment.