Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added
- Added `updateAuthToken(String)` method for updating the auth token without triggering login side effects (push registration, in-app sync, embedded sync). Use this when you only need to refresh the token for an already logged-in user.

### Deprecated
- `setAuthToken(String)` is now deprecated. It still triggers login operations (push registration, in-app sync, embedded sync) for backward compatibility, but will be changed to only store the token in a future release. Migrate to `updateAuthToken(String)` to update the token without side effects, or use `setEmail(email, authToken)` / `setUserId(userId, authToken)` to set credentials and trigger login operations.

## [3.6.5]
### Fixed
- Fixed IterableEmbeddedView not having an empty constructor and causing crashes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.iterable.iterableapi;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

class ApiEndpointClassification {

private static final Set<String> DEFAULT_UNAUTHENTICATED = new HashSet<>(Arrays.asList(
IterableConstants.ENDPOINT_DISABLE_DEVICE,
IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION,
IterableConstants.ENDPOINT_MERGE_USER,
IterableConstants.ENDPOINT_CRITERIA_LIST,
IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION,
IterableConstants.ENDPOINT_TRACK_CONSENT
));

private volatile Set<String> unauthenticatedPaths = new HashSet<>(DEFAULT_UNAUTHENTICATED);

boolean requiresJwt(String path) {
return !unauthenticatedPaths.contains(path);
}

void updateFromRemoteConfig(Set<String> paths) {
this.unauthenticatedPaths = new HashSet<>(paths);
}
}
80 changes: 61 additions & 19 deletions iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ public class IterableApi {
private IterableNotificationData _notificationData;
private String _deviceId;
private boolean _firstForegroundHandled;
private boolean _autoRetryOnJwtFailure;
private IterableHelper.SuccessHandler _setUserSuccessCallbackHandler;
private IterableHelper.FailureHandler _setUserFailureCallbackHandler;

IterableApiClient apiClient = new IterableApiClient(new IterableApiAuthProvider());
final ApiEndpointClassification apiEndpointClassification = new ApiEndpointClassification();
private static final UnknownUserMerge unknownUserMerge = new UnknownUserMerge();
private @Nullable UnknownUserManager unknownUserManager;
private @Nullable IterableInAppManager inAppManager;
Expand Down Expand Up @@ -104,6 +106,14 @@ public void execute(@Nullable String data) {
SharedPreferences sharedPref = sharedInstance.getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_SAVED_CONFIGURATION, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putBoolean(IterableConstants.SHARED_PREFS_OFFLINE_MODE_KEY, offlineConfiguration);

// Parse autoRetry flag from remote config.
if (jsonData.has(IterableConstants.KEY_AUTO_RETRY)) {
boolean autoRetryRemote = jsonData.getBoolean(IterableConstants.KEY_AUTO_RETRY);
editor.putBoolean(IterableConstants.SHARED_PREFS_AUTO_RETRY_KEY, autoRetryRemote);
_autoRetryOnJwtFailure = autoRetryRemote;
}

editor.apply();
} catch (JSONException e) {
IterableLogger.e(TAG, "Failed to read remote configuration");
Expand All @@ -127,7 +137,7 @@ public String getAuthToken() {
private void checkAndUpdateAuthToken(@Nullable String authToken) {
// If authHandler exists and if authToken is new, it will be considered as a call to update the authToken.
if (config.authHandler != null && authToken != null && authToken != _authToken) {
setAuthToken(authToken);
updateAuthToken(authToken);
}
}

Expand Down Expand Up @@ -194,6 +204,15 @@ static void loadLastSavedConfiguration(Context context) {
SharedPreferences sharedPref = context.getSharedPreferences(IterableConstants.SHARED_PREFS_SAVED_CONFIGURATION, Context.MODE_PRIVATE);
boolean offlineMode = sharedPref.getBoolean(IterableConstants.SHARED_PREFS_OFFLINE_MODE_KEY, false);
sharedInstance.apiClient.setOfflineProcessingEnabled(offlineMode);

sharedInstance._autoRetryOnJwtFailure = sharedPref.getBoolean(IterableConstants.SHARED_PREFS_AUTO_RETRY_KEY, false);
}

/**
* Returns whether auto-retry on JWT failure is enabled, as determined by remote configuration.
*/
boolean isAutoRetryOnJwtFailure() {
return _autoRetryOnJwtFailure;
}

/**
Expand Down Expand Up @@ -406,20 +425,24 @@ private void onLogin(
@Nullable IterableHelper.FailureHandler failureHandler
) {
if (!isInitialized()) {
setAuthToken(null);
updateAuthToken(null);
return;
}

getAuthManager().pauseAuthRetries(false);
if (authToken != null) {
setAuthToken(authToken);
updateAuthToken(authToken);
completeUserLogin();
attemptMergeAndEventReplay(userIdOrEmail, isEmail, merge, replay, isUnknown, failureHandler);
} else {
getAuthManager().requestNewAuthToken(false, data -> attemptMergeAndEventReplay(userIdOrEmail, isEmail, merge, replay, isUnknown, failureHandler));
getAuthManager().requestNewAuthToken(false, data -> {
completeUserLogin();
attemptMergeAndEventReplay(userIdOrEmail, isEmail, merge, replay, isUnknown, failureHandler);
});
}
}

private void completeUserLogin() {
void completeUserLogin() {
completeUserLogin(_email, _userId, _authToken);
}

Expand Down Expand Up @@ -660,19 +683,16 @@ public void resetAuth() {

//region API functions (private/internal)
//---------------------------------------------------------------------------------------
void setAuthToken(String authToken, boolean bypassAuth) {

/**
* Updates the auth token without triggering login side effects (push registration, in-app sync, etc.).
* Use this method when you only need to update the token for an already logged-in user.
* For initial login, use {@code setEmail(email, authToken)} or {@code setUserId(userId, authToken)}.
*/
public void updateAuthToken(@Nullable String authToken) {
if (isInitialized()) {
if ((authToken != null && !authToken.equalsIgnoreCase(_authToken)) || (_authToken != null && !_authToken.equalsIgnoreCase(authToken))) {
_authToken = authToken;
// SECURITY: Use completion handler to atomically store and pass validated credentials.
// The completion handler receives exact values stored to keychain, preventing TOCTOU
// attacks where keychain could be modified between storage and completeUserLogin execution.
storeAuthData((email, userId, token) -> completeUserLogin(email, userId, token));
} else if (bypassAuth) {
// SECURITY: Pass current credentials directly to completeUserLogin.
// completeUserLogin will validate authToken presence when JWT auth is enabled.
completeUserLogin(_email, _userId, _authToken);
}
_authToken = authToken;
storeAuthData();
}
}

Expand Down Expand Up @@ -1056,6 +1076,9 @@ public void setEmail(@Nullable String email, @Nullable String authToken, @Nullab

if (_email != null && _email.equals(email)) {
checkAndUpdateAuthToken(authToken);
_setUserSuccessCallbackHandler = successHandler;
_setUserFailureCallbackHandler = failureHandler;
onLogin(authToken, email, true, merge, replay, false, failureHandler);
return;
}

Expand Down Expand Up @@ -1126,6 +1149,9 @@ public void setUserId(@Nullable String userId, @Nullable String authToken, @Null

if (_userId != null && _userId.equals(userId)) {
checkAndUpdateAuthToken(authToken);
_setUserSuccessCallbackHandler = successHandler;
_setUserFailureCallbackHandler = failureHandler;
onLogin(authToken, userId, false, merge, replay, isUnknown, failureHandler);
return;
}

Expand Down Expand Up @@ -1192,8 +1218,24 @@ private void attemptAndProcessMerge(@NonNull String destinationUser, boolean isE
});
}

public void setAuthToken(String authToken) {
setAuthToken(authToken, false);
/**
* Sets the auth token and triggers login operations (push registration, in-app sync, embedded sync).
*
* @deprecated This method triggers login side effects beyond just setting the token.
* To update the auth token without login side effects, use {@link #updateAuthToken(String)}.
* To set credentials and trigger login operations, use {@code setEmail(email, authToken)}
* or {@code setUserId(userId, authToken)}.
* In a future release, this method will only store the auth token without triggering login operations.
*/
@Deprecated
public void setAuthToken(@Nullable String authToken) {
if (isInitialized()) {
IterableLogger.w(TAG, "setAuthToken() is deprecated. Use updateAuthToken() to update the token, " +
"or setEmail(email, authToken) / setUserId(userId, authToken) for login. " +
"In a future release, this method will only store the auth token without triggering login operations.");
_authToken = authToken;
storeAuthData(this::completeUserLogin);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,20 @@ private RequestProcessor getRequestProcessor() {
}

void setOfflineProcessingEnabled(boolean offlineMode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (offlineMode) {
if (this.requestProcessor == null || this.requestProcessor.getClass() != OfflineRequestProcessor.class) {
this.requestProcessor = new OfflineRequestProcessor(authProvider.getContext());
}
} else {
if (this.requestProcessor == null || this.requestProcessor.getClass() != OnlineRequestProcessor.class) {
this.requestProcessor = new OnlineRequestProcessor();
}
}
if (offlineMode && this.requestProcessor instanceof OfflineRequestProcessor) {
return;
}
if (!offlineMode && this.requestProcessor instanceof OnlineRequestProcessor) {
return;
}

if (this.requestProcessor instanceof OfflineRequestProcessor) {
((OfflineRequestProcessor) this.requestProcessor).dispose();
}

this.requestProcessor = offlineMode
? new OfflineRequestProcessor(authProvider.getContext())
: new OnlineRequestProcessor();
}

void getRemoteConfiguration(IterableHelper.IterableActionHandler actionHandler) {
Expand Down
Loading
Loading