Skip to content

AB#268288 Federated Authentication#87

Open
graciecooper wants to merge 5 commits intomasterfrom
feature/268288-federated-auth
Open

AB#268288 Federated Authentication#87
graciecooper wants to merge 5 commits intomasterfrom
feature/268288-federated-auth

Conversation

@graciecooper
Copy link
Copy Markdown
Contributor

@graciecooper graciecooper commented Mar 25, 2026

Description of Changes

Adds Federated JWT authentication to the Android SDK. When OptimoveConfig.Builder.enableAuth(AuthTokenProvider) is used, the SDK asks the app for a JWT per user context and attaches it as X-User-JWT on user-identified outbound traffic (anonymous / install-scoped traffic omits the JWT where applicable).

Key changes

  • AuthManager + enableAuth(AuthTokenProvider) on OptimoveConfig.Builder
  • X-Optimove-Auth-Capable: 1 on SDK HTTP traffic (HttpClient + OptimobileHttpClient)
  • JWT for user-identified calls: Optistream (OptiTrack), Realtime, Preference Center, Embedded Messaging, Optimobile analytics, in-app (inbox fetch)
  • OptistreamHandler groups batches by customer so each request has one JWT
  • Optimobile analytics pulls the next queue slice per userIdentifier so the JWT matches the batch
  • Token failures: Optistream / Realtime skip sending that attempt; blocking-JWT Optimobile paths may still send without X-User-JWT

Usage

Kotlin

OptimoveConfig.Builder(/**/)
    .enableAuth { userId, callback ->
        MyAuthService.getJwt(userId) { jwt, error ->
            callback.onComplete(jwt, error)
        }
    }
    //
    .build()

Breaking Changes

  • None

Release Checklist

Prepare:

  • Detail any breaking changes. Breaking changes require a new major version number, and a migration guide in wiki / README.md

Bump versions in:

  • CHANGELOG.md
  • gradle.properties
  • add links to newly created wiki pages to readme
  • Update major version numbers in wiki (basic integration + push guides)

Integration tests

T&T Only

  • Init SDK with only optimove credentials
  • Associate customer
  • Associate email
  • Track events

Mobile Only

  • Init SDK with all credentials
  • Track events
  • Associate customer (verify both backends)
  • Register for push
  • Opt-in for In-App
  • Send test push
  • Send test In-App
  • Receive / trigger deep link handler (In-App/Push)
  • Receive / trigger the content extension, render image and action buttons for push
  • Verify push opened handler

Deferred Deep Links

  • With app installed, trigger deep link handler
  • With app uninstalled, follow deep link, install test bundle, verify deep link read from Clipboard, trigger deep link handler

Combined

  • Track event for T&T, verify push received
  • Trigger scheduled campaign, verify push received
  • Trigger scheduled campaign, verify In-App received

Release Procedure

  • Squash and merge dev to master
  • Delete branch once merged

@graciecooper graciecooper force-pushed the feature/268288-federated-auth branch from 7e18f19 to db27d29 Compare April 2, 2026 11:16
@graciecooper graciecooper marked this pull request as ready for review April 2, 2026 11:46
this(httpClient, realtimeConfigs, context, null);
}

public RealtimeManager(@NonNull HttpClient httpClient, @NonNull RealtimeConfigs realtimeConfigs,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need this additional constructor? shouldnt we just have one and mock the auth in tests? (We should add tests to RT in any case)

}
result = super.postSync(url, postBody, true);
String jwt = super.resolveJwt(this.customerId);
if (AuthJwtResolver.isMissingRequiredJwt(Optimove.getConfig().getAuthTokenProvider(), this.customerId, jwt)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

See line 99, could this cause all visitors to start failing requests? (userId is either userId or visitor here)

}

public RequestBuilder<T> userJwt(@Nullable String jwt) {
return this;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are we intentionally dropping this?

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public final class AuthManager {
Copy link
Copy Markdown
Collaborator

@k-antipochkin k-antipochkin Apr 17, 2026

Choose a reason for hiding this comment

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

Ideally we would want to inject it to all components that need it.

It is now possible in OptistreamHandler and RT.
It isnt possible with the singleton/static components.

We have 3 options:

  1. Leave it like it is now - 5 instantiations with Optimove.getConfig().getAuthTokenProvider()
  2. Inject where DI pattern is used (OptistreamHandler and RT) and use Optimove.getAuthManager() for the singletons instead
  3. Use Optimove.getAuthManager() for all

I think 2 is cleanest (sooner or later we would need to stop letting random components seek the values they need in other singletons and get them using DI instead). 3 is pragmatic IMO. Let me know what you think.

@k-antipochkin
Copy link
Copy Markdown
Collaborator

@graciecooper Can we add some more test coverage for the auth flows (Embedded, grouping etc) or that would require significant refactoring?

error != null ? error.getMessage() : "null token");
dispatchRequestWaitsForResponse = false;
scheduleTheNextDispatch();
return;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we abort all the remaining groups if one fails?


Runnable postOnExecutor = () -> postGroupJson(group, groups, index, null);

if (authManager != null && !customerKey.isEmpty()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we reduce the indentation here?

if (authManager == null || customerIsEmpty) {
 postOnExecutor.run();
 return;
}

// rest of auth

@k-antipochkin
Copy link
Copy Markdown
Collaborator

@graciecooper Is it possible to introduce a single dispatcher layer for RT and optistream like we did in iOS? or it would require a big refactoring?

Comment thread CHANGELOG.md
- Added Federated JWT authentication; OptimoveConfig.Builder.enableAuth(AuthTokenProvider) supplies tokens; the SDK adds X-User-JWT for user-identified Optistream, realtime, Preference Center, Embedded Messaging, Optimobile analytics, and in-app network calls.
- Added Auth-capable signaling; Outbound SDK requests sent through HttpClient and OptimobileHttpClient include X-Optimove-Auth-Capable: 1 so backends can detect JWT-capable SDK versions.
- Optistream and realtime paths now group events by customer identity so each request can carry a single JWT. Optimobile analytics drains queued events per stored user id (install/visitor-scoped batches do not attach a user JWT).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove?

Map<String, List<OptistreamEvent>> map = new LinkedHashMap<>();
for (OptistreamEvent ev : events) {
String key = userKey(ev);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(ev);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does it require API 24? I think our minimum is 21?

}
httpClient.postJson(realtimeConfigs.getRealtimeGateway(), realtimeGson.toJson(group))
.userJwt(token)
.successListener(jsonResponse -> dispatchGroupAtIndex(groups, index + 1, allForFailure))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should it be allForFailure or just the one group?

}

@Nullable
public static String blockingJwt(@Nullable AuthManager authManager, @Nullable String userId, long timeoutMs) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In case of failures, should we log?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants