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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 7.14.0

- - Adds federated JWT authentication to the Android SDK. When enableAuth() is used, the SDK fetches JWTs from a client-provided closure and attaches them via X-User-JWT on all user-identified requests.

## 7.13.0

- Implementation for Overlay Messaging channel. Check optimove developer docs for more.
Expand Down
8 changes: 3 additions & 5 deletions OptimoveSDK/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m

sdk_version=7.13.0
sdk_version_code=71300

sdk_version=7.14.0
sdk_version_code=71400
sdk_platform=Android
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
android.nonFinalResIds=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.optimove.android;

import androidx.annotation.Nullable;

import com.optimove.android.main.tools.opti_logger.OptiLoggerStreamsContainer;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public final class AuthJwtResolver {

private AuthJwtResolver() {
}

/**
* When federated auth is configured and {@code userId} is non-empty, a JWT is required before sending
* user-identified requests. Returns true if {@code jwt} is missing after {@link #blockingJwt} (failure,
* timeout, or empty token).
*/
public static boolean isMissingRequiredJwt(
@Nullable AuthManager authManager,
@Nullable String userId,
@Nullable String jwt) {
if (authManager == null) {
return false;
}
if (userId == null || userId.trim().isEmpty()) {
return false;
}
return jwt == null || jwt.isEmpty();
}

@Nullable
public static String blockingJwt(@Nullable AuthManager authManager, @Nullable String userId, long timeoutMs) {
Comment thread
k-antipochkin marked this conversation as resolved.
if (authManager == null || userId == null || userId.isEmpty()) {
return null;
}
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<String> tokenRef = new AtomicReference<>();
AtomicReference<Exception> errorRef = new AtomicReference<>();
authManager.getToken(userId, (token, error) -> {
tokenRef.set(token);
errorRef.set(error);
latch.countDown();
});
try {
if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
OptiLoggerStreamsContainer.warn("JWT fetch timed out for user '%s' after %d ms", userId, timeoutMs);
return null;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
OptiLoggerStreamsContainer.warn("JWT fetch interrupted for user '%s'", userId);
return null;
}
Exception error = errorRef.get();
if (error != null) {
OptiLoggerStreamsContainer.warn("JWT fetch failed for user '%s': %s", userId, error.getMessage());
return null;
}
return tokenRef.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.optimove.android;

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

public final class AuthManager {
Comment thread
k-antipochkin marked this conversation as resolved.

private final @NonNull AuthTokenProvider provider;

public AuthManager(@NonNull AuthTokenProvider provider) {
this.provider = provider;
}

public void getToken(@Nullable String userId, @NonNull AuthTokenProvider.Callback completion) {
if (userId == null) {
completion.onComplete(null, new AuthTokenException(AuthTokenException.Kind.NO_USER_ID));
return;
}
String id = userId.trim();
if (id.isEmpty()) {
completion.onComplete(null, new AuthTokenException(AuthTokenException.Kind.NO_USER_ID));
return;
}
provider.getToken(id, (token, error) -> {
if (token != null) {
completion.onComplete(token, null);
} else {
completion.onComplete(null, error != null ? error : new AuthTokenException(AuthTokenException.Kind.TOKEN_FETCH_FAILED));
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.optimove.android;

import androidx.annotation.NonNull;

public final class AuthTokenException extends Exception {

public enum Kind {
TOKEN_FETCH_FAILED("Failed to fetch auth token from provider."),
NO_USER_ID("No userId available for auth token request.");

private final String message;

Kind(String message) {
this.message = message;
}
}

private final @NonNull Kind kind;

public AuthTokenException(@NonNull Kind kind) {
super(kind.message);
this.kind = kind;
}

public @NonNull Kind getKind() {
return kind;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.optimove.android;

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


public interface AuthTokenProvider {

void getToken(@NonNull String userId, @NonNull Callback callback);

@FunctionalInterface
interface Callback {
void onComplete(@Nullable String token, @Nullable Exception error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.optimove.android.AuthManager;
import com.optimove.android.AuthTokenProvider;
import com.optimove.android.embeddedmessaging.OptimoveEmbeddedMessaging;
import com.optimove.android.main.common.EventHandlerFactory;
import com.optimove.android.main.common.EventHandlerProvider;
Expand Down Expand Up @@ -72,6 +74,7 @@ final public class Optimove {
private final LifecycleObserver lifecycleObserver;

private static OptimoveConfig currentConfig;
private static @Nullable AuthManager authManager;

public enum IBeaconProximity {
UNKNOWN,
Expand Down Expand Up @@ -103,13 +106,17 @@ private Optimove(@NonNull Context context, OptimoveConfig config) {
this.localConfigKeysPreferences =
context.getSharedPreferences(TenantConfigsKeys.LOCAL_INIT_SP_FILE, Context.MODE_PRIVATE);
this.lifecycleObserver = new LifecycleObserver();
AuthTokenProvider authTokenProvider = config.getAuthTokenProvider();
AuthManager localAuthManager = authTokenProvider != null ? new AuthManager(authTokenProvider) : null;
Optimove.authManager = localAuthManager;
this.eventHandlerProvider = new EventHandlerProvider(EventHandlerFactory.builder()
.userInfo(userInfo)
.httpClient(HttpClient.getInstance())
.maximumBufferSize(OPTITRACK_BUFFER_SIZE)
.optistreamDbHelper(new OptistreamDbHelper(context))
.lifecycleObserver(lifecycleObserver)
.context(context)
.authManager(localAuthManager)
.build());

this.optimoveLifecycleEventGenerator = new OptimoveLifecycleEventGenerator(eventHandlerProvider, userInfo,
Expand Down Expand Up @@ -285,6 +292,11 @@ public static OptimoveConfig getConfig() {
return currentConfig;
}

@Nullable
public static AuthManager getAuthManager() {
return authManager;
}

/**
* Enables remote logs for investigations. Don't call it unless we explicitly asked you to.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.optimove.android;

public final class OptimoveAuthHeaders {

public static final String USER_JWT = "X-User-JWT";
public static final String AUTH_CAPABLE = "X-Optimove-Auth-Capable";
public static final String AUTH_CAPABLE_VALUE = "1";
public static final String PLATFORM = "X-Optimove-Platform";
public static final String PLATFORM_VALUE = "android";

private OptimoveAuthHeaders() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public final class OptimoveConfig {

private @Nullable EmbeddedMessagingConfig embeddedMessagingConfig;

private @Nullable AuthTokenProvider authTokenProvider;
private boolean overlayMessagingEnabled;
private @Nullable Integer overlayMessagingSessionLengthHours;

Expand Down Expand Up @@ -204,6 +205,10 @@ private void setMinLogLevel(@Nullable LogLevel minLogLevel) {
this.minLogLevel = minLogLevel;
}

private void setAuthTokenProvider(@Nullable AuthTokenProvider authTokenProvider) {
this.authTokenProvider = authTokenProvider;
}

void setCredentials(@Nullable String optimoveCredentials, @Nullable String optimobileCredentials) {
if (optimoveCredentials == null && optimobileCredentials == null) {
throw new IllegalArgumentException("Should provide at least optimove or optimobile credentials");
Expand Down Expand Up @@ -419,6 +424,10 @@ public boolean usesDelayedConfiguration() {
return this.embeddedMessagingConfig;
}

public @Nullable AuthTokenProvider getAuthTokenProvider() {
return this.authTokenProvider;
}

public boolean isOverlayMessagingEnabled() {
return this.overlayMessagingEnabled;
}
Expand Down Expand Up @@ -486,6 +495,7 @@ public static class Builder {

private @Nullable LogLevel minLogLevel;

private @Nullable AuthTokenProvider authTokenProvider;
private @Nullable Integer overlayMessagingSessionLengthHours;

/**
Expand Down Expand Up @@ -600,6 +610,24 @@ public Builder enableOverlayMessaging(int sessionLengthHours) {
return this;
}

/**
* Enables JWT-based federated authentication for user-identified SDK traffic.
* <p>
* When set, the SDK will call {@link AuthTokenProvider#getToken(String, AuthTokenProvider.Callback)} to obtain
* a JWT per request context; the token is sent as the {@code X-User-JWT} header.
* <p>
* <b>Threading:</b> the provider may be invoked from a background thread; the {@link AuthTokenProvider.Callback}
* may complete on any thread.
*
* @param provider non-null implementation that fetches JWTs for a given user id
* @return this builder
*/
@NonNull
public Builder enableAuth(@NonNull AuthTokenProvider provider) {
this.authTokenProvider = provider;
return this;
}

/**
* The minimum amount of time the user has to have left the app for a session end event to be
* recorded.
Expand Down Expand Up @@ -690,6 +718,7 @@ public OptimoveConfig build() {

newConfig.setMinLogLevel(this.minLogLevel);

newConfig.setAuthTokenProvider(this.authTokenProvider);
newConfig.overlayMessagingEnabled = this.overlayMessagingSessionLengthHours != null;
newConfig.overlayMessagingSessionLengthHours = this.overlayMessagingSessionLengthHours;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public EmbeddedMessageEventRequest(
this.customerId = customerId;
this.visitorId = visitorId;
}

public String getCustomerId() {
return customerId;
}

public JSONObject toJSONObject() throws JSONException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
JSONObject metricsObj = new JSONObject();
Expand Down
Loading
Loading