Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MOB-8517] - Part 1 JWT #733

Merged
merged 31 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8b95b5c
JWT improvements
hardikmashru Apr 3, 2024
01d9fd1
removed sample app changes
hardikmashru Apr 3, 2024
2afca4c
sample app revert
hardikmashru Apr 3, 2024
281a19e
sample app revert
hardikmashru Apr 3, 2024
4dfafb9
updates
hardikmashru Apr 4, 2024
680b6fa
Update IterableConfig.java
hardikmashru Apr 4, 2024
8d860c4
Update IterableRequestTask.java
hardikmashru Apr 4, 2024
4f9f52a
Jwt imrovement part1 (#722)
hardikmashru Apr 5, 2024
e69ca65
Merge branch 'JWT_Imrovement_Part2' into feature/JWTRetryEnhancement
hardikmashru Apr 5, 2024
cd972d3
Implemented suggested changes
hardikmashru Apr 11, 2024
9cb4909
updates for retry mechanism
hardikmashru Apr 15, 2024
95497d8
Update IterableRequestTask.java
hardikmashru Apr 15, 2024
f8d7b74
updates
hardikmashru Apr 15, 2024
eb136f9
further improvements on parallel API calls to respect retrycount
hardikmashru Apr 15, 2024
71734e0
Merge branch 'feature/JWTRetryEnhancement' into JWT_Imrovement_Part1
hardikmashru Apr 16, 2024
c3d1cf4
Update IterableRequestTask.java
hardikmashru Apr 16, 2024
0ea96a2
conflict fixes
hardikmashru Apr 16, 2024
d270495
authmanager reset function added
hardikmashru Apr 17, 2024
fe9a166
did suggested changes
hardikmashru Apr 19, 2024
5980927
style updates
hardikmashru Apr 19, 2024
7fad778
checkstyle fixes
hardikmashru Apr 19, 2024
08dfedc
Update IterableAuthManager.java
hardikmashru Apr 19, 2024
32b9922
removed Part 2 code
hardikmashru Apr 22, 2024
03861a7
Update IterableRequestTask.java
hardikmashru Apr 23, 2024
cc80f40
Update IterableRequestTask.java
hardikmashru Apr 23, 2024
b6c8a14
Update IterableRequestTask.java
hardikmashru Apr 23, 2024
95687c2
handled null and exception scenario for authtoken
hardikmashru Apr 25, 2024
4074a89
Making some properties package friendly
May 8, 2024
3bfe86d
Merge branch 'feature/JWTImprovement' into JWT_Imrovement_Part1
Ayyanchira May 10, 2024
ec5ba3d
Merge pull request #718 from Iterable/JWT_Imrovement_Part1
Ayyanchira May 13, 2024
ce5ee4d
Merge branch 'master' into feature/JWTImprovement
Ayyanchira May 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is not used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be used in Part 2. So will keep it to avoid the cycle

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
Loading