diff --git a/CHANGELOG.md b/CHANGELOG.md index 426e5274..a5fa13f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 19.5.0 +Project ID: the key the SDK is initialized with is now called the Project ID everywhere (SDK docs, README, dashboard). The value is the same, so every existing key keeps working and nothing has to change. `Gleap.initialize(String projectId, Application application)` keeps its signature; only the parameter name and its Javadoc changed. Log lines say "Project ID" instead of "SDK key" / "API key". + +The JWT provider restores a signed session after an app restart. `setUserJwt` keeps the token in memory only, so after a restart the SDK holds the session but no token, and the provider was only asked when a token expired. Now, when the session answer says the project runs Signed (`identity.mode: "signed"`, `signed: false`) for an identified session that has no grant (missing, or expired and dropped by the server's answer), no token or user hash is at hand and a provider is set, the SDK asks the provider once per app start and sends its token through the normal `setUserJwt` path. That identify is the one signed identify of the session (never a loop); the provider's rules still apply (one call at a time, 10 second timeout, 30 second pause after a failure); a null token sends nothing; a logout, an identify of the app or a grant arriving while the provider runs drops its token; a token for another user goes through the `identity_mismatch` recovery. `setUserJwtProvider` called after the session answer runs the same check, so the provider works whether it is set before or after `Gleap.initialize`. + ## 19.4.0 Customer identity: `Gleap.getInstance().setUserJwt(String)` identifies the user with one HS256 JWT signed by your backend. The SDK reads `userId` (or `user_id`) from the payload without checking the signature and sends the identify with the token as the user hash and no other contact fields; a token that is not three base64url parts, has no user id or is longer than 4096 characters is refused with a log line and nothing is sent. The token stays in memory only (it is never written to the session store) and is sent again whenever it says something new (another claim or key), even when nothing else did (`GleapSessionProperties.equals` compares the token's `kid` and claims without iat/exp/nbf/jti, so a token re-minted per app start for the same user and data is not sent again). `setUserJwtProvider(GleapUserJwtProvider)` fetches a fresh token (`callback.onToken(token)`) when the server answers `token_expired` (the identify is retried once) and before the identify is sent again (file session refresh, retry) when the token expires within 5 minutes; one call at a time, 10 second timeout, 30 second pause after a failure. An identify refused with `token_expired` or `token_invalid` keeps the session and the user (a warning is logged) instead of starting over as a guest; `signature_required` and other refusals behave as before (`signature_required` now logs why). `setAuthTokens(Map)` stores per-user tokens for API tools on the signed contact (`POST /sessions/auth-tokens`, a null value removes one), keeps them in memory and sends them again after a signed identify on a new session. `clearIdentity` drops the token and the auth tokens; the provider stays. diff --git a/README.md b/README.md index c09f36ad..b0888bdd 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,10 @@ import io.gleap.Gleap; The last step is to initialize the Gleap SDK by adding the following Code to the ```onCreate``` method: ``` -Gleap.initialize("YOUR_API_KEY", this); +Gleap.initialize("YOUR_PROJECT_ID", this); ``` -(Your API key can be found in the project settings within Gleap) +Your Project ID is shown in the Gleap dashboard on the SDK install page and under Settings → Project. It used to be called "SDK key" or "API key": the value is the same, so existing keys keep working. ## Data regions @@ -64,7 +64,7 @@ Gleap projects are hosted in the EU by default. If your project lives in the US ``` Gleap.getInstance().setRegion("us"); -Gleap.initialize("YOUR_API_KEY", this); +Gleap.initialize("YOUR_PROJECT_ID", this); ``` `setRegion` accepts `"eu"` (default) or `"us"` (case-insensitive) and sets all regional hosts at once. Unknown values are ignored with a warning. @@ -119,13 +119,14 @@ Without it nothing changes: banners overlay the app as before. ### Signed identity (recommended) -Your backend signs an HS256 JWT with an identity key from Settings → Security → Identity keys (put the key id in the `kid` header; the secret never leaves your server). The `userId` claim is required. Optional claims: `email`, `name`, `company` (`{ id, name }`), `plan`, and provider ids such as `stripeCustomerId`. Set `exp` to at most one hour. Everything in the token lands on the contact as signed data the app cannot change. Pass the token to the SDK after login and on every app start of a logged-in user; `setUserJwt` keeps it in memory only. `setUserJwtProvider` fetches a fresh token when it expires, and `setAuthTokens` hands Kai your own API tokens for this user. See [Customer identity](https://docs.gleap.ai/documentation/guides/customer-identity). +Your backend signs an HS256 JWT with an identity key from Settings → Security → Identity keys (put the key id in the `kid` header; the secret never leaves your server). The `userId` claim is required. Optional claims: `email`, `name`, `company` (`{ id, name }`), `plan`, and provider ids such as `stripeCustomerId`. Set `exp` to at most one hour. Everything in the token lands on the contact as signed data the app cannot change. Pass the token to the SDK after login and on every app start of a logged-in user; `setUserJwt` keeps it in memory only. `setUserJwtProvider` fetches a fresh token when it expires, and also signs the session again after an app restart: when the server reports Signed mode without a valid grant and no token was passed to `setUserJwt`, the provider is asked once per app start and its token is sent like one passed to `setUserJwt` (a null token sends nothing). The provider can be set before or after `Gleap.initialize`. `setAuthTokens` hands Kai your own API tokens for this user. See [Customer identity](https://docs.gleap.ai/documentation/guides/customer-identity). ```java // After login and on every app start of a logged-in user Gleap.getInstance().setUserJwt(token); -// Called when the token has expired or is about to; pass null when it failed +// Called when the token has expired or is about to, and once per app start to restore a +// signed session when no token was passed; pass null when it failed Gleap.getInstance().setUserJwtProvider(callback -> myBackend.fetchGleapToken(token -> callback.onToken(token)) ); diff --git a/app/src/main/java/gleap/io/gleap_android_sdk/MainApplication.java b/app/src/main/java/gleap/io/gleap_android_sdk/MainApplication.java index 8d020917..2b74d4b6 100644 --- a/app/src/main/java/gleap/io/gleap_android_sdk/MainApplication.java +++ b/app/src/main/java/gleap/io/gleap_android_sdk/MainApplication.java @@ -45,7 +45,7 @@ public void onError(Throwable error, String context) { } }); - String sdkKey = "ogWhNhuiZcGWrva5nlDS8l7a78OfaLlV"; + String projectId = "ogWhNhuiZcGWrva5nlDS8l7a78OfaLlV"; // Debug builds only: every request goes to a local stub server instead of Gleap, e.g. // adb shell setprop debug.gleap.stub http://10.0.2.2:8787 (then restart the app). String stub = BuildConfig.DEBUG ? systemProperty("debug.gleap.stub") : ""; @@ -56,10 +56,10 @@ public void onError(Throwable error, String context) { Gleap.getInstance().setBannerUrl(stub + "/outbound"); Gleap.getInstance().setModalUrl(stub + "/outbound/modal"); Gleap.getInstance().setRealtimeHost(android.net.Uri.parse(stub).getAuthority()); - sdkKey = "stub-sdk-key"; + projectId = "stub-sdk-key"; } - Gleap.initialize(sdkKey, this); + Gleap.initialize(projectId, this); // The widget callbacks: a capture closes and reopens the widget, which the app does not // hear about (one widget session). diff --git a/gleap/src/main/java/io/gleap/ConfigLoader.java b/gleap/src/main/java/io/gleap/ConfigLoader.java index 95d3f362..38282f6b 100644 --- a/gleap/src/main/java/io/gleap/ConfigLoader.java +++ b/gleap/src/main/java/io/gleap/ConfigLoader.java @@ -44,7 +44,7 @@ protected void onPostExecute(JSONObject result) { protected JSONObject doInBackground(GleapBug... gleapBugs) { String sdkKey = GleapConfig.getInstance().getSdkKey(); if (sdkKey == null || sdkKey.trim().isEmpty()) { - GleapLog.e("SDK key is missing in ConfigLoader"); + GleapLog.e("Project ID is missing in ConfigLoader"); return new JSONObject(); } @@ -111,7 +111,7 @@ public void run() { GleapCallbacks.getInstance().deliverConfigLoaded(result.getJSONObject("flowConfig")); GleapCallbacks.getInstance().deliverInitialized(); } else { - GleapErrors.report(new Exception("Config could not be loaded. Incorrect API key."), "Gleap config loader"); + GleapErrors.report(new Exception("Config could not be loaded. Incorrect Project ID."), "Gleap config loader"); } } diff --git a/gleap/src/main/java/io/gleap/Gleap.java b/gleap/src/main/java/io/gleap/Gleap.java index 48b6b93b..ddc56f63 100644 --- a/gleap/src/main/java/io/gleap/Gleap.java +++ b/gleap/src/main/java/io/gleap/Gleap.java @@ -62,11 +62,13 @@ public static Gleap getInstance() { /** * Auto-configures the Gleap SDK from the remote config. * - * @param sdkKey The SDK key, which can be found on dashboard.Gleap.io + * @param projectId The Project ID of your Gleap project (formerly called "SDK key" or "API + * key"; existing keys keep working unchanged). Find it in the Gleap + * dashboard under Settings → Project or on the SDK install page. * @param application used to have context and access to take screenshot */ - public static void initialize(String sdkKey, Application application) { - GleapInitializer.initialize(sdkKey, application); + public static void initialize(String projectId, Application application) { + GleapInitializer.initialize(projectId, application); } public void processOpenPushActions() { @@ -619,7 +621,11 @@ static void clearPendingUserJwt() { /** * Sets the provider that fetches a fresh identity token when the current one expired or is - * about to. One call at a time, 10 s timeout, 30 s pause after a failure. + * about to, and once per app start to sign the session again when the server reports Signed + * mode without a valid grant and no token was passed to {@link #setUserJwt(String)} (the + * token is kept in memory only, so an app restart has none). One call at a time, 10 s + * timeout, 30 s pause after a failure. Can be set before or after + * {@link #initialize(String, Application)}. * * @param provider fetches the token and passes it to {@code callback.onToken(token)} */ @@ -627,6 +633,11 @@ static void clearPendingUserJwt() { public void setUserJwtProvider(GleapUserJwtProvider provider) { try { GleapUserJwtProviderGate.getInstance().setProvider(provider); + // Set after the session answer arrived: a signed session can still be restored. + GleapSessionController controller = GleapSessionController.getInstance(); + if (provider != null && controller != null) { + controller.identifyWithProviderForSignedModeIfNeeded(); + } } catch (Error | Exception exception) { handleError(exception, "setUserJwtProvider"); } @@ -1125,7 +1136,7 @@ public void setLogFlushHandler(GleapLogFlushHandler handler) { /** * This is called, when the config is received from the server. The config is loaded once per * process: a callback set after it was loaded is called once with the loaded config, posted - * to the main thread. Calling {@link Gleap#initialize} again with the same SDK key hands the + * to the main thread. Calling {@link Gleap#initialize} again with the same Project ID hands the * loaded config to the set callback again, like on iOS. * * @param configLoadedCallback callback which is called @@ -1138,7 +1149,7 @@ public void setConfigLoadedCallback(ConfigLoadedCallback configLoadedCallback) { /** * This is called, when Gleap got initialized (the config was received from the server). A * callback set after that is called once, posted to the main thread. Calling - * {@link Gleap#initialize} again with the same SDK key calls the set callback again, like on + * {@link Gleap#initialize} again with the same Project ID calls the set callback again, like on * iOS. * * @param initializedCallback callback which is called diff --git a/gleap/src/main/java/io/gleap/GleapBaseSessionService.java b/gleap/src/main/java/io/gleap/GleapBaseSessionService.java index 80308744..a08b546e 100644 --- a/gleap/src/main/java/io/gleap/GleapBaseSessionService.java +++ b/gleap/src/main/java/io/gleap/GleapBaseSessionService.java @@ -76,6 +76,12 @@ private void performSessionRequest(int generation) throws Exception { JSONObject result = GleapHttp.readLastJsonLine(conn.getInputStream()); if (GleapSessionController.getInstance() != null) { GleapSessionController.getInstance().processSessionActionResult(result, true, true, generation); + // Signed mode without a grant and no token or hash to send: the JWT provider is asked for one. + try { + GleapSessionController.getInstance().identifyWithProviderForSignedModeIfNeeded(); + } catch (Error | Exception error) { + GleapErrors.report(error, "identifyWithProviderForSignedMode"); + } } } catch (Exception e) { if (GleapSessionController.getInstance() != null) { diff --git a/gleap/src/main/java/io/gleap/GleapInitializer.java b/gleap/src/main/java/io/gleap/GleapInitializer.java index 288ea53e..3778715c 100644 --- a/gleap/src/main/java/io/gleap/GleapInitializer.java +++ b/gleap/src/main/java/io/gleap/GleapInitializer.java @@ -28,7 +28,7 @@ static Application getApplication() { static void initialize(String sdkKey, Application app) { if (sdkKey == null || sdkKey.trim().isEmpty()) { - GleapErrors.report(new IllegalArgumentException("Gleap SDK key is missing or empty."), "initialize"); + GleapErrors.report(new IllegalArgumentException("Gleap Project ID is missing or empty."), "initialize"); return; } diff --git a/gleap/src/main/java/io/gleap/GleapSessionController.java b/gleap/src/main/java/io/gleap/GleapSessionController.java index abf75d90..d501e518 100644 --- a/gleap/src/main/java/io/gleap/GleapSessionController.java +++ b/gleap/src/main/java/io/gleap/GleapSessionController.java @@ -47,6 +47,10 @@ public class GleapSessionController { // returning user on a project that switched to Signed gets one identify per app start and // session, never a loop. private String signedGrantRequestedFor; + // The same answer when no token or hash is at hand (setUserJwt's token lives in memory only, + // so an app restart has none) but a JWT provider is set: the provider is asked once per app + // start for a token to identify with. Reset on logout. + private boolean signedModeProviderAsked; // A grant is renewed (the last signed identify sent again) this long before it expires. static final long GRANT_RENEW_MARGIN_MS = 5 * 60 * 1000; private static final long GRANT_RENEW_MIN_INTERVAL_MS = 60 * 1000; @@ -296,6 +300,7 @@ public void clearUserSession() { gleapSession = null; isSessionLoaded = false; identityMode = null; + signedModeProviderAsked = false; // The grant goes with the session (store.clear removed the stored copy). setGrantInMemory(null, null); } @@ -401,6 +406,62 @@ boolean claimSignedGrantRequest(GleapSessionProperties identify) { } } + /** + * Signed mode on an identified session without a grant, and no token or hash at hand to send + * for one (an app restart after setUserJwt: the token lives in memory only). Holds the lock. + */ + private boolean signedModeWithoutHeldToken() { + synchronized (this) { + GleapSession session = gleapSession; + GleapSessionProperties user = gleapSessionProperties; + if (session == null || user == null || !signedGrantMissing(session.getId(), user.getUserId())) { + return false; + } + // A held token or hash goes out as the held identify instead (see processSessionActionResult). + boolean heldIdentify = lastIdentify != null && lastIdentify.getHash() != null && !lastIdentify.getHash().isEmpty(); + boolean heldHash = user.getHash() != null && !user.getHash().isEmpty(); + return !heldIdentify && !heldHash; + } + } + + /** + * Restores a signed session from the JWT provider alone: when the session answer says the + * project runs Signed, the identified session has no grant and no token or hash is at hand, + * the provider is asked once per app start and its token is sent through + * {@link Gleap#setUserJwt(String)}, like a token from the app (that identify is the one + * signed identify of this session, see {@link #claimSignedGrantRequest}). The provider's + * rules apply (one call at a time, 10 s timeout, 30 s pause after a failure); a null token + * sends nothing. A logout, an identify of the app or a grant arriving meanwhile drops the + * token. Also run when the provider is set after the session answer. + */ + void identifyWithProviderForSignedModeIfNeeded() { + final int generation; + synchronized (this) { + if (signedModeProviderAsked || !isSessionLoaded || pendingIdentificationAction != null + || GleapIdentifyService.isRunning() || !GleapUserJwtProviderGate.getInstance().hasProvider() + || !signedModeWithoutHeldToken()) { + return; + } + signedModeProviderAsked = true; + generation = currentGeneration(); + } + GleapUserJwtProviderGate.getInstance().requestToken(new GleapUserJwtProviderGate.TokenCallback() { + @Override + public void onResult(String token) { + if (token == null) { + return; + } + synchronized (GleapSessionController.this) { + if (!isCurrentGeneration(generation) || pendingIdentificationAction != null + || GleapIdentifyService.isRunning() || !signedModeWithoutHeldToken()) { + return; + } + } + Gleap.getInstance().setUserJwt(token); + } + }); + } + private void setGrantInMemory(String value, String sessionId) { if (value == null || value.isEmpty() || sessionId == null || sessionId.isEmpty()) { grant = null; diff --git a/gleap/src/main/java/io/gleap/iGleap.java b/gleap/src/main/java/io/gleap/iGleap.java index d7c92d27..3f575e26 100644 --- a/gleap/src/main/java/io/gleap/iGleap.java +++ b/gleap/src/main/java/io/gleap/iGleap.java @@ -323,10 +323,14 @@ interface iGleap { * Sets the provider that fetches a fresh identity token from your backend. Gleap calls it * when the server answers that the token expired (and retries once with the new token), and * before it sends the identify again (file session refresh, replay) when the token expires - * within 5 minutes. One call at a time; a call without an answer within 10 seconds counts as - * failed, and after a failure Gleap waits 30 seconds before it asks again. Pass null to - * {@code callback.onToken} when fetching failed. {@link #clearIdentity()} keeps the provider; - * pass null here to remove it. + * within 5 minutes. It is also asked once per app start to sign the session again when the + * server reports Signed mode without a valid grant and no token was passed to + * {@link #setUserJwt(String)} (the token is kept in memory only, so an app restart has none); + * its token is then sent like one passed to setUserJwt. One call at a time; a call without an + * answer within 10 seconds counts as failed, and after a failure Gleap waits 30 seconds before + * it asks again. Pass null to {@code callback.onToken} when fetching failed (nothing is sent). + * Can be set before or after {@code Gleap.initialize}. {@link #clearIdentity()} keeps the + * provider; pass null here to remove it. * * @param provider fetches the token and passes it to {@code callback.onToken(token)} */ @@ -653,7 +657,7 @@ interface iGleap { /** * This is called, when the config is received from the server. The config is loaded once per * process: a callback set after it was loaded is called once with the loaded config, posted - * to the main thread. Calling {@link Gleap#initialize} again with the same SDK key hands the + * to the main thread. Calling {@link Gleap#initialize} again with the same Project ID hands the * loaded config to the set callback again, like on iOS. * * @param configLoadedCallback callback which is called @@ -663,7 +667,7 @@ interface iGleap { /** * This is called, when Gleap got initialized (the config was received from the server). A * callback set after that is called once, posted to the main thread. Calling - * {@link Gleap#initialize} again with the same SDK key calls the set callback again, like on + * {@link Gleap#initialize} again with the same Project ID calls the set callback again, like on * iOS. * * @param initializedCallback callback which is called diff --git a/gleap/src/test/java/io/gleap/SignedModeProviderRestoreTest.java b/gleap/src/test/java/io/gleap/SignedModeProviderRestoreTest.java new file mode 100644 index 00000000..bab7f37d --- /dev/null +++ b/gleap/src/test/java/io/gleap/SignedModeProviderRestoreTest.java @@ -0,0 +1,250 @@ +package io.gleap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +/** + * Signed mode restored by the JWT provider alone: after an app restart the SDK holds the session + * (and maybe an expired or rejected grant) but not the token setUserJwt was given, which lives in + * memory only. When the session answer says Signed mode without a grant for an identified + * session, the provider is asked once per app start and its token is sent like a token from + * setUserJwt. + */ +public class SignedModeProviderRestoreTest { + private SdkTestEnvironment sdk; + private GleapUserJwtProviderGateTest.ManualScheduler scheduler; + private int providerCalls; + private GleapUserJwtProvider.Callback pendingCallback; + // What the provider answers right away; null leaves the call pending (see pendingCallback). + private String providerToken; + private boolean providerAnswersNull; + + @Before + public void setUp() { + sdk = new SdkTestEnvironment(); + sdk.storeSession("id-1", "hash-1"); + scheduler = new GleapUserJwtProviderGateTest.ManualScheduler(); + GleapUserJwtProviderGate.setInstanceForTesting(new GleapUserJwtProviderGate(scheduler, scheduler)); + } + + @After + public void tearDown() { + GleapUserJwtProviderGate.setInstanceForTesting(null); + sdk.tearDown(); + } + + private static long inSeconds(long seconds) { + return System.currentTimeMillis() / 1000 + seconds; + } + + private static String token(String userId) { + return GleapUserJwtTest.jwt("{\"userId\":\"" + userId + "\",\"exp\":" + inSeconds(3600) + "}"); + } + + private static String grant(String sid) { + return GleapUserJwtTest.jwt("{\"sid\":\"" + sid + "\",\"uid\":\"user-1\",\"kind\":\"jwt\",\"exp\":" + inSeconds(3600) + "}"); + } + + private static String signedWithoutGrant(String id) { + return "{\"gleapId\":\"" + id + "\",\"gleapHash\":\"hash-" + id + "\",\"userId\":\"user-1\"," + + "\"identity\":{\"signed\":false,\"grant\":null,\"mode\":\"signed\"}}"; + } + + private static String signedAnswer(String id, String grant) { + return "{\"gleapId\":\"" + id + "\",\"gleapHash\":\"hash-" + id + "\",\"userId\":\"user-1\"," + + "\"identity\":{\"signed\":true,\"grant\":\"" + grant + "\"}}"; + } + + /** + * The previous app start identified user-1 with setUserJwt: the user is stored, the token is not. + */ + private void restartAsIdentifiedUser() { + sdk.store.putString("userId", "user-1"); + sdk.controller = new GleapSessionController(sdk.store); + GleapSessionController.setInstanceForTesting(sdk.controller); + } + + private void setProvider() { + Gleap.getInstance().setUserJwtProvider(new GleapUserJwtProvider() { + @Override + public void fetchToken(Callback callback) { + providerCalls++; + if (providerAnswersNull) { + callback.onToken(null); + } else if (providerToken != null) { + callback.onToken(providerToken); + } else { + pendingCallback = callback; + } + } + }); + } + + /** + * A session load: the session answer, the provider call it may post, and the identify it may queue. + */ + private void loadSession() { + new GleapBaseSessionService().doInBackground(); + scheduler.runPosted(); + new GleapIdentifyService().doInBackground(); + } + + @Test + public void theProviderIsAskedOnceAndTheIdentifyCarriesItsToken() throws Exception { + restartAsIdentifiedUser(); + providerToken = token("user-1"); + setProvider(); + sdk.server.respond("/sessions", 200, signedWithoutGrant("id-1")); + sdk.server.respond("/sessions/identify", 200, signedAnswer("id-1", grant("id-1"))); + + loadSession(); + + assertEquals(1, providerCalls); + List identifies = sdk.server.requestsTo("/sessions/identify"); + assertEquals(1, identifies.size()); + assertEquals(providerToken, identifies.get(0).bodyJson().getString("userHash")); + assertNotNull(sdk.controller.getGrant("id-1")); + } + + @Test + public void repeatedUnsignedAnswersNeverLoop() { + restartAsIdentifiedUser(); + providerToken = token("user-1"); + setProvider(); + sdk.server.respond("/sessions", 200, signedWithoutGrant("id-1")); + // The server keeps answering without a grant. + sdk.server.respond("/sessions/identify", 200, signedWithoutGrant("id-1")); + + loadSession(); + loadSession(); + loadSession(); + + assertEquals(1, providerCalls); + assertEquals(1, sdk.server.requestsTo("/sessions/identify").size()); + } + + @Test + public void aNullTokenSendsNothingAndIsNotAskedAgain() { + restartAsIdentifiedUser(); + providerAnswersNull = true; + setProvider(); + sdk.server.respond("/sessions", 200, signedWithoutGrant("id-1")); + + loadSession(); + scheduler.now += GleapUserJwtProviderGate.PAUSE_AFTER_FAILURE_MS + 1; + loadSession(); + + assertEquals(1, providerCalls); + assertEquals(0, sdk.server.requestsTo("/sessions/identify").size()); + assertNull(sdk.controller.getPendingIdentificationAction()); + } + + @Test + public void withoutAProviderNothingIsSent() { + restartAsIdentifiedUser(); + sdk.server.respond("/sessions", 200, signedWithoutGrant("id-1")); + + loadSession(); + + assertEquals(0, sdk.server.requestsTo("/sessions/identify").size()); + } + + @Test + public void aProviderSetAfterTheSessionAnswerIsAsked() throws Exception { + restartAsIdentifiedUser(); + sdk.server.respond("/sessions", 200, signedWithoutGrant("id-1")); + sdk.server.respond("/sessions/identify", 200, signedAnswer("id-1", grant("id-1"))); + loadSession(); + assertEquals(0, sdk.server.requestsTo("/sessions/identify").size()); + + providerToken = token("user-1"); + setProvider(); + scheduler.runPosted(); + new GleapIdentifyService().doInBackground(); + + assertEquals(1, providerCalls); + assertEquals(providerToken, sdk.server.last("/sessions/identify").bodyJson().getString("userHash")); + } + + @Test + public void aClassicAnswerAsksNothing() { + restartAsIdentifiedUser(); + providerToken = token("user-1"); + setProvider(); + sdk.server.respond("/sessions", 200, "{\"gleapId\":\"id-1\",\"gleapHash\":\"hash-id-1\",\"userId\":\"user-1\"}"); + + loadSession(); + + assertEquals(0, providerCalls); + assertEquals(0, sdk.server.requestsTo("/sessions/identify").size()); + } + + @Test + public void anAnonymousSessionAsksNothing() { + providerToken = token("user-1"); + setProvider(); + sdk.server.respond("/sessions", 200, + "{\"gleapId\":\"id-1\",\"gleapHash\":\"hash-id-1\",\"identity\":{\"signed\":false,\"grant\":null,\"mode\":\"signed\"}}"); + + loadSession(); + + assertEquals(0, providerCalls); + } + + @Test + public void aTokenFromTheAppIsSentInsteadOfAskingTheProvider() throws Exception { + restartAsIdentifiedUser(); + providerToken = token("user-1"); + setProvider(); + String appToken = token("user-1"); + sdk.server.respond("/sessions", 200, signedWithoutGrant("id-1")); + sdk.server.respond("/sessions/identify", 200, signedAnswer("id-1", grant("id-1"))); + + Gleap.getInstance().setUserJwt(appToken); + loadSession(); + + assertEquals(0, providerCalls); + assertEquals(1, sdk.server.requestsTo("/sessions/identify").size()); + assertEquals(appToken, sdk.server.last("/sessions/identify").bodyJson().getString("userHash")); + } + + @Test + public void aLegacyHashKeptFromTheLastStartIsSentInsteadOfAskingTheProvider() throws Exception { + sdk.store.putString("hash", "legacy-hash"); + restartAsIdentifiedUser(); + providerToken = token("user-1"); + setProvider(); + sdk.server.respond("/sessions", 200, signedWithoutGrant("id-1")); + sdk.server.respond("/sessions/identify", 200, signedAnswer("id-1", grant("id-1"))); + + loadSession(); + + assertEquals(0, providerCalls); + assertEquals("legacy-hash", sdk.server.last("/sessions/identify").bodyJson().getString("userHash")); + } + + @Test + public void aLogoutWhileTheProviderRunsDropsItsToken() { + restartAsIdentifiedUser(); + setProvider(); + sdk.server.respond("/sessions", 200, signedWithoutGrant("id-1")); + + loadSession(); + assertEquals(1, providerCalls); + assertNotNull(pendingCallback); + + sdk.controller.clearUserSession(); + pendingCallback.onToken(token("user-1")); + new GleapIdentifyService().doInBackground(); + + assertNull(sdk.controller.getPendingIdentificationAction()); + assertEquals(0, sdk.server.requestsTo("/sessions/identify").size()); + } +} diff --git a/gradle.properties b/gradle.properties index 7d01df3b..87d930c4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,8 +13,8 @@ android.useAndroidX=true # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=19.4.0 -VERSION_CODE=194000 +VERSION_NAME=19.5.0 +VERSION_CODE=195000 GROUP=io.gleap POM_ARTIFACT_ID=gleap-android-sdk POM_NAME=gleap-android-sdk