From ca8339ee88242b4d3660b410f9b3c1645996264f Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 21 Oct 2020 10:58:02 -0300 Subject: [PATCH 01/18] Revert "Add license scan report and status" --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 9e157d317..5db93cb63 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](http://doge.mit-license.org) [![Maven Central](https://img.shields.io/maven-central/v/com.auth0.android/auth0.svg?style=flat-square)](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.auth0.android%22%20AND%20a%3A%22auth0%22) [![Bintray](https://img.shields.io/bintray/v/auth0/android/auth0.svg?style=flat-square)](https://bintray.com/auth0/android/auth0/_latestVersion) -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fauth0%2FAuth0.Android.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fauth0%2FAuth0.Android?ref=badge_shield) Android Java toolkit for Auth0 API @@ -857,6 +856,3 @@ If you have found a bug or if you have a feature request, please report them at ## License This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. - - -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fauth0%2FAuth0.Android.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fauth0%2FAuth0.Android?ref=badge_large) \ No newline at end of file From 84a2fd57071b9f65bcc2b2bdccf3f0196bb47159 Mon Sep 17 00:00:00 2001 From: lbalmaceda Date: Tue, 27 Oct 2020 07:15:59 -0300 Subject: [PATCH 02/18] Setup the CODEOWNERS for pull request reviews --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9ff49218..60f116c05 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/dx-sdks-approver +* @auth0/dx-sdks-engineer From 4aa9c17407aa2cd803e9a7f4aafd00dce6b9d7a1 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 29 Oct 2020 17:11:33 -0300 Subject: [PATCH 03/18] Setup pull-request and issue templates --- .github/ISSUE_TEMPLATE.md | 41 ----------------- .github/ISSUE_TEMPLATE/config.yml | 5 +++ .github/ISSUE_TEMPLATE/feature_request.md | 39 ++++++++++++++++ .github/ISSUE_TEMPLATE/report_a_bug.md | 55 +++++++++++++++++++++++ 4 files changed, 99 insertions(+), 41 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/report_a_bug.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 682cf2afc..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,41 +0,0 @@ -In order to efficiently and accurately address your issue or feature request, please read through the template below and answer all relevant questions. Your additional work here is greatly appreciated and will help us respond as quickly as possible. Please delete any sections or questions below that do not pertain to this request. - -For general support or usage questions, please use the [Auth0 Community](https://community.auth0.com/) or [Auth0 Support](https://support.auth0.com.). - -### Description - -Description of the bug or feature request and why it's a problem. Consider including: - -- The use case or overall problem you're trying to solve -- Information about when the problem started - -### Prerequisites - -- [ ] I have checked the documentation for this library [Add a link]. - -- [ ] I have checked the [Auth0 Community](https://community.auth0.com/) for related posts. - -- [ ] I have checked for related or duplicate [Issues](https://github.com/auth0/Auth0.Android/issues) and [PRs](https://github.com/auth0/Auth0.Android/pulls). - -- [ ] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md). - -- [ ] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). - -### Environment - -Please provide the following: - -- Version of Auth0.Android used: -- Version of Android being used: -- Additional packages that might be affecting your instance: - -### Reproduction - -Detail the steps taken to reproduce this error and note if this issue can be reproduced consistently or if it is intermittent. - -Please include: - -- Specific devices affected -- Log files or stacktraces (redact/remove sensitive information) -- Snippet of the code you're running (redact/remove sensitive information) -- Screenshots, if helpful diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..b785e5273 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Auth0 Community + url: https://community.auth0.com/c/sdks/5 + about: Discuss this SDK in the Auth0 Community forums \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..9faeb3c50 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,39 @@ +--- + name: Feature request + about: Suggest an idea or a feature for this project + title: '' + labels: feature request + assignees: '' + --- + + + + ### Describe the problem you'd like to have solved + + + + ### Describe the ideal solution + + + + ## Alternatives and current work-arounds + + + + ### Additional information, if any + + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/report_a_bug.md b/.github/ISSUE_TEMPLATE/report_a_bug.md new file mode 100644 index 000000000..5786565ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/report_a_bug.md @@ -0,0 +1,55 @@ +--- + name: Report a bug + about: Have you found a bug or issue? Create a bug report for this SDK + title: '' + labels: bug report + assignees: '' + --- + + + + ### Describe the problem + + + + ### What was the expected behavior? + + + + ### Reproduction + + + - Step 1.. + - Step 2.. + - ... + + ### Environment + + + + - **Version of this library used:** + - **Which framework are you using, if applicable:** + - **Other modules/plugins/libraries that might be involved:** + - **Any other relevant information you think would be useful:** \ No newline at end of file From 94304ff3ee7ef47ba8a29a664608d8827b491ef6 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 21 Oct 2020 09:40:09 -0300 Subject: [PATCH 04/18] allow to pass minTTL and scope to the CredentialsManager --- .../storage/CredentialsManager.java | 87 +++++++++-- .../storage/CredentialsManagerTest.java | 146 +++++++++++++++++- 2 files changed, 215 insertions(+), 18 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java index effa3221d..8b12fdda5 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java @@ -9,9 +9,11 @@ import com.auth0.android.callback.AuthenticationCallback; import com.auth0.android.callback.BaseCallback; import com.auth0.android.jwt.JWT; +import com.auth0.android.request.ParameterizableRequest; import com.auth0.android.result.Credentials; import com.auth0.android.util.Clock; +import java.util.Arrays; import java.util.Date; import static android.text.TextUtils.isEmpty; @@ -73,16 +75,7 @@ public void saveCredentials(@NonNull Credentials credentials) { throw new CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value."); } - long expiresAt = credentials.getExpiresAt().getTime(); - - if (credentials.getIdToken() != null) { - JWT idToken = jwtDecoder.decode(credentials.getIdToken()); - Date idTokenExpiresAtDate = idToken.getExpiresAt(); - - if (idTokenExpiresAtDate != null) { - expiresAt = Math.min(idTokenExpiresAtDate.getTime(), expiresAt); - } - } + long expiresAt = calculateExpiresAt(credentials); storage.store(KEY_ACCESS_TOKEN, credentials.getAccessToken()); storage.store(KEY_REFRESH_TOKEN, credentials.getRefreshToken()); @@ -100,24 +93,42 @@ public void saveCredentials(@NonNull Credentials credentials) { * * @param callback the callback that will receive a valid {@link Credentials} or the {@link CredentialsManagerException}. */ - public void getCredentials(@NonNull final BaseCallback callback) { + public void getCredentials(@NonNull BaseCallback callback) { + getCredentials(null, 0, callback); + } + + /** + * Retrieves the credentials from the storage and refresh them if they have already expired. + * It will fail with {@link CredentialsManagerException} if the saved access_token or id_token is null, + * or if the tokens have already expired and the refresh_token is null. + * + * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. + * @param minTtl the minimum time in seconds that both the access token and id token should last before expiration. + * @param callback the callback that will receive a valid {@link Credentials} or the {@link CredentialsManagerException}. + */ + public void getCredentials(@Nullable String scope, final int minTtl, @NonNull final BaseCallback callback) { String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN); final String refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN); String idToken = storage.retrieveString(KEY_ID_TOKEN); String tokenType = storage.retrieveString(KEY_TOKEN_TYPE); Long expiresAt = storage.retrieveLong(KEY_EXPIRES_AT); - String scope = storage.retrieveString(KEY_SCOPE); + String storedScope = storage.retrieveString(KEY_SCOPE); Long cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT); if (cacheExpiresAt == null) { cacheExpiresAt = expiresAt; } - if (isEmpty(accessToken) && isEmpty(idToken) || expiresAt == null) { + boolean hasEmptyCredentials = isEmpty(accessToken) && isEmpty(idToken) || expiresAt == null; + if (hasEmptyCredentials) { callback.onFailure(new CredentialsManagerException("No Credentials were previously set.")); return; } - if (cacheExpiresAt > getCurrentTimeInMillis()) { - callback.onSuccess(recreateCredentials(idToken, accessToken, tokenType, refreshToken, new Date(expiresAt), scope)); + + boolean willExpire = willExpire(cacheExpiresAt, minTtl); + boolean scopeChanged = hasScopeChanged(storedScope, scope); + + if (!willExpire && !scopeChanged) { + callback.onSuccess(recreateCredentials(idToken, accessToken, tokenType, refreshToken, new Date(expiresAt), storedScope)); return; } if (refreshToken == null) { @@ -125,9 +136,22 @@ public void getCredentials(@NonNull final BaseCallback() { + final ParameterizableRequest request = authClient.renewAuth(refreshToken); + if (scope != null) { + request.addParameter("scope", scope); + } + request.start(new AuthenticationCallback() { @Override public void onSuccess(@Nullable Credentials fresh) { + long nextCacheExpiresAt = calculateExpiresAt(fresh); + boolean willExpire = willExpire(nextCacheExpiresAt, minTtl); + if (willExpire) { + long tokenLifetime = (nextCacheExpiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; + CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token or Id Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); + callback.onFailure(wrongTtlException); + return; + } + //non-empty refresh token for refresh token rotation scenarios String updatedRefreshToken = isEmpty(fresh.getRefreshToken()) ? refreshToken : fresh.getRefreshToken(); Credentials credentials = new Credentials(fresh.getIdToken(), fresh.getAccessToken(), fresh.getType(), updatedRefreshToken, fresh.getExpiresAt(), fresh.getScope()); @@ -140,6 +164,37 @@ public void onFailure(@NonNull AuthenticationException error) { callback.onFailure(new CredentialsManagerException("An error occurred while trying to use the Refresh Token to renew the Credentials.", error)); } }); + + } + + private boolean hasScopeChanged(@NonNull String storedScope, @Nullable String requiredScope) { + if (requiredScope == null) { + return false; + } + String[] stored = storedScope.split(" "); + Arrays.sort(stored); + String[] required = requiredScope.split(" "); + Arrays.sort(required); + return stored != required; + } + + private boolean willExpire(long expiresAt, long minTtl) { + long nextClock = getCurrentTimeInMillis() + minTtl * 1000; + return expiresAt <= nextClock; + } + + private long calculateExpiresAt(@NonNull Credentials credentials) { + long expiresAt = credentials.getExpiresAt().getTime(); + + if (credentials.getIdToken() != null) { + JWT idToken = jwtDecoder.decode(credentials.getIdToken()); + Date idTokenExpiresAtDate = idToken.getExpiresAt(); + + if (idTokenExpiresAtDate != null) { + expiresAt = Math.min(idTokenExpiresAtDate.getTime(), expiresAt); + } + } + return expiresAt; } /** diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java index e15416a70..77569e6dd 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java @@ -40,6 +40,7 @@ import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -344,6 +345,106 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyAccessTokenIsAvaila assertThat(retrievedCredentials.getScope(), is("scope")); } + @Test + public void shouldRenewCredentialsWhenScopeHasChanged() { + when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); + when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); + when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); + when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); + long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; //non expired credentials + when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); + when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); + when(storage.retrieveString("com.auth0.scope")).thenReturn("some new scope"); + when(client.renewAuth("refreshToken")).thenReturn(request); + + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); + JWT jwtMock = mock(JWT.class); + when(jwtMock.getExpiresAt()).thenReturn(newDate); + when(jwtDecoder.decode("newId")).thenReturn(jwtMock); + + manager.getCredentials("some scope", 0, callback); + verify(request).start(requestCallbackCaptor.capture()); + verify(request).addParameter(eq("scope"), eq("some scope")); + + //Trigger success + String newRefresh = null; + Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope"); + requestCallbackCaptor.getValue().onSuccess(renewedCredentials); + verify(callback).onSuccess(credentialsCaptor.capture()); + + // Verify the credentials are property stored + verify(storage).store("com.auth0.id_token", renewedCredentials.getIdToken()); + verify(storage).store("com.auth0.access_token", renewedCredentials.getAccessToken()); + //RefreshToken should not be replaced + verify(storage, never()).store("com.auth0.refresh_token", newRefresh); + verify(storage).store("com.auth0.refresh_token", "refreshToken"); + verify(storage).store("com.auth0.token_type", renewedCredentials.getType()); + verify(storage).store("com.auth0.expires_at", renewedCredentials.getExpiresAt().getTime()); + verify(storage).store("com.auth0.scope", renewedCredentials.getScope()); + verify(storage).store("com.auth0.cache_expires_at", renewedCredentials.getExpiresAt().getTime()); + verify(storage, never()).remove(anyString()); + + //// Verify the returned credentials are the latest + Credentials retrievedCredentials = credentialsCaptor.getValue(); + assertThat(retrievedCredentials, is(notNullValue())); + assertThat(retrievedCredentials.getIdToken(), is("newId")); + assertThat(retrievedCredentials.getAccessToken(), is("newAccess")); + assertThat(retrievedCredentials.getType(), is("newType")); + assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); + assertThat(retrievedCredentials.getExpiresAt(), is(newDate)); + assertThat(retrievedCredentials.getScope(), is("newScope")); + } + + @Test + public void shouldRenewCredentialsWithMinTtl() { + when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); + when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); + when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); + when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); + long expirationTime = CredentialsMock.CURRENT_TIME_MS; //Same as current time --> expired + when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); + when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); + when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); + when(client.renewAuth("refreshToken")).thenReturn(request); + + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 61 * 1000); //new token expires in minTTL + 1 seconds + JWT jwtMock = mock(JWT.class); + when(jwtMock.getExpiresAt()).thenReturn(newDate); + when(jwtDecoder.decode("newId")).thenReturn(jwtMock); + + manager.getCredentials(null, 60, callback); //60 seconds of minTTL + verify(request, never()).addParameter(eq("scope"), anyString()); + verify(request).start(requestCallbackCaptor.capture()); + + //Trigger success + String newRefresh = null; + Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope"); + requestCallbackCaptor.getValue().onSuccess(renewedCredentials); + verify(callback).onSuccess(credentialsCaptor.capture()); + + // Verify the credentials are property stored + verify(storage).store("com.auth0.id_token", renewedCredentials.getIdToken()); + verify(storage).store("com.auth0.access_token", renewedCredentials.getAccessToken()); + //RefreshToken should not be replaced + verify(storage, never()).store("com.auth0.refresh_token", newRefresh); + verify(storage).store("com.auth0.refresh_token", "refreshToken"); + verify(storage).store("com.auth0.token_type", renewedCredentials.getType()); + verify(storage).store("com.auth0.expires_at", renewedCredentials.getExpiresAt().getTime()); + verify(storage).store("com.auth0.scope", renewedCredentials.getScope()); + verify(storage).store("com.auth0.cache_expires_at", renewedCredentials.getExpiresAt().getTime()); + verify(storage, never()).remove(anyString()); + + //// Verify the returned credentials are the latest + Credentials retrievedCredentials = credentialsCaptor.getValue(); + assertThat(retrievedCredentials, is(notNullValue())); + assertThat(retrievedCredentials.getIdToken(), is("newId")); + assertThat(retrievedCredentials.getAccessToken(), is("newAccess")); + assertThat(retrievedCredentials.getType(), is("newType")); + assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); + assertThat(retrievedCredentials.getExpiresAt(), is(newDate)); + assertThat(retrievedCredentials.getScope(), is("newScope")); + } + @Test public void shouldGetAndSuccessfullyRenewExpiredCredentials() { when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); @@ -356,12 +457,13 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentials() { when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); when(client.renewAuth("refreshToken")).thenReturn(request); - Date newDate = new Date(123412341234L); + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); manager.getCredentials(callback); + verify(request, never()).addParameter(eq("scope"), anyString()); verify(request).start(requestCallbackCaptor.capture()); //Trigger success @@ -393,6 +495,46 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentials() { assertThat(retrievedCredentials.getScope(), is("newScope")); } + @Test + public void shouldGetAndFailToRenewExpiredCredentialsWhenReceivedTokenHasLowerTtl() { + when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); + when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); + when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); + when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); + long expirationTime = CredentialsMock.CURRENT_TIME_MS; //Same as current time --> expired + when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); + when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); + when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); + when(client.renewAuth("refreshToken")).thenReturn(request); + + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 59 * 1000); //new token expires in minTTL - 1 seconds + JWT jwtMock = mock(JWT.class); + when(jwtMock.getExpiresAt()).thenReturn(newDate); + when(jwtDecoder.decode("newId")).thenReturn(jwtMock); + + manager.getCredentials(null, 60, callback); //60 seconds of minTTL + verify(request, never()).addParameter(eq("scope"), anyString()); + verify(request).start(requestCallbackCaptor.capture()); + + //Trigger success + String newRefresh = null; + Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope"); + requestCallbackCaptor.getValue().onSuccess(renewedCredentials); + verify(callback).onFailure(exceptionCaptor.capture()); + + // Verify the credentials are never stored + verify(storage, never()).store(anyString(), anyInt()); + verify(storage, never()).store(anyString(), anyLong()); + verify(storage, never()).store(anyString(), anyString()); + verify(storage, never()).store(anyString(), anyBoolean()); + verify(storage, never()).remove(anyString()); + + CredentialsManagerException exception = exceptionCaptor.getValue(); + assertThat(exception, is(notNullValue())); + assertThat(exception.getCause(), is(nullValue())); + assertThat(exception.getMessage(), is("The lifetime of the renewed Access Token or Id Token (1) is less than the minTTL requested (60). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.")); + } + @Test public void shouldGetAndSuccessfullyRenewExpiredCredentialsWithRefreshTokenRotation() { when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); @@ -405,7 +547,7 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentialsWithRefreshTokenRotat when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); when(client.renewAuth("refreshToken")).thenReturn(request); - Date newDate = new Date(123412341234L); + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); From 7cb620de1441fc646b3a7047627787458a0cfd28 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 21 Oct 2020 17:45:42 -0300 Subject: [PATCH 05/18] Apply suggestions from code review Co-authored-by: Rita Zerrizuela --- .../storage/CredentialsManagerTest.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java index 77569e6dd..9e928a5ad 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java @@ -351,7 +351,7 @@ public void shouldRenewCredentialsWhenScopeHasChanged() { when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; //non expired credentials + long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; // non expired credentials when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); when(storage.retrieveString("com.auth0.scope")).thenReturn("some new scope"); @@ -366,7 +366,7 @@ public void shouldRenewCredentialsWhenScopeHasChanged() { verify(request).start(requestCallbackCaptor.capture()); verify(request).addParameter(eq("scope"), eq("some scope")); - //Trigger success + // Trigger success String newRefresh = null; Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope"); requestCallbackCaptor.getValue().onSuccess(renewedCredentials); @@ -375,7 +375,7 @@ public void shouldRenewCredentialsWhenScopeHasChanged() { // Verify the credentials are property stored verify(storage).store("com.auth0.id_token", renewedCredentials.getIdToken()); verify(storage).store("com.auth0.access_token", renewedCredentials.getAccessToken()); - //RefreshToken should not be replaced + // RefreshToken should not be replaced verify(storage, never()).store("com.auth0.refresh_token", newRefresh); verify(storage).store("com.auth0.refresh_token", "refreshToken"); verify(storage).store("com.auth0.token_type", renewedCredentials.getType()); @@ -384,7 +384,7 @@ public void shouldRenewCredentialsWhenScopeHasChanged() { verify(storage).store("com.auth0.cache_expires_at", renewedCredentials.getExpiresAt().getTime()); verify(storage, never()).remove(anyString()); - //// Verify the returned credentials are the latest + // Verify the returned credentials are the latest Credentials retrievedCredentials = credentialsCaptor.getValue(); assertThat(retrievedCredentials, is(notNullValue())); assertThat(retrievedCredentials.getIdToken(), is("newId")); @@ -401,22 +401,22 @@ public void shouldRenewCredentialsWithMinTtl() { when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); - long expirationTime = CredentialsMock.CURRENT_TIME_MS; //Same as current time --> expired + long expirationTime = CredentialsMock.CURRENT_TIME_MS; // Same as current time --> expired when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); when(client.renewAuth("refreshToken")).thenReturn(request); - Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 61 * 1000); //new token expires in minTTL + 1 seconds + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 61 * 1000); // New token expires in minTTL + 1 second JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); - manager.getCredentials(null, 60, callback); //60 seconds of minTTL + manager.getCredentials(null, 60, callback); // 60 seconds of minTTL verify(request, never()).addParameter(eq("scope"), anyString()); verify(request).start(requestCallbackCaptor.capture()); - //Trigger success + // Trigger success String newRefresh = null; Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope"); requestCallbackCaptor.getValue().onSuccess(renewedCredentials); @@ -425,7 +425,7 @@ public void shouldRenewCredentialsWithMinTtl() { // Verify the credentials are property stored verify(storage).store("com.auth0.id_token", renewedCredentials.getIdToken()); verify(storage).store("com.auth0.access_token", renewedCredentials.getAccessToken()); - //RefreshToken should not be replaced + // RefreshToken should not be replaced verify(storage, never()).store("com.auth0.refresh_token", newRefresh); verify(storage).store("com.auth0.refresh_token", "refreshToken"); verify(storage).store("com.auth0.token_type", renewedCredentials.getType()); @@ -434,7 +434,7 @@ public void shouldRenewCredentialsWithMinTtl() { verify(storage).store("com.auth0.cache_expires_at", renewedCredentials.getExpiresAt().getTime()); verify(storage, never()).remove(anyString()); - //// Verify the returned credentials are the latest + // Verify the returned credentials are the latest Credentials retrievedCredentials = credentialsCaptor.getValue(); assertThat(retrievedCredentials, is(notNullValue())); assertThat(retrievedCredentials.getIdToken(), is("newId")); @@ -501,22 +501,22 @@ public void shouldGetAndFailToRenewExpiredCredentialsWhenReceivedTokenHasLowerTt when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); - long expirationTime = CredentialsMock.CURRENT_TIME_MS; //Same as current time --> expired + long expirationTime = CredentialsMock.CURRENT_TIME_MS; // Same as current time --> expired when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); when(client.renewAuth("refreshToken")).thenReturn(request); - Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 59 * 1000); //new token expires in minTTL - 1 seconds + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 59 * 1000); // New token expires in minTTL - 1 second JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); - manager.getCredentials(null, 60, callback); //60 seconds of minTTL + manager.getCredentials(null, 60, callback); // 60 seconds of minTTL verify(request, never()).addParameter(eq("scope"), anyString()); verify(request).start(requestCallbackCaptor.capture()); - //Trigger success + // Trigger failure String newRefresh = null; Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope"); requestCallbackCaptor.getValue().onSuccess(renewedCredentials); @@ -722,4 +722,4 @@ private void prepareJwtDecoderMock(@Nullable Date expiresAt) { when(jwtMock.getExpiresAt()).thenReturn(expiresAt); when(jwtDecoder.decode("idToken")).thenReturn(jwtMock); } -} \ No newline at end of file +} From ac08e6027e2a8d82500d9ab546740680d267621d Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 21 Oct 2020 17:56:53 -0300 Subject: [PATCH 06/18] chore PR review comments --- .../storage/CredentialsManager.java | 4 +- .../storage/CredentialsManagerTest.java | 39 ++++++++++--------- .../auth0/android/result/CredentialsMock.java | 1 + 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java index 8b12fdda5..1089ec898 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java @@ -132,7 +132,7 @@ public void getCredentials(@Nullable String scope, final int minTtl, @NonNull fi return; } if (refreshToken == null) { - callback.onFailure(new CredentialsManagerException("Credentials have expired and no Refresh Token was available to renew them.")); + callback.onFailure(new CredentialsManagerException("Credentials need to be renewed but no Refresh Token is available to renew them.")); return; } @@ -175,7 +175,7 @@ private boolean hasScopeChanged(@NonNull String storedScope, @Nullable String re Arrays.sort(stored); String[] required = requiredScope.split(" "); Arrays.sort(required); - return stored != required; + return !Arrays.equals(stored, required); } private boolean willExpire(long expiresAt, long minTtl) { diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java index 9e928a5ad..5a917a6c7 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java @@ -54,6 +54,8 @@ @Config(sdk = 21) public class CredentialsManagerTest { + private static final long ONE_HOUR_SECONDS = 60 * 60; + @Mock private AuthenticationAPIClient client; @Mock @@ -98,7 +100,7 @@ public Object answer(InvocationOnMock invocation) { @Test public void shouldSaveRefreshableCredentialsInStorage() { - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", "refreshToken", new Date(expirationTime), "scope"); prepareJwtDecoderMock(new Date(expirationTime)); manager.saveCredentials(credentials); @@ -115,7 +117,7 @@ public void shouldSaveRefreshableCredentialsInStorage() { @Test public void shouldSaveRefreshableCredentialsUsingAccessTokenExpForCacheExpirationInStorage() { - long accessTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long accessTokenExpirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; Credentials credentials = new CredentialsMock(null, "accessToken", "type", "refreshToken", new Date(accessTokenExpirationTime), "scope"); prepareJwtDecoderMock(new Date(accessTokenExpirationTime)); manager.saveCredentials(credentials); @@ -132,7 +134,7 @@ public void shouldSaveRefreshableCredentialsUsingAccessTokenExpForCacheExpiratio @Test public void shouldSaveRefreshableCredentialsUsingIdTokenExpForCacheExpirationInStorage() { - long accessTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long accessTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 5000 * 1000; long idTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 2000 * 1000; Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", "refreshToken", new Date(accessTokenExpirationTime), "scope"); prepareJwtDecoderMock(new Date(idTokenExpirationTime)); @@ -150,7 +152,7 @@ public void shouldSaveRefreshableCredentialsUsingIdTokenExpForCacheExpirationInS @Test public void shouldSaveNonRefreshableCredentialsInStorage() { - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", null, new Date(expirationTime), "scope"); prepareJwtDecoderMock(new Date(expirationTime)); manager.saveCredentials(credentials); @@ -180,7 +182,6 @@ public void shouldThrowOnSetIfCredentialsDoesNotHaveExpiresAt() { exception.expectMessage("Credentials must have a valid date of expiration and a valid access_token or id_token value."); Date date = null; - //noinspection ConstantConditions Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", "refreshToken", date, "scope"); manager.saveCredentials(credentials); } @@ -206,7 +207,7 @@ public void shouldFailOnGetCredentialsWhenNoAccessTokenOrIdTokenWasSaved() { when(storage.retrieveString("com.auth0.access_token")).thenReturn(null); when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); @@ -236,7 +237,7 @@ public void shouldFailOnGetCredentialsWhenExpiredAndNoRefreshTokenWasSaved() { verify(callback).onFailure(exceptionCaptor.capture()); CredentialsManagerException exception = exceptionCaptor.getValue(); assertThat(exception, is(notNullValue())); - assertThat(exception.getMessage(), is("Credentials have expired and no Refresh Token was available to renew them.")); + assertThat(exception.getMessage(), is("Credentials need to be renewed but no Refresh Token is available to renew them.")); } @Test @@ -246,7 +247,7 @@ public void shouldNotFailOnGetCredentialsWhenCacheExpiresAtNotSetButExpiresAtIsP when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(null); when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); @@ -266,7 +267,7 @@ public void shouldGetNonExpiredCredentialsFromStorage() { when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); @@ -281,7 +282,7 @@ public void shouldGetNonExpiredCredentialsFromStorage() { assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); assertThat(retrievedCredentials.getType(), is("type")); assertThat(retrievedCredentials.getExpiresIn(), is(notNullValue())); - assertThat(retrievedCredentials.getExpiresIn().doubleValue(), CoreMatchers.is(closeTo(123456L, 1))); + assertThat(retrievedCredentials.getExpiresIn().doubleValue(), CoreMatchers.is(closeTo(ONE_HOUR_SECONDS, 1))); assertThat(retrievedCredentials.getExpiresAt(), is(notNullValue())); assertThat(retrievedCredentials.getExpiresAt().getTime(), is(expirationTime)); assertThat(retrievedCredentials.getScope(), is("scope")); @@ -295,7 +296,7 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyIdTokenIsAvailable( when(storage.retrieveString("com.auth0.access_token")).thenReturn(null); when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); @@ -310,7 +311,7 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyIdTokenIsAvailable( assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); assertThat(retrievedCredentials.getType(), is("type")); assertThat(retrievedCredentials.getExpiresIn(), is(notNullValue())); - assertThat(retrievedCredentials.getExpiresIn().doubleValue(), CoreMatchers.is(closeTo(123456L, 1))); + assertThat(retrievedCredentials.getExpiresIn().doubleValue(), CoreMatchers.is(closeTo(ONE_HOUR_SECONDS, 1))); assertThat(retrievedCredentials.getExpiresAt(), is(notNullValue())); assertThat(retrievedCredentials.getExpiresAt().getTime(), is(expirationTime)); assertThat(retrievedCredentials.getScope(), is("scope")); @@ -324,7 +325,7 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyAccessTokenIsAvaila when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); - Long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; + Long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); @@ -339,7 +340,7 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyAccessTokenIsAvaila assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); assertThat(retrievedCredentials.getType(), is("type")); assertThat(retrievedCredentials.getExpiresIn(), is(notNullValue())); - assertThat(retrievedCredentials.getExpiresIn().doubleValue(), CoreMatchers.is(closeTo(123456L, 1))); + assertThat(retrievedCredentials.getExpiresIn().doubleValue(), CoreMatchers.is(closeTo(ONE_HOUR_SECONDS, 1))); assertThat(retrievedCredentials.getExpiresAt(), is(notNullValue())); assertThat(retrievedCredentials.getExpiresAt().getTime(), is(expirationTime)); assertThat(retrievedCredentials.getScope(), is("scope")); @@ -351,13 +352,13 @@ public void shouldRenewCredentialsWhenScopeHasChanged() { when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; // non expired credentials + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; // non expired credentials when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); when(storage.retrieveString("com.auth0.scope")).thenReturn("some new scope"); when(client.renewAuth("refreshToken")).thenReturn(request); - Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); + Date newDate = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000); JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); @@ -457,7 +458,7 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentials() { when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); when(client.renewAuth("refreshToken")).thenReturn(request); - Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); + Date newDate = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); @@ -547,7 +548,7 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentialsWithRefreshTokenRotat when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); when(client.renewAuth("refreshToken")).thenReturn(request); - Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); + Date newDate = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); @@ -629,7 +630,7 @@ public void shouldClearCredentials() { @Test public void shouldHaveCredentialsWhenTokenHasNotExpired() { - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); diff --git a/auth0/src/test/java/com/auth0/android/result/CredentialsMock.java b/auth0/src/test/java/com/auth0/android/result/CredentialsMock.java index 416714487..e9210f7d4 100644 --- a/auth0/src/test/java/com/auth0/android/result/CredentialsMock.java +++ b/auth0/src/test/java/com/auth0/android/result/CredentialsMock.java @@ -10,6 +10,7 @@ public class CredentialsMock extends Credentials { public static final long CURRENT_TIME_MS = calculateCurrentTime(); + public static final long ONE_HOUR_AHEAD_MS = CURRENT_TIME_MS + 60 * 60 * 1000; public CredentialsMock(@Nullable String idToken, @Nullable String accessToken, @Nullable String type, @Nullable String refreshToken, @Nullable Long expiresIn) { super(idToken, accessToken, type, refreshToken, expiresIn); From 0f5506f53ded01401cedb96c8305417f11b5183d Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 21 Oct 2020 18:05:56 -0300 Subject: [PATCH 07/18] add additional test case --- .../storage/CredentialsManagerTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java index 5a917a6c7..348722b17 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java @@ -346,6 +346,56 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyAccessTokenIsAvaila assertThat(retrievedCredentials.getScope(), is("scope")); } + @Test + public void shouldRenewExpiredCredentialsWhenScopeHasChanged() { + when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); + when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); + when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken"); + when(storage.retrieveString("com.auth0.token_type")).thenReturn("type"); + long expirationTime = CredentialsMock.CURRENT_TIME_MS; // expired credentials + when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); + when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); + when(storage.retrieveString("com.auth0.scope")).thenReturn("scope"); + when(client.renewAuth("refreshToken")).thenReturn(request); + + Date newDate = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000); + JWT jwtMock = mock(JWT.class); + when(jwtMock.getExpiresAt()).thenReturn(newDate); + when(jwtDecoder.decode("newId")).thenReturn(jwtMock); + + manager.getCredentials("some scope", 0, callback); + verify(request).start(requestCallbackCaptor.capture()); + verify(request).addParameter(eq("scope"), eq("some scope")); + + // Trigger success + String newRefresh = null; + Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "some scope"); + requestCallbackCaptor.getValue().onSuccess(renewedCredentials); + verify(callback).onSuccess(credentialsCaptor.capture()); + + // Verify the credentials are property stored + verify(storage).store("com.auth0.id_token", renewedCredentials.getIdToken()); + verify(storage).store("com.auth0.access_token", renewedCredentials.getAccessToken()); + // RefreshToken should not be replaced + verify(storage, never()).store("com.auth0.refresh_token", newRefresh); + verify(storage).store("com.auth0.refresh_token", "refreshToken"); + verify(storage).store("com.auth0.token_type", renewedCredentials.getType()); + verify(storage).store("com.auth0.expires_at", renewedCredentials.getExpiresAt().getTime()); + verify(storage).store("com.auth0.scope", renewedCredentials.getScope()); + verify(storage).store("com.auth0.cache_expires_at", renewedCredentials.getExpiresAt().getTime()); + verify(storage, never()).remove(anyString()); + + // Verify the returned credentials are the latest + Credentials retrievedCredentials = credentialsCaptor.getValue(); + assertThat(retrievedCredentials, is(notNullValue())); + assertThat(retrievedCredentials.getIdToken(), is("newId")); + assertThat(retrievedCredentials.getAccessToken(), is("newAccess")); + assertThat(retrievedCredentials.getType(), is("newType")); + assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); + assertThat(retrievedCredentials.getExpiresAt(), is(newDate)); + assertThat(retrievedCredentials.getScope(), is("some scope")); + } + @Test public void shouldRenewCredentialsWhenScopeHasChanged() { when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); @@ -554,6 +604,7 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentialsWithRefreshTokenRotat when(jwtDecoder.decode("newId")).thenReturn(jwtMock); manager.getCredentials(callback); + verify(request, never()).addParameter(eq("scope"), anyString()); verify(request).start(requestCallbackCaptor.capture()); //Trigger success From b1901bb9b05a28a20584515e1d1ae3e16016c425 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 29 Oct 2020 19:11:15 -0300 Subject: [PATCH 08/18] update minTTL condition to apply only for access tokens --- .../storage/CredentialsManager.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java index 1089ec898..6c512cfdc 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java @@ -75,7 +75,7 @@ public void saveCredentials(@NonNull Credentials credentials) { throw new CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value."); } - long expiresAt = calculateExpiresAt(credentials); + long cacheExpiresAt = calculateCacheExpiresAt(credentials); storage.store(KEY_ACCESS_TOKEN, credentials.getAccessToken()); storage.store(KEY_REFRESH_TOKEN, credentials.getRefreshToken()); @@ -83,7 +83,7 @@ public void saveCredentials(@NonNull Credentials credentials) { storage.store(KEY_TOKEN_TYPE, credentials.getType()); storage.store(KEY_EXPIRES_AT, credentials.getExpiresAt().getTime()); storage.store(KEY_SCOPE, credentials.getScope()); - storage.store(KEY_CACHE_EXPIRES_AT, expiresAt); + storage.store(KEY_CACHE_EXPIRES_AT, cacheExpiresAt); } /** @@ -103,7 +103,7 @@ public void getCredentials(@NonNull BaseCallback callback) { @@ -124,10 +124,11 @@ public void getCredentials(@Nullable String scope, final int minTtl, @NonNull fi return; } - boolean willExpire = willExpire(cacheExpiresAt, minTtl); + boolean hasEitherExpired = hasExpired(cacheExpiresAt); + boolean willAccessTokenExpire = willExpire(expiresAt, minTtl); boolean scopeChanged = hasScopeChanged(storedScope, scope); - if (!willExpire && !scopeChanged) { + if (!hasEitherExpired && !willAccessTokenExpire && !scopeChanged) { callback.onSuccess(recreateCredentials(idToken, accessToken, tokenType, refreshToken, new Date(expiresAt), storedScope)); return; } @@ -143,10 +144,11 @@ public void getCredentials(@Nullable String scope, final int minTtl, @NonNull fi request.start(new AuthenticationCallback() { @Override public void onSuccess(@Nullable Credentials fresh) { - long nextCacheExpiresAt = calculateExpiresAt(fresh); - boolean willExpire = willExpire(nextCacheExpiresAt, minTtl); - if (willExpire) { - long tokenLifetime = (nextCacheExpiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; + //noinspection ConstantConditions + long expiresAt = fresh.getExpiresAt().getTime(); + boolean willAccessTokenExpire = willExpire(expiresAt, minTtl); + if (willAccessTokenExpire) { + long tokenLifetime = (expiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token or Id Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); callback.onFailure(wrongTtlException); return; @@ -183,7 +185,11 @@ private boolean willExpire(long expiresAt, long minTtl) { return expiresAt <= nextClock; } - private long calculateExpiresAt(@NonNull Credentials credentials) { + private boolean hasExpired(long expiresAt) { + return expiresAt <= getCurrentTimeInMillis(); + } + + private long calculateCacheExpiresAt(@NonNull Credentials credentials) { long expiresAt = credentials.getExpiresAt().getTime(); if (credentials.getIdToken() != null) { From 9ab34d8b6d0c6f933a7f99dba978c3f0574f01b3 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 30 Oct 2020 09:43:32 -0300 Subject: [PATCH 09/18] add fallback for when cache expires at is not set --- .../authentication/storage/CredentialsManager.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java index 6c512cfdc..6315a3f20 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java @@ -213,10 +213,14 @@ public boolean hasValidCredentials() { String refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN); String idToken = storage.retrieveString(KEY_ID_TOKEN); Long expiresAt = storage.retrieveLong(KEY_EXPIRES_AT); + Long cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT); + if (cacheExpiresAt == null) { + cacheExpiresAt = expiresAt; + } return !(isEmpty(accessToken) && isEmpty(idToken) || - expiresAt == null || - expiresAt <= getCurrentTimeInMillis() && refreshToken == null); + cacheExpiresAt == null || + hasExpired(cacheExpiresAt) && refreshToken == null); } /** From 1e6e3e3239955540789a39dcbfd61e53da43054c Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 30 Oct 2020 17:05:07 -0300 Subject: [PATCH 10/18] accept minTtl when checking for credentials existence --- .../storage/CredentialsManager.java | 18 +++++++++++++----- .../storage/CredentialsManagerTest.java | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java index 6315a3f20..14f8a1022 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java @@ -144,12 +144,11 @@ public void getCredentials(@Nullable String scope, final int minTtl, @NonNull fi request.start(new AuthenticationCallback() { @Override public void onSuccess(@Nullable Credentials fresh) { - //noinspection ConstantConditions long expiresAt = fresh.getExpiresAt().getTime(); boolean willAccessTokenExpire = willExpire(expiresAt, minTtl); if (willAccessTokenExpire) { long tokenLifetime = (expiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; - CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token or Id Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); + CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); callback.onFailure(wrongTtlException); return; } @@ -209,6 +208,16 @@ private long calculateCacheExpiresAt(@NonNull Credentials credentials) { * @return whether there are valid credentials stored on this manager. */ public boolean hasValidCredentials() { + return hasValidCredentials(0); + } + + /** + * Checks if a non-expired pair of credentials can be obtained from this manager. + * + * @param minTtl the minimum time in seconds that the access token should last before expiration. + * @return whether there are valid credentials stored on this manager. + */ + public boolean hasValidCredentials(long minTtl) { String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN); String refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN); String idToken = storage.retrieveString(KEY_ID_TOKEN); @@ -218,9 +227,8 @@ public boolean hasValidCredentials() { cacheExpiresAt = expiresAt; } - return !(isEmpty(accessToken) && isEmpty(idToken) || - cacheExpiresAt == null || - hasExpired(cacheExpiresAt) && refreshToken == null); + boolean emptyCredentials = isEmpty(accessToken) && isEmpty(idToken) || cacheExpiresAt == null || expiresAt == null; + return !(emptyCredentials || (hasExpired(cacheExpiresAt) || willExpire(expiresAt, minTtl)) && refreshToken == null); } /** diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java index 348722b17..3d0d59854 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java @@ -35,6 +35,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyInt; @@ -688,10 +689,12 @@ public void shouldHaveCredentialsWhenTokenHasNotExpired() { when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); when(storage.retrieveString("com.auth0.access_token")).thenReturn(null); assertThat(manager.hasValidCredentials(), is(true)); + assertThat(manager.hasValidCredentials(ONE_HOUR_SECONDS - 1), is(true)); when(storage.retrieveString("com.auth0.id_token")).thenReturn(null); when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); assertThat(manager.hasValidCredentials(), is(true)); + assertThat(manager.hasValidCredentials(ONE_HOUR_SECONDS - 1), is(true)); } @Test @@ -710,6 +713,19 @@ public void shouldNotHaveCredentialsWhenTokenHasExpiredAndNoRefreshTokenIsAvaila assertFalse(manager.hasValidCredentials()); } + @Test + public void shouldNotHaveCredentialsWhenAccessTokenWillExpireAndNoRefreshTokenIsAvailable() { + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; + when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime); + when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime); + when(storage.retrieveString("com.auth0.refresh_token")).thenReturn(null); + + when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken"); + when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken"); + + assertFalse(manager.hasValidCredentials(ONE_HOUR_SECONDS)); + } + @Test public void shouldHaveCredentialsWhenTokenHasExpiredButRefreshTokenIsAvailable() { long expirationTime = CredentialsMock.CURRENT_TIME_MS; //Same as current time --> expired From 23f1da67af6363e934f5845a73a245ba819df09d Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 30 Oct 2020 18:07:48 -0300 Subject: [PATCH 11/18] fix test exception message assertion --- .../android/authentication/storage/CredentialsManagerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java index 3d0d59854..a40f94a81 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.java @@ -584,7 +584,7 @@ public void shouldGetAndFailToRenewExpiredCredentialsWhenReceivedTokenHasLowerTt CredentialsManagerException exception = exceptionCaptor.getValue(); assertThat(exception, is(notNullValue())); assertThat(exception.getCause(), is(nullValue())); - assertThat(exception.getMessage(), is("The lifetime of the renewed Access Token or Id Token (1) is less than the minTTL requested (60). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.")); + assertThat(exception.getMessage(), is("The lifetime of the renewed Access Token (1) is less than the minTTL requested (60). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.")); } @Test From 23595fb42911d8e9a1625bb5fbbb35d8cf898891 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 21 Oct 2020 17:05:11 -0300 Subject: [PATCH 12/18] allow to pass minTTL and scope to the SecureCredentialsManager --- .../storage/SecureCredentialsManager.java | 99 +++++++++--- .../storage/SecureCredentialsManagerTest.java | 146 +++++++++++++++++- 2 files changed, 219 insertions(+), 26 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java index f8d0a95e0..2bde5c2c8 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java @@ -19,11 +19,13 @@ import com.auth0.android.callback.AuthenticationCallback; import com.auth0.android.callback.BaseCallback; import com.auth0.android.jwt.JWT; +import com.auth0.android.request.ParameterizableRequest; import com.auth0.android.request.internal.GsonProvider; import com.auth0.android.result.Credentials; import com.auth0.android.util.Clock; import com.google.gson.Gson; +import java.util.Arrays; import java.util.Date; import static android.text.TextUtils.isEmpty; @@ -61,6 +63,8 @@ public class SecureCredentialsManager { //State for retrying operations private BaseCallback decryptCallback; private Intent authIntent; + private String scope; + private int minTtl; @VisibleForTesting @@ -140,7 +144,7 @@ public boolean checkAuthenticationResult(int requestCode, int resultCode) { return false; } if (resultCode == Activity.RESULT_OK) { - continueGetCredentials(decryptCallback); + continueGetCredentials(scope, minTtl, decryptCallback); } else { decryptCallback.onFailure(new CredentialsManagerException("The user didn't pass the authentication challenge.")); decryptCallback = null; @@ -160,17 +164,7 @@ public void saveCredentials(@NonNull Credentials credentials) throws Credentials throw new CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value."); } - long expiresAt = credentials.getExpiresAt().getTime(); - - if (credentials.getIdToken() != null) { - JWT idToken = jwtDecoder.decode(credentials.getIdToken()); - Date idTokenExpiresAtDate = idToken.getExpiresAt(); - - if (idTokenExpiresAtDate != null) { - expiresAt = Math.min(idTokenExpiresAtDate.getTime(), expiresAt); - } - } - + long expiresAt = calculateExpiresAt(credentials); String json = gson.toJson(credentials); boolean canRefresh = !isEmpty(credentials.getRefreshToken()); @@ -207,6 +201,23 @@ public void saveCredentials(@NonNull Credentials credentials) throws Credentials * @param callback the callback to receive the result in. */ public void getCredentials(@NonNull BaseCallback callback) { + getCredentials(null, 0, callback); + } + + /** + * Tries to obtain the credentials from the Storage. The callback's {@link BaseCallback#onSuccess(Object)} method will be called with the result. + * If something unexpected happens, the {@link BaseCallback#onFailure(Auth0Exception)} method will be called with the error. Some devices are not compatible + * at all with the cryptographic implementation and will have {@link CredentialsManagerException#isDeviceIncompatible()} return true. + *

+ * If a LockScreen is setup and {@link #requireAuthentication(Activity, int, String, String)} was called, the user will be asked to authenticate before accessing + * the credentials. Your activity must override the {@link Activity#onActivityResult(int, int, Intent)} method and call + * {@link #checkAuthenticationResult(int, int)} with the received values. + * + * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. + * @param minTtl the minimum time in seconds that both the access token and ID token should last before expiration. + * @param callback the callback to receive the result in. + */ + public void getCredentials(@Nullable String scope, int minTtl, @NonNull BaseCallback callback) { if (!hasValidCredentials()) { callback.onFailure(new CredentialsManagerException("No Credentials were previously set.")); return; @@ -214,11 +225,13 @@ public void getCredentials(@NonNull BaseCallback callback) { + private void continueGetCredentials(@Nullable String scope, final int minTtl, final BaseCallback callback) { String encryptedEncoded = storage.retrieveString(KEY_CREDENTIALS); byte[] encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT); @@ -268,12 +281,17 @@ private void continueGetCredentials(final BaseCallback getCurrentTimeInMillis()) { + + boolean willExpire = willExpire(expiresAt, minTtl); + boolean scopeChanged = hasScopeChanged(credentials.getScope(), scope); + + if (!willExpire && !scopeChanged) { callback.onSuccess(credentials); decryptCallback = null; return; @@ -285,11 +303,24 @@ private void continueGetCredentials(final BaseCallback() { + ParameterizableRequest request = apiClient.renewAuth(credentials.getRefreshToken()); + if (scope != null) { + request.addParameter("scope", scope); + } + request.start(new AuthenticationCallback() { @Override public void onSuccess(@Nullable Credentials fresh) { + long nextExpiresAt = calculateExpiresAt(fresh); + boolean willExpire = willExpire(nextExpiresAt, minTtl); + if (willExpire) { + long tokenLifetime = (nextExpiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; + CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token or Id Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); + callback.onFailure(wrongTtlException); + decryptCallback = null; + return; + } + //non-empty refresh token for refresh token rotation scenarios - //noinspection ConstantConditions String updatedRefreshToken = isEmpty(fresh.getRefreshToken()) ? credentials.getRefreshToken() : fresh.getRefreshToken(); Credentials refreshed = new Credentials(fresh.getIdToken(), fresh.getAccessToken(), fresh.getType(), updatedRefreshToken, fresh.getExpiresAt(), fresh.getScope()); saveCredentials(refreshed); @@ -310,4 +341,34 @@ long getCurrentTimeInMillis() { return clock.getCurrentTimeMillis(); } + private boolean hasScopeChanged(@NonNull String storedScope, @Nullable String requiredScope) { + if (requiredScope == null) { + return false; + } + String[] stored = storedScope.split(" "); + Arrays.sort(stored); + String[] required = requiredScope.split(" "); + Arrays.sort(required); + return stored != required; + } + + private boolean willExpire(long expiresAt, long minTtl) { + long nextClock = getCurrentTimeInMillis() + minTtl * 1000; + return expiresAt <= nextClock; + } + + private long calculateExpiresAt(Credentials credentials) { + long expiresAt = credentials.getExpiresAt().getTime(); + + if (credentials.getIdToken() != null) { + JWT idToken = jwtDecoder.decode(credentials.getIdToken()); + Date idTokenExpiresAtDate = idToken.getExpiresAt(); + + if (idTokenExpiresAtDate != null) { + expiresAt = Math.min(idTokenExpiresAtDate.getTime(), expiresAt); + } + } + return expiresAt; + } + } diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java index 11231678c..0f6341df5 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java @@ -284,7 +284,6 @@ public void shouldThrowOnSaveIfCredentialsDoesNotHaveExpiresAt() { exception.expectMessage("Credentials must have a valid date of expiration and a valid access_token or id_token value."); Date date = null; - //noinspection ConstantConditions Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", "refreshToken", date, "scope"); prepareJwtDecoderMock(new Date()); manager.saveCredentials(credentials); @@ -443,21 +442,154 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyAccessTokenIsAvaila assertThat(retrievedCredentials.getScope(), is("scope")); } + @Test + public void shouldRenewCredentialsWithMinTtl() { + Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS); // expired credentials + insertTestCredentials(false, true, true, expiresAt); + + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 61 * 1000); // new token expires in minTTL + 1 seconds + JWT jwtMock = mock(JWT.class); + when(jwtMock.getExpiresAt()).thenReturn(newDate); + when(jwtDecoder.decode("newId")).thenReturn(jwtMock); + when(client.renewAuth("refreshToken")).thenReturn(request); + + manager.getCredentials(null, 60, callback); + verify(request, never()).addParameter(eq("scope"), anyString()); + verify(request).start(requestCallbackCaptor.capture()); + + // Trigger success + Credentials expectedCredentials = new Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope"); + String expectedJson = gson.toJson(expectedCredentials); + when(crypto.encrypt(expectedJson.getBytes())).thenReturn(expectedJson.getBytes()); + requestCallbackCaptor.getValue().onSuccess(expectedCredentials); + verify(callback).onSuccess(credentialsCaptor.capture()); + verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); + verify(storage).store("com.auth0.credentials_expires_at", newDate.getTime()); + verify(storage).store("com.auth0.credentials_can_refresh", true); + verify(storage, never()).remove(anyString()); + + // Verify the returned credentials are the latest + Credentials retrievedCredentials = credentialsCaptor.getValue(); + assertThat(retrievedCredentials, is(notNullValue())); + assertThat(retrievedCredentials.getIdToken(), is("newId")); + assertThat(retrievedCredentials.getAccessToken(), is("newAccess")); + assertThat(retrievedCredentials.getType(), is("newType")); + assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); + assertThat(retrievedCredentials.getExpiresAt(), is(newDate)); + assertThat(retrievedCredentials.getScope(), is("newScope")); + + // Verify the credentials are property stored + String encodedJson = stringCaptor.getValue(); + assertThat(encodedJson, is(notNullValue())); + final byte[] decoded = Base64.decode(encodedJson, Base64.DEFAULT); + Credentials renewedStoredCredentials = gson.fromJson(new String(decoded), Credentials.class); + assertThat(renewedStoredCredentials.getIdToken(), is("newId")); + assertThat(renewedStoredCredentials.getAccessToken(), is("newAccess")); + assertThat(renewedStoredCredentials.getRefreshToken(), is("refreshToken")); + assertThat(renewedStoredCredentials.getType(), is("newType")); + assertThat(renewedStoredCredentials.getExpiresAt(), is(notNullValue())); + assertThat(renewedStoredCredentials.getExpiresAt().getTime(), is(newDate.getTime())); + assertThat(renewedStoredCredentials.getScope(), is("newScope")); + } + + @Test + public void shouldGetAndFailToRenewExpiredCredentialsWhenReceivedTokenHasLowerTtl() { + Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS); // expired credentials + insertTestCredentials(false, true, true, expiresAt); + + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 59 * 1000); // new token expires in minTTL - 1 seconds + JWT jwtMock = mock(JWT.class); + when(jwtMock.getExpiresAt()).thenReturn(newDate); + when(jwtDecoder.decode("newId")).thenReturn(jwtMock); + when(client.renewAuth("refreshToken")).thenReturn(request); + + manager.getCredentials(null, 60, callback); + verify(request, never()).addParameter(eq("scope"), anyString()); + verify(request).start(requestCallbackCaptor.capture()); + + // Trigger success + Credentials expectedCredentials = new Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope"); + String expectedJson = gson.toJson(expectedCredentials); + when(crypto.encrypt(expectedJson.getBytes())).thenReturn(expectedJson.getBytes()); + requestCallbackCaptor.getValue().onSuccess(expectedCredentials); + + verify(callback).onFailure(exceptionCaptor.capture()); + CredentialsManagerException exception = exceptionCaptor.getValue(); + assertThat(exception, is(notNullValue())); + assertThat(exception.getMessage(), is("The lifetime of the renewed Access Token or Id Token (1) is less than the minTTL requested (60). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.")); + + verify(storage, never()).store(eq("com.auth0.credentials"), anyString()); + verify(storage, never()).store(eq("com.auth0.credentials_expires_at"), anyLong()); + verify(storage, never()).store(eq("com.auth0.credentials_can_refresh"), anyBoolean()); + verify(storage, never()).remove(anyString()); + } + + @Test + public void shouldRenewCredentialsWhenScopeHasChanged() { + Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); // non expired credentials + insertTestCredentials(false, true, true, expiresAt); // "scope" is set + + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 2222 * 1000); + JWT jwtMock = mock(JWT.class); + when(jwtMock.getExpiresAt()).thenReturn(newDate); + when(jwtDecoder.decode("newId")).thenReturn(jwtMock); + when(client.renewAuth("refreshToken")).thenReturn(request); + + manager.getCredentials("different scope", 0, callback); + verify(request).addParameter(eq("scope"), eq("different scope")); + verify(request).start(requestCallbackCaptor.capture()); + + // Trigger success + Credentials expectedCredentials = new Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "different scope"); + String expectedJson = gson.toJson(expectedCredentials); + when(crypto.encrypt(expectedJson.getBytes())).thenReturn(expectedJson.getBytes()); + requestCallbackCaptor.getValue().onSuccess(expectedCredentials); + verify(callback).onSuccess(credentialsCaptor.capture()); + verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); + verify(storage).store("com.auth0.credentials_expires_at", newDate.getTime()); + verify(storage).store("com.auth0.credentials_can_refresh", true); + verify(storage, never()).remove(anyString()); + + // Verify the returned credentials are the latest + Credentials retrievedCredentials = credentialsCaptor.getValue(); + assertThat(retrievedCredentials, is(notNullValue())); + assertThat(retrievedCredentials.getIdToken(), is("newId")); + assertThat(retrievedCredentials.getAccessToken(), is("newAccess")); + assertThat(retrievedCredentials.getType(), is("newType")); + assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); + assertThat(retrievedCredentials.getExpiresAt(), is(newDate)); + assertThat(retrievedCredentials.getScope(), is("different scope")); + + // Verify the credentials are property stored + String encodedJson = stringCaptor.getValue(); + assertThat(encodedJson, is(notNullValue())); + final byte[] decoded = Base64.decode(encodedJson, Base64.DEFAULT); + Credentials renewedStoredCredentials = gson.fromJson(new String(decoded), Credentials.class); + assertThat(renewedStoredCredentials.getIdToken(), is("newId")); + assertThat(renewedStoredCredentials.getAccessToken(), is("newAccess")); + assertThat(renewedStoredCredentials.getRefreshToken(), is("refreshToken")); + assertThat(renewedStoredCredentials.getType(), is("newType")); + assertThat(renewedStoredCredentials.getExpiresAt(), is(notNullValue())); + assertThat(renewedStoredCredentials.getExpiresAt().getTime(), is(newDate.getTime())); + assertThat(renewedStoredCredentials.getScope(), is("different scope")); + } + @Test public void shouldGetAndSuccessfullyRenewExpiredCredentials() { - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS); + Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS); // current time means expired credentials insertTestCredentials(false, true, true, expiresAt); - Date newDate = new Date(123412341234L); + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); JWT jwtMock = mock(JWT.class); - when(jwtMock.getExpiresAt()).thenReturn(expiresAt); + when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); when(client.renewAuth("refreshToken")).thenReturn(request); manager.getCredentials(callback); + verify(request, never()).addParameter(eq("scope"), anyString()); verify(request).start(requestCallbackCaptor.capture()); - //Trigger success + // Trigger success Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", null, newDate, "newScope"); Credentials expectedCredentials = new Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope"); String expectedJson = gson.toJson(expectedCredentials); @@ -498,9 +630,9 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentialsWithRefreshTokenRotat Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS); insertTestCredentials(false, true, true, expiresAt); - Date newDate = new Date(123412341234L); + Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); JWT jwtMock = mock(JWT.class); - when(jwtMock.getExpiresAt()).thenReturn(expiresAt); + when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); when(client.renewAuth("refreshToken")).thenReturn(request); From 3f8d90f3d423bc9961bfb9083737b73298263096 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 29 Oct 2020 19:24:26 -0300 Subject: [PATCH 13/18] update minTTL condition to apply only for access tokens --- .../storage/SecureCredentialsManager.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java index 2bde5c2c8..6fd0eb4f7 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java @@ -164,7 +164,7 @@ public void saveCredentials(@NonNull Credentials credentials) throws Credentials throw new CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value."); } - long expiresAt = calculateExpiresAt(credentials); + long cacheExpiresAt = calculateCacheExpiresAt(credentials); String json = gson.toJson(credentials); boolean canRefresh = !isEmpty(credentials.getRefreshToken()); @@ -173,7 +173,7 @@ public void saveCredentials(@NonNull Credentials credentials) throws Credentials byte[] encrypted = crypto.encrypt(json.getBytes()); String encryptedEncoded = Base64.encodeToString(encrypted, Base64.DEFAULT); storage.store(KEY_CREDENTIALS, encryptedEncoded); - storage.store(KEY_EXPIRES_AT, expiresAt); + storage.store(KEY_EXPIRES_AT, cacheExpiresAt); storage.store(KEY_CAN_REFRESH, canRefresh); storage.store(KEY_CRYPTO_ALIAS, KEY_ALIAS); } catch (IncompatibleDeviceException e) { @@ -214,7 +214,7 @@ public void getCredentials(@NonNull BaseCallback callback) { @@ -280,18 +280,21 @@ private void continueGetCredentials(@Nullable String scope, final int minTtl, fi return; } final Credentials credentials = gson.fromJson(json, Credentials.class); - Long expiresAt = storage.retrieveLong(KEY_EXPIRES_AT); - boolean hasEmptyCredentials = isEmpty(credentials.getAccessToken()) && isEmpty(credentials.getIdToken()) || expiresAt == null; + Long cacheExpiresAt = storage.retrieveLong(KEY_EXPIRES_AT); + boolean hasEmptyCredentials = isEmpty(credentials.getAccessToken()) && isEmpty(credentials.getIdToken()) || cacheExpiresAt == null; if (hasEmptyCredentials) { callback.onFailure(new CredentialsManagerException("No Credentials were previously set.")); decryptCallback = null; return; } - boolean willExpire = willExpire(expiresAt, minTtl); + //noinspection ConstantConditions + boolean hasEitherExpired = hasExpired(credentials.getExpiresAt().getTime()); + boolean willAccessTokenExpire = willExpire(cacheExpiresAt, minTtl); + //noinspection ConstantConditions boolean scopeChanged = hasScopeChanged(credentials.getScope(), scope); - if (!willExpire && !scopeChanged) { + if (!hasEitherExpired && !willAccessTokenExpire && !scopeChanged) { callback.onSuccess(credentials); decryptCallback = null; return; @@ -310,10 +313,10 @@ private void continueGetCredentials(@Nullable String scope, final int minTtl, fi request.start(new AuthenticationCallback() { @Override public void onSuccess(@Nullable Credentials fresh) { - long nextExpiresAt = calculateExpiresAt(fresh); - boolean willExpire = willExpire(nextExpiresAt, minTtl); - if (willExpire) { - long tokenLifetime = (nextExpiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; + long expiresAt = fresh.getExpiresAt().getTime(); + boolean willAccessTokenExpire = willExpire(expiresAt, minTtl); + if (willAccessTokenExpire) { + long tokenLifetime = (expiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token or Id Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); callback.onFailure(wrongTtlException); decryptCallback = null; @@ -357,7 +360,11 @@ private boolean willExpire(long expiresAt, long minTtl) { return expiresAt <= nextClock; } - private long calculateExpiresAt(Credentials credentials) { + private boolean hasExpired(long expiresAt) { + return expiresAt <= getCurrentTimeInMillis(); + } + + private long calculateCacheExpiresAt(Credentials credentials) { long expiresAt = credentials.getExpiresAt().getTime(); if (credentials.getIdToken() != null) { From 92b8d65f84ef5b7227fcf25b2e23c432c9208170 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Mon, 2 Nov 2020 18:46:58 -0300 Subject: [PATCH 14/18] add minTtl for hasValidCredentials and update tests --- .../storage/SecureCredentialsManager.java | 39 +++-- .../storage/SecureCredentialsManagerTest.java | 145 ++++++++++++++---- 2 files changed, 147 insertions(+), 37 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java index 6fd0eb4f7..845441caf 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java @@ -42,7 +42,8 @@ public class SecureCredentialsManager { private static final String TAG = SecureCredentialsManager.class.getSimpleName(); private static final String KEY_CREDENTIALS = "com.auth0.credentials"; - private static final String KEY_EXPIRES_AT = "com.auth0.credentials_expires_at"; + private static final String KEY_EXPIRES_AT = "com.auth0.credentials_access_token_expires_at"; + private static final String KEY_CACHE_EXPIRES_AT = "com.auth0.credentials_expires_at"; private static final String KEY_CAN_REFRESH = "com.auth0.credentials_can_refresh"; private static final String KEY_CRYPTO_ALIAS = "com.auth0.manager_key_alias"; @VisibleForTesting @@ -173,7 +174,8 @@ public void saveCredentials(@NonNull Credentials credentials) throws Credentials byte[] encrypted = crypto.encrypt(json.getBytes()); String encryptedEncoded = Base64.encodeToString(encrypted, Base64.DEFAULT); storage.store(KEY_CREDENTIALS, encryptedEncoded); - storage.store(KEY_EXPIRES_AT, cacheExpiresAt); + storage.store(KEY_EXPIRES_AT, credentials.getExpiresAt().getTime()); + storage.store(KEY_CACHE_EXPIRES_AT, cacheExpiresAt); storage.store(KEY_CAN_REFRESH, canRefresh); storage.store(KEY_CRYPTO_ALIAS, KEY_ALIAS); } catch (IncompatibleDeviceException e) { @@ -240,6 +242,7 @@ public void getCredentials(@Nullable String scope, int minTtl, @NonNull BaseCall public void clearCredentials() { storage.remove(KEY_CREDENTIALS); storage.remove(KEY_EXPIRES_AT); + storage.remove(KEY_CACHE_EXPIRES_AT); storage.remove(KEY_CAN_REFRESH); storage.remove(KEY_CRYPTO_ALIAS); Log.d(TAG, "Credentials were just removed from the storage"); @@ -251,13 +254,26 @@ public void clearCredentials() { * @return whether this manager contains a valid non-expired pair of credentials or not. */ public boolean hasValidCredentials() { + return hasValidCredentials(0); + } + + /** + * Returns whether this manager contains a valid non-expired pair of credentials. + * + * @param minTtl the minimum time in seconds that the access token should last before expiration. + * @return whether this manager contains a valid non-expired pair of credentials or not. + */ + public boolean hasValidCredentials(long minTtl) { String encryptedEncoded = storage.retrieveString(KEY_CREDENTIALS); Long expiresAt = storage.retrieveLong(KEY_EXPIRES_AT); + Long cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT); Boolean canRefresh = storage.retrieveBoolean(KEY_CAN_REFRESH); String keyAliasUsed = storage.retrieveString(KEY_CRYPTO_ALIAS); + boolean emptyCredentials = isEmpty(encryptedEncoded) || cacheExpiresAt == null; + return KEY_ALIAS.equals(keyAliasUsed) && - !(isEmpty(encryptedEncoded) || expiresAt == null || - expiresAt <= getCurrentTimeInMillis() && (canRefresh == null || !canRefresh)); + !(emptyCredentials || (hasExpired(cacheExpiresAt) || willExpire(expiresAt, minTtl)) && + (canRefresh == null || !canRefresh)); } private void continueGetCredentials(@Nullable String scope, final int minTtl, final BaseCallback callback) { @@ -280,7 +296,8 @@ private void continueGetCredentials(@Nullable String scope, final int minTtl, fi return; } final Credentials credentials = gson.fromJson(json, Credentials.class); - Long cacheExpiresAt = storage.retrieveLong(KEY_EXPIRES_AT); + Long cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT); + Long expiresAt = credentials.getExpiresAt().getTime(); boolean hasEmptyCredentials = isEmpty(credentials.getAccessToken()) && isEmpty(credentials.getIdToken()) || cacheExpiresAt == null; if (hasEmptyCredentials) { callback.onFailure(new CredentialsManagerException("No Credentials were previously set.")); @@ -289,8 +306,8 @@ private void continueGetCredentials(@Nullable String scope, final int minTtl, fi } //noinspection ConstantConditions - boolean hasEitherExpired = hasExpired(credentials.getExpiresAt().getTime()); - boolean willAccessTokenExpire = willExpire(cacheExpiresAt, minTtl); + boolean hasEitherExpired = hasExpired(cacheExpiresAt); + boolean willAccessTokenExpire = willExpire(expiresAt, minTtl); //noinspection ConstantConditions boolean scopeChanged = hasScopeChanged(credentials.getScope(), scope); @@ -317,7 +334,7 @@ public void onSuccess(@Nullable Credentials fresh) { boolean willAccessTokenExpire = willExpire(expiresAt, minTtl); if (willAccessTokenExpire) { long tokenLifetime = (expiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; - CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token or Id Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); + CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); callback.onFailure(wrongTtlException); decryptCallback = null; return; @@ -355,7 +372,11 @@ private boolean hasScopeChanged(@NonNull String storedScope, @Nullable String re return stored != required; } - private boolean willExpire(long expiresAt, long minTtl) { + private boolean willExpire(Long expiresAt, long minTtl) { + if (minTtl == 0 && (expiresAt == null || expiresAt == 0)) { + //expiresAt (access token) only considered if it has a positive value, to avoid logging out users + return false; + } long nextClock = getCurrentTimeInMillis() + minTtl * 1000; return expiresAt <= nextClock; } diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java index 0f6341df5..1c3221a38 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java @@ -63,6 +63,8 @@ @Config(sdk = 21) public class SecureCredentialsManagerTest { + private static final long ONE_HOUR_SECONDS = 60 * 60; + @Mock private AuthenticationAPIClient client; @Mock @@ -118,7 +120,7 @@ public void shouldCreateAManagerInstance() { @Test public void shouldSaveRefreshableCredentialsInStorage() { - long sharedExpirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long sharedExpirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", "refreshToken", new Date(sharedExpirationTime), "scope"); String json = gson.toJson(credentials); prepareJwtDecoderMock(new Date(sharedExpirationTime)); @@ -128,6 +130,7 @@ public void shouldSaveRefreshableCredentialsInStorage() { verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); verify(storage).store("com.auth0.credentials_expires_at", sharedExpirationTime); + verify(storage).store("com.auth0.credentials_access_token_expires_at", sharedExpirationTime); verify(storage).store("com.auth0.credentials_can_refresh", true); verify(storage).store("com.auth0.manager_key_alias", SecureCredentialsManager.KEY_ALIAS); verifyNoMoreInteractions(storage); @@ -146,7 +149,7 @@ public void shouldSaveRefreshableCredentialsInStorage() { @Test public void shouldSaveRefreshableCredentialsUsingAccessTokenExpForCacheExpirationInStorage() { - long accessTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long accessTokenExpirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; Credentials credentials = new CredentialsMock(null, "accessToken", "type", "refreshToken", new Date(accessTokenExpirationTime), "scope"); String json = gson.toJson(credentials); prepareJwtDecoderMock(new Date(accessTokenExpirationTime)); @@ -156,6 +159,7 @@ public void shouldSaveRefreshableCredentialsUsingAccessTokenExpForCacheExpiratio verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); verify(storage).store("com.auth0.credentials_expires_at", accessTokenExpirationTime); + verify(storage).store("com.auth0.credentials_access_token_expires_at", accessTokenExpirationTime); verify(storage).store("com.auth0.credentials_can_refresh", true); verify(storage).store("com.auth0.manager_key_alias", SecureCredentialsManager.KEY_ALIAS); verifyNoMoreInteractions(storage); @@ -174,7 +178,7 @@ public void shouldSaveRefreshableCredentialsUsingAccessTokenExpForCacheExpiratio @Test public void shouldSaveRefreshableCredentialsUsingIdTokenExpForCacheExpirationInStorage() { - long accessTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long accessTokenExpirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; long idTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 2000 * 1000; Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", "refreshToken", new Date(accessTokenExpirationTime), "scope"); String json = gson.toJson(credentials); @@ -185,6 +189,7 @@ public void shouldSaveRefreshableCredentialsUsingIdTokenExpForCacheExpirationInS verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); verify(storage).store("com.auth0.credentials_expires_at", idTokenExpirationTime); + verify(storage).store("com.auth0.credentials_access_token_expires_at", accessTokenExpirationTime); verify(storage).store("com.auth0.credentials_can_refresh", true); verify(storage).store("com.auth0.manager_key_alias", SecureCredentialsManager.KEY_ALIAS); verifyNoMoreInteractions(storage); @@ -203,7 +208,7 @@ public void shouldSaveRefreshableCredentialsUsingIdTokenExpForCacheExpirationInS @Test public void shouldSaveNonRefreshableCredentialsInStorage() { - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", null, new Date(expirationTime), "scope"); String json = gson.toJson(credentials); prepareJwtDecoderMock(new Date(expirationTime)); @@ -213,6 +218,7 @@ public void shouldSaveNonRefreshableCredentialsInStorage() { verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); verify(storage).store("com.auth0.credentials_expires_at", expirationTime); + verify(storage).store("com.auth0.credentials_access_token_expires_at", expirationTime); verify(storage).store("com.auth0.credentials_can_refresh", false); verify(storage).store("com.auth0.manager_key_alias", SecureCredentialsManager.KEY_ALIAS); verifyNoMoreInteractions(storage); @@ -231,7 +237,7 @@ public void shouldSaveNonRefreshableCredentialsInStorage() { @Test public void shouldClearStoredCredentialsAndThrowOnSaveOnCryptoException() { - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", "refreshToken", new Date(expirationTime), "scope"); prepareJwtDecoderMock(new Date(expirationTime)); when(crypto.encrypt(any(byte[].class))).thenThrow(new CryptoException(null, null)); @@ -253,7 +259,7 @@ public void shouldClearStoredCredentialsAndThrowOnSaveOnCryptoException() { @Test public void shouldThrowOnSaveOnIncompatibleDeviceException() { - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456 * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; Credentials credentials = new CredentialsMock("idToken", "accessToken", "type", "refreshToken", new Date(expirationTime), "scope"); prepareJwtDecoderMock(new Date(expirationTime)); when(crypto.encrypt(any(byte[].class))).thenThrow(new IncompatibleDeviceException(null)); @@ -274,7 +280,7 @@ public void shouldThrowOnSaveIfCredentialsDoesNotHaveIdTokenOrAccessToken() { exception.expect(CredentialsManagerException.class); exception.expectMessage("Credentials must have a valid date of expiration and a valid access_token or id_token value."); - Credentials credentials = new CredentialsMock(null, null, "type", "refreshToken", 123456L); + Credentials credentials = new CredentialsMock(null, null, "type", "refreshToken", ONE_HOUR_SECONDS); manager.saveCredentials(credentials); } @@ -291,14 +297,14 @@ public void shouldThrowOnSaveIfCredentialsDoesNotHaveExpiresAt() { @Test public void shouldNotThrowOnSaveIfCredentialsHaveAccessTokenAndExpiresIn() { - Credentials credentials = new CredentialsMock(null, "accessToken", "type", "refreshToken", 123456L); + Credentials credentials = new CredentialsMock(null, "accessToken", "type", "refreshToken", ONE_HOUR_SECONDS); when(crypto.encrypt(any(byte[].class))).thenReturn(new byte[]{12, 34, 56, 78}); manager.saveCredentials(credentials); } @Test public void shouldNotThrowOnSaveIfCredentialsHaveIdTokenAndExpiresIn() { - Credentials credentials = new CredentialsMock("idToken", null, "type", "refreshToken", 123456L); + Credentials credentials = new CredentialsMock("idToken", null, "type", "refreshToken", ONE_HOUR_SECONDS); prepareJwtDecoderMock(new Date()); when(crypto.encrypt(any(byte[].class))).thenReturn(new byte[]{12, 34, 56, 78}); manager.saveCredentials(credentials); @@ -312,7 +318,7 @@ public void shouldNotThrowOnSaveIfCredentialsHaveIdTokenAndExpiresIn() { public void shouldClearStoredCredentialsAndFailOnGetCredentialsWhenCryptoExceptionIsThrown() { verifyNoMoreInteractions(client); - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); + Date expiresAt = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); String storedJson = insertTestCredentials(true, true, true, expiresAt); when(crypto.decrypt(storedJson.getBytes())).thenThrow(new CryptoException(null, null)); manager.getCredentials(callback); @@ -334,7 +340,7 @@ public void shouldClearStoredCredentialsAndFailOnGetCredentialsWhenCryptoExcepti public void shouldFailOnGetCredentialsWhenIncompatibleDeviceExceptionIsThrown() { verifyNoMoreInteractions(client); - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); + Date expiresAt = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); String storedJson = insertTestCredentials(true, true, true, expiresAt); when(crypto.decrypt(storedJson.getBytes())).thenThrow(new IncompatibleDeviceException(null)); manager.getCredentials(callback); @@ -354,7 +360,7 @@ public void shouldFailOnGetCredentialsWhenIncompatibleDeviceExceptionIsThrown() public void shouldFailOnGetCredentialsWhenNoAccessTokenOrIdTokenWasSaved() { verifyNoMoreInteractions(client); - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); + Date expiresAt = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); insertTestCredentials(false, false, true, expiresAt); manager.getCredentials(callback); @@ -382,7 +388,7 @@ public void shouldFailOnGetCredentialsWhenExpiredAndNoRefreshTokenWasSaved() { public void shouldGetNonExpiredCredentialsFromStorage() { verifyNoMoreInteractions(client); - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); + Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000); insertTestCredentials(true, true, true, expiresAt); manager.getCredentials(callback); @@ -403,7 +409,7 @@ public void shouldGetNonExpiredCredentialsFromStorage() { public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyIdTokenIsAvailable() { verifyNoMoreInteractions(client); - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); + Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000); insertTestCredentials(true, false, true, expiresAt); @@ -425,7 +431,7 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyIdTokenIsAvailable( public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyAccessTokenIsAvailable() { verifyNoMoreInteractions(client); - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); + Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000); insertTestCredentials(false, true, true, expiresAt); manager.getCredentials(callback); @@ -465,6 +471,7 @@ public void shouldRenewCredentialsWithMinTtl() { verify(callback).onSuccess(credentialsCaptor.capture()); verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); verify(storage).store("com.auth0.credentials_expires_at", newDate.getTime()); + verify(storage).store("com.auth0.credentials_access_token_expires_at", newDate.getTime()); verify(storage).store("com.auth0.credentials_can_refresh", true); verify(storage, never()).remove(anyString()); @@ -516,7 +523,7 @@ public void shouldGetAndFailToRenewExpiredCredentialsWhenReceivedTokenHasLowerTt verify(callback).onFailure(exceptionCaptor.capture()); CredentialsManagerException exception = exceptionCaptor.getValue(); assertThat(exception, is(notNullValue())); - assertThat(exception.getMessage(), is("The lifetime of the renewed Access Token or Id Token (1) is less than the minTTL requested (60). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.")); + assertThat(exception.getMessage(), is("The lifetime of the renewed Access Token (1) is less than the minTTL requested (60). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.")); verify(storage, never()).store(eq("com.auth0.credentials"), anyString()); verify(storage, never()).store(eq("com.auth0.credentials_expires_at"), anyLong()); @@ -526,10 +533,10 @@ public void shouldGetAndFailToRenewExpiredCredentialsWhenReceivedTokenHasLowerTt @Test public void shouldRenewCredentialsWhenScopeHasChanged() { - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); // non expired credentials + Date expiresAt = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); // non expired credentials insertTestCredentials(false, true, true, expiresAt); // "scope" is set - Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 2222 * 1000); + Date newDate = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000); JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); @@ -547,6 +554,7 @@ public void shouldRenewCredentialsWhenScopeHasChanged() { verify(callback).onSuccess(credentialsCaptor.capture()); verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); verify(storage).store("com.auth0.credentials_expires_at", newDate.getTime()); + verify(storage).store("com.auth0.credentials_access_token_expires_at", newDate.getTime()); verify(storage).store("com.auth0.credentials_can_refresh", true); verify(storage, never()).remove(anyString()); @@ -574,12 +582,75 @@ public void shouldRenewCredentialsWhenScopeHasChanged() { assertThat(renewedStoredCredentials.getScope(), is("different scope")); } + @Test + public void shouldRenewExpiredCredentialsWhenScopeHasChanged() { + Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS); // current time means expired credentials + insertTestCredentials(false, true, true, expiresAt); + + Date newDate = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); + JWT jwtMock = mock(JWT.class); + when(jwtMock.getExpiresAt()).thenReturn(newDate); + when(jwtDecoder.decode("newId")).thenReturn(jwtMock); + when(client.renewAuth("refreshToken")).thenReturn(request); + + manager.getCredentials(callback); + verify(request, never()).addParameter(eq("scope"), anyString()); + verify(request).start(requestCallbackCaptor.capture()); + + // Trigger success + Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", null, newDate, "newScope"); + Credentials expectedCredentials = new Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope"); + String expectedJson = gson.toJson(expectedCredentials); + when(crypto.encrypt(expectedJson.getBytes())).thenReturn(expectedJson.getBytes()); + requestCallbackCaptor.getValue().onSuccess(renewedCredentials); + verify(callback).onSuccess(credentialsCaptor.capture()); + verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); + verify(storage).store("com.auth0.credentials_expires_at", newDate.getTime()); + verify(storage).store("com.auth0.credentials_access_token_expires_at", newDate.getTime()); + verify(storage).store("com.auth0.credentials_can_refresh", true); + verify(storage, never()).remove(anyString()); + + // Verify the returned credentials are the latest + Credentials retrievedCredentials = credentialsCaptor.getValue(); + assertThat(retrievedCredentials, is(notNullValue())); + assertThat(retrievedCredentials.getIdToken(), is("newId")); + assertThat(retrievedCredentials.getAccessToken(), is("newAccess")); + assertThat(retrievedCredentials.getType(), is("newType")); + assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); + assertThat(retrievedCredentials.getExpiresAt(), is(newDate)); + assertThat(retrievedCredentials.getScope(), is("newScope")); + + // Verify the credentials are property stored + String encodedJson = stringCaptor.getValue(); + assertThat(encodedJson, is(notNullValue())); + final byte[] decoded = Base64.decode(encodedJson, Base64.DEFAULT); + Credentials renewedStoredCredentials = gson.fromJson(new String(decoded), Credentials.class); + assertThat(renewedStoredCredentials.getIdToken(), is("newId")); + assertThat(renewedStoredCredentials.getAccessToken(), is("newAccess")); + assertThat(renewedStoredCredentials.getRefreshToken(), is("refreshToken")); + assertThat(renewedStoredCredentials.getType(), is("newType")); + assertThat(renewedStoredCredentials.getExpiresAt(), is(notNullValue())); + assertThat(renewedStoredCredentials.getExpiresAt().getTime(), is(newDate.getTime())); + assertThat(renewedStoredCredentials.getScope(), is("newScope")); + } + + @Test + public void shouldNotHaveCredentialsWhenAccessTokenWillExpireAndNoRefreshTokenIsAvailable() { + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; + when(storage.retrieveLong("com.auth0.credentials_expires_at")).thenReturn(expirationTime); + when(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")).thenReturn(expirationTime); + when(storage.retrieveBoolean("com.auth0.credentials_can_refresh")).thenReturn(false); + when(storage.retrieveString("com.auth0.credentials")).thenReturn("{\"id_token\":\"idToken\"}"); + + assertFalse(manager.hasValidCredentials(ONE_HOUR_SECONDS)); + } + @Test public void shouldGetAndSuccessfullyRenewExpiredCredentials() { Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS); // current time means expired credentials insertTestCredentials(false, true, true, expiresAt); - Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); + Date newDate = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); @@ -630,13 +701,14 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentialsWithRefreshTokenRotat Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS); insertTestCredentials(false, true, true, expiresAt); - Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000); + Date newDate = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); JWT jwtMock = mock(JWT.class); when(jwtMock.getExpiresAt()).thenReturn(newDate); when(jwtDecoder.decode("newId")).thenReturn(jwtMock); when(client.renewAuth("refreshToken")).thenReturn(request); manager.getCredentials(callback); + verify(request, never()).addParameter(eq("scope"), anyString()); verify(request).start(requestCallbackCaptor.capture()); //Trigger success @@ -709,6 +781,7 @@ public void shouldClearCredentials() { verify(storage).remove("com.auth0.credentials"); verify(storage).remove("com.auth0.credentials_expires_at"); + verify(storage).remove("com.auth0.credentials_access_token_expires_at"); verify(storage).remove("com.auth0.credentials_can_refresh"); verify(storage).remove("com.auth0.manager_key_alias"); verifyNoMoreInteractions(storage); @@ -718,17 +791,33 @@ public void shouldClearCredentials() { * HAS Credentials tests */ + @Test + public void shouldHaveValidCredentialsDuringMigrationWhenAccessTokenExpiresAtValueWasNotSaved() { + long cacheExpiresAt = CredentialsMock.ONE_HOUR_AHEAD_MS; + when(storage.retrieveLong("com.auth0.credentials_expires_at")).thenReturn(cacheExpiresAt); + when(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")).thenReturn(null); + when(storage.retrieveBoolean("com.auth0.credentials_can_refresh")).thenReturn(false); + when(storage.retrieveString("com.auth0.credentials")).thenReturn("{\"id_token\":\"idToken\"}"); + when(storage.retrieveString("com.auth0.manager_key_alias")).thenReturn(SecureCredentialsManager.KEY_ALIAS); + assertThat(manager.hasValidCredentials(), is(true)); + + when(storage.retrieveString("com.auth0.credentials")).thenReturn("{\"access_token\":\"accessToken\"}"); + assertThat(manager.hasValidCredentials(), is(true)); + } + @Test public void shouldHaveCredentialsWhenTokenHasNotExpired() { - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.credentials_expires_at")).thenReturn(expirationTime); when(storage.retrieveBoolean("com.auth0.credentials_can_refresh")).thenReturn(false); when(storage.retrieveString("com.auth0.credentials")).thenReturn("{\"id_token\":\"idToken\"}"); when(storage.retrieveString("com.auth0.manager_key_alias")).thenReturn(SecureCredentialsManager.KEY_ALIAS); assertThat(manager.hasValidCredentials(), is(true)); + assertThat(manager.hasValidCredentials(ONE_HOUR_SECONDS - 1), is(true)); when(storage.retrieveString("com.auth0.credentials")).thenReturn("{\"access_token\":\"accessToken\"}"); assertThat(manager.hasValidCredentials(), is(true)); + assertThat(manager.hasValidCredentials(ONE_HOUR_SECONDS - 1), is(true)); } @Test @@ -767,7 +856,7 @@ public void shouldNotHaveCredentialsWhenAccessTokenAndIdTokenAreMissing() { @Test public void shouldNotHaveCredentialsWhenTheAliasUsedHasNotBeenMigratedYet() { - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.credentials_expires_at")).thenReturn(expirationTime); when(storage.retrieveBoolean("com.auth0.credentials_can_refresh")).thenReturn(false); when(storage.retrieveString("com.auth0.credentials")).thenReturn("{\"id_token\":\"idToken\"}"); @@ -780,7 +869,7 @@ public void shouldNotHaveCredentialsWhenTheAliasUsedHasNotBeenMigratedYet() { @Test public void shouldNotHaveCredentialsWhenTheAliasUsedHasNotBeenSetYet() { - long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; + long expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.credentials_expires_at")).thenReturn(expirationTime); when(storage.retrieveBoolean("com.auth0.credentials_can_refresh")).thenReturn(false); when(storage.retrieveString("com.auth0.credentials")).thenReturn("{\"id_token\":\"idToken\"}"); @@ -878,7 +967,7 @@ public void shouldRequireAuthenticationIfAPI23AndLockScreenEnabled() { @Test public void shouldGetCredentialsAfterAuthentication() { - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); + Date expiresAt = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); insertTestCredentials(true, true, false, expiresAt); when(storage.retrieveLong("com.auth0.credentials_expires_at")).thenReturn(expiresAt.getTime()); @@ -921,8 +1010,8 @@ public void shouldGetCredentialsAfterAuthentication() { @Test public void shouldNotGetCredentialsWhenCredentialsHaveExpired() { - Date credentialsExpiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); - Date storedExpiresAt = new Date(CredentialsMock.CURRENT_TIME_MS - 60L * 1000); + Date credentialsExpiresAt = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); + Date storedExpiresAt = new Date(CredentialsMock.CURRENT_TIME_MS - ONE_HOUR_SECONDS * 1000); insertTestCredentials(true, true, false, credentialsExpiresAt); when(storage.retrieveLong("com.auth0.credentials_expires_at")).thenReturn(storedExpiresAt.getTime()); @@ -951,7 +1040,7 @@ public void shouldNotGetCredentialsWhenCredentialsHaveExpired() { @Test public void shouldNotGetCredentialsAfterCanceledAuthentication() { - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); + Date expiresAt = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); insertTestCredentials(true, true, false, expiresAt); //Require authentication @@ -984,7 +1073,7 @@ public void shouldNotGetCredentialsAfterCanceledAuthentication() { @Test public void shouldNotGetCredentialsOnDifferentAuthenticationRequestCode() { - Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456L * 1000); + Date expiresAt = new Date(CredentialsMock.ONE_HOUR_AHEAD_MS); insertTestCredentials(true, true, false, expiresAt); //Require authentication From 1643e3804d9be09c829ce75123202dfc10ddc2a9 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 3 Nov 2020 12:02:47 -0300 Subject: [PATCH 15/18] chore PR comments --- .../storage/SecureCredentialsManager.java | 10 ++++---- .../storage/SecureCredentialsManagerTest.java | 23 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java index 845441caf..0a4553212 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java @@ -220,7 +220,7 @@ public void getCredentials(@NonNull BaseCallback callback) { - if (!hasValidCredentials()) { + if (!hasValidCredentials(minTtl)) { callback.onFailure(new CredentialsManagerException("No Credentials were previously set.")); return; } @@ -369,11 +369,11 @@ private boolean hasScopeChanged(@NonNull String storedScope, @Nullable String re Arrays.sort(stored); String[] required = requiredScope.split(" "); Arrays.sort(required); - return stored != required; + return !Arrays.equals(stored, required); } - private boolean willExpire(Long expiresAt, long minTtl) { - if (minTtl == 0 && (expiresAt == null || expiresAt == 0)) { + private boolean willExpire(@Nullable Long expiresAt, long minTtl) { + if (expiresAt == null || expiresAt <= 0) { //expiresAt (access token) only considered if it has a positive value, to avoid logging out users return false; } @@ -385,7 +385,7 @@ private boolean hasExpired(long expiresAt) { return expiresAt <= getCurrentTimeInMillis(); } - private long calculateCacheExpiresAt(Credentials credentials) { + private long calculateCacheExpiresAt(@NonNull Credentials credentials) { long expiresAt = credentials.getExpiresAt().getTime(); if (credentials.getIdToken() != null) { diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java index 1c3221a38..b80c63868 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.java @@ -459,7 +459,7 @@ public void shouldRenewCredentialsWithMinTtl() { when(jwtDecoder.decode("newId")).thenReturn(jwtMock); when(client.renewAuth("refreshToken")).thenReturn(request); - manager.getCredentials(null, 60, callback); + manager.getCredentials(null, 60, callback); // minTTL of 1 minute verify(request, never()).addParameter(eq("scope"), anyString()); verify(request).start(requestCallbackCaptor.capture()); @@ -510,11 +510,11 @@ public void shouldGetAndFailToRenewExpiredCredentialsWhenReceivedTokenHasLowerTt when(jwtDecoder.decode("newId")).thenReturn(jwtMock); when(client.renewAuth("refreshToken")).thenReturn(request); - manager.getCredentials(null, 60, callback); + manager.getCredentials(null, 60, callback); // minTTL of 1 minute verify(request, never()).addParameter(eq("scope"), anyString()); verify(request).start(requestCallbackCaptor.capture()); - // Trigger success + // Trigger failure Credentials expectedCredentials = new Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope"); String expectedJson = gson.toJson(expectedCredentials); when(crypto.encrypt(expectedJson.getBytes())).thenReturn(expectedJson.getBytes()); @@ -542,7 +542,7 @@ public void shouldRenewCredentialsWhenScopeHasChanged() { when(jwtDecoder.decode("newId")).thenReturn(jwtMock); when(client.renewAuth("refreshToken")).thenReturn(request); - manager.getCredentials("different scope", 0, callback); + manager.getCredentials("different scope", 0, callback); // minTTL of 0 seconds (default) verify(request).addParameter(eq("scope"), eq("different scope")); verify(request).start(requestCallbackCaptor.capture()); @@ -593,16 +593,15 @@ public void shouldRenewExpiredCredentialsWhenScopeHasChanged() { when(jwtDecoder.decode("newId")).thenReturn(jwtMock); when(client.renewAuth("refreshToken")).thenReturn(request); - manager.getCredentials(callback); - verify(request, never()).addParameter(eq("scope"), anyString()); + manager.getCredentials("different scope", 0, callback); // minTTL of 0 seconds (default) + verify(request).addParameter(eq("scope"), eq("different scope")); verify(request).start(requestCallbackCaptor.capture()); // Trigger success - Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", null, newDate, "newScope"); - Credentials expectedCredentials = new Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope"); + Credentials expectedCredentials = new Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "different scope"); String expectedJson = gson.toJson(expectedCredentials); when(crypto.encrypt(expectedJson.getBytes())).thenReturn(expectedJson.getBytes()); - requestCallbackCaptor.getValue().onSuccess(renewedCredentials); + requestCallbackCaptor.getValue().onSuccess(expectedCredentials); verify(callback).onSuccess(credentialsCaptor.capture()); verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()); verify(storage).store("com.auth0.credentials_expires_at", newDate.getTime()); @@ -618,7 +617,7 @@ public void shouldRenewExpiredCredentialsWhenScopeHasChanged() { assertThat(retrievedCredentials.getType(), is("newType")); assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken")); assertThat(retrievedCredentials.getExpiresAt(), is(newDate)); - assertThat(retrievedCredentials.getScope(), is("newScope")); + assertThat(retrievedCredentials.getScope(), is("different scope")); // Verify the credentials are property stored String encodedJson = stringCaptor.getValue(); @@ -631,7 +630,7 @@ public void shouldRenewExpiredCredentialsWhenScopeHasChanged() { assertThat(renewedStoredCredentials.getType(), is("newType")); assertThat(renewedStoredCredentials.getExpiresAt(), is(notNullValue())); assertThat(renewedStoredCredentials.getExpiresAt().getTime(), is(newDate.getTime())); - assertThat(renewedStoredCredentials.getScope(), is("newScope")); + assertThat(renewedStoredCredentials.getScope(), is("different scope")); } @Test @@ -792,7 +791,7 @@ public void shouldClearCredentials() { */ @Test - public void shouldHaveValidCredentialsDuringMigrationWhenAccessTokenExpiresAtValueWasNotSaved() { + public void shouldPreventLoggingOutUsersWhenAccessTokenExpiresAtWasNotSaved() { long cacheExpiresAt = CredentialsMock.ONE_HOUR_AHEAD_MS; when(storage.retrieveLong("com.auth0.credentials_expires_at")).thenReturn(cacheExpiresAt); when(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")).thenReturn(null); From e56425ea3a3eca16a8ed5ad127cbf22f7dc83ac0 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 3 Nov 2020 13:27:56 -0300 Subject: [PATCH 16/18] abstract common logic to a base credentials manager class --- .../storage/BaseCredentialsManager.java | 126 ++++++++++++++++++ .../storage/CredentialsManager.java | 76 ++--------- .../storage/SecureCredentialsManager.java | 88 +++--------- 3 files changed, 153 insertions(+), 137 deletions(-) create mode 100644 auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.java diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.java new file mode 100644 index 000000000..8da0a22ce --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.java @@ -0,0 +1,126 @@ +package com.auth0.android.authentication.storage; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import com.auth0.android.authentication.AuthenticationAPIClient; +import com.auth0.android.callback.BaseCallback; +import com.auth0.android.jwt.JWT; +import com.auth0.android.result.Credentials; +import com.auth0.android.util.Clock; + +import java.util.Arrays; +import java.util.Date; + +/** + * Base class meant to abstract common logic across Credentials Manager implementations. + * The scope of this class is package-private, as it's not meant to expose + */ +abstract class BaseCredentialsManager { + + protected final AuthenticationAPIClient authenticationClient; + protected final Storage storage; + protected final JWTDecoder jwtDecoder; + protected Clock clock; + + BaseCredentialsManager(@NonNull AuthenticationAPIClient authenticationClient, @NonNull Storage storage, @NonNull JWTDecoder jwtDecoder) { + this.authenticationClient = authenticationClient; + this.storage = storage; + this.jwtDecoder = jwtDecoder; + this.clock = new ClockImpl(); + } + + public abstract void saveCredentials(@NonNull Credentials credentials) throws CredentialsManagerException; + + public abstract void getCredentials(@NonNull BaseCallback callback); + + public abstract void getCredentials(@Nullable String scope, int minTtl, @NonNull BaseCallback callback); + + public abstract void clearCredentials(); + + public abstract boolean hasValidCredentials(); + + public abstract boolean hasValidCredentials(long minTtl); + + /** + * Updates the clock instance used for expiration verification purposes. + * The use of this method can help on situations where the clock comes from an external synced source. + * The default implementation uses the time returned by {@link System#currentTimeMillis()}. + * + * @param clock the new clock instance to use. + */ + public void setClock(@NonNull Clock clock) { + this.clock = clock; + } + + @VisibleForTesting + long getCurrentTimeInMillis() { + return clock.getCurrentTimeMillis(); + } + + /** + * Checks if the stored scope is the same as the requested one. + * + * @param storedScope the stored scope, separated by space characters. + * @param requiredScope the required scope, separated by space characters. + * @return whether the scope are different or not + */ + protected boolean hasScopeChanged(@NonNull String storedScope, @Nullable String requiredScope) { + if (requiredScope == null) { + return false; + } + String[] stored = storedScope.split(" "); + Arrays.sort(stored); + String[] required = requiredScope.split(" "); + Arrays.sort(required); + return !Arrays.equals(stored, required); + } + + /** + * Checks if given the required minimum time to live, the expiration time can satisfy that value or not. + * + * @param expiresAt the expiration time, in milliseconds. + * @param minTtl the time to live required, in seconds. + * @return whether the value will become expired within the given min TTL or not. + */ + protected boolean willExpire(long expiresAt, long minTtl) { + if (expiresAt <= 0) { + // Avoids logging out users when this value was not saved (migration scenario) + return false; + } + long nextClock = getCurrentTimeInMillis() + minTtl * 1000; + return expiresAt <= nextClock; + } + + /** + * Checks whether the given expiration time has been reached or not. + * + * @param expiresAt the expiration time, in milliseconds. + * @return whether the given expiration time has been reached or not. + */ + protected boolean hasExpired(long expiresAt) { + return expiresAt <= getCurrentTimeInMillis(); + } + + /** + * Takes a credentials object and returns the lowest expiration time, considering + * both the access token and the ID token expiration time. + * + * @param credentials the credentials object to check. + * @return the lowest expiration time between the access token and the ID token. + */ + protected long calculateCacheExpiresAt(@NonNull Credentials credentials) { + long expiresAt = credentials.getExpiresAt().getTime(); + + if (credentials.getIdToken() != null) { + JWT idToken = jwtDecoder.decode(credentials.getIdToken()); + Date idTokenExpiresAtDate = idToken.getExpiresAt(); + + if (idTokenExpiresAtDate != null) { + expiresAt = Math.min(idTokenExpiresAtDate.getTime(), expiresAt); + } + } + return expiresAt; + } +} diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java index 14f8a1022..a5b7be242 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.java @@ -8,13 +8,11 @@ import com.auth0.android.authentication.AuthenticationException; import com.auth0.android.callback.AuthenticationCallback; import com.auth0.android.callback.BaseCallback; -import com.auth0.android.jwt.JWT; import com.auth0.android.request.ParameterizableRequest; import com.auth0.android.result.Credentials; -import com.auth0.android.util.Clock; -import java.util.Arrays; import java.util.Date; +import java.util.Locale; import static android.text.TextUtils.isEmpty; @@ -22,7 +20,7 @@ * Class that handles credentials and allows to save and retrieve them. */ @SuppressWarnings({"WeakerAccess", "unused"}) -public class CredentialsManager { +public class CredentialsManager extends BaseCredentialsManager { private static final String KEY_ACCESS_TOKEN = "com.auth0.access_token"; private static final String KEY_REFRESH_TOKEN = "com.auth0.refresh_token"; private static final String KEY_ID_TOKEN = "com.auth0.id_token"; @@ -31,17 +29,9 @@ public class CredentialsManager { private static final String KEY_SCOPE = "com.auth0.scope"; private static final String KEY_CACHE_EXPIRES_AT = "com.auth0.cache_expires_at"; - private final AuthenticationAPIClient authClient; - private final Storage storage; - private final JWTDecoder jwtDecoder; - private Clock clock; - @VisibleForTesting CredentialsManager(@NonNull AuthenticationAPIClient authenticationClient, @NonNull Storage storage, @NonNull JWTDecoder jwtDecoder) { - this.authClient = authenticationClient; - this.storage = storage; - this.jwtDecoder = jwtDecoder; - this.clock = new ClockImpl(); + super(authenticationClient, storage, jwtDecoder); } /** @@ -54,22 +44,12 @@ public CredentialsManager(@NonNull AuthenticationAPIClient authenticationClient, this(authenticationClient, storage, new JWTDecoder()); } - /** - * Updates the clock instance used for expiration verification purposes. - * The use of this method can help on situations where the clock comes from an external synced source. - * The default implementation uses the time returned by {@link System#currentTimeMillis()}. - * - * @param clock the new clock instance to use. - */ - public void setClock(@NonNull Clock clock) { - this.clock = clock; - } - /** * Stores the given credentials in the storage. Must have an access_token or id_token and a expires_in value. * * @param credentials the credentials to save in the storage. */ + @Override public void saveCredentials(@NonNull Credentials credentials) { if ((isEmpty(credentials.getAccessToken()) && isEmpty(credentials.getIdToken())) || credentials.getExpiresAt() == null) { throw new CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value."); @@ -93,6 +73,7 @@ public void saveCredentials(@NonNull Credentials credentials) { * * @param callback the callback that will receive a valid {@link Credentials} or the {@link CredentialsManagerException}. */ + @Override public void getCredentials(@NonNull BaseCallback callback) { getCredentials(null, 0, callback); } @@ -106,6 +87,7 @@ public void getCredentials(@NonNull BaseCallback callback) { String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN); final String refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN); @@ -137,7 +119,7 @@ public void getCredentials(@Nullable String scope, final int minTtl, @NonNull fi return; } - final ParameterizableRequest request = authClient.renewAuth(refreshToken); + final ParameterizableRequest request = authenticationClient.renewAuth(refreshToken); if (scope != null) { request.addParameter("scope", scope); } @@ -148,7 +130,7 @@ public void onSuccess(@Nullable Credentials fresh) { boolean willAccessTokenExpire = willExpire(expiresAt, minTtl); if (willAccessTokenExpire) { long tokenLifetime = (expiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; - CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); + CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format(Locale.getDefault(), "The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); callback.onFailure(wrongTtlException); return; } @@ -168,45 +150,12 @@ public void onFailure(@NonNull AuthenticationException error) { } - private boolean hasScopeChanged(@NonNull String storedScope, @Nullable String requiredScope) { - if (requiredScope == null) { - return false; - } - String[] stored = storedScope.split(" "); - Arrays.sort(stored); - String[] required = requiredScope.split(" "); - Arrays.sort(required); - return !Arrays.equals(stored, required); - } - - private boolean willExpire(long expiresAt, long minTtl) { - long nextClock = getCurrentTimeInMillis() + minTtl * 1000; - return expiresAt <= nextClock; - } - - private boolean hasExpired(long expiresAt) { - return expiresAt <= getCurrentTimeInMillis(); - } - - private long calculateCacheExpiresAt(@NonNull Credentials credentials) { - long expiresAt = credentials.getExpiresAt().getTime(); - - if (credentials.getIdToken() != null) { - JWT idToken = jwtDecoder.decode(credentials.getIdToken()); - Date idTokenExpiresAtDate = idToken.getExpiresAt(); - - if (idTokenExpiresAtDate != null) { - expiresAt = Math.min(idTokenExpiresAtDate.getTime(), expiresAt); - } - } - return expiresAt; - } - /** * Checks if a non-expired pair of credentials can be obtained from this manager. * * @return whether there are valid credentials stored on this manager. */ + @Override public boolean hasValidCredentials() { return hasValidCredentials(0); } @@ -217,6 +166,7 @@ public boolean hasValidCredentials() { * @param minTtl the minimum time in seconds that the access token should last before expiration. * @return whether there are valid credentials stored on this manager. */ + @Override public boolean hasValidCredentials(long minTtl) { String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN); String refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN); @@ -234,6 +184,7 @@ public boolean hasValidCredentials(long minTtl) { /** * Removes the credentials from the storage if present. */ + @Override public void clearCredentials() { storage.remove(KEY_ACCESS_TOKEN); storage.remove(KEY_REFRESH_TOKEN); @@ -249,9 +200,4 @@ Credentials recreateCredentials(String idToken, String accessToken, String token return new Credentials(idToken, accessToken, tokenType, refreshToken, expiresAt, scope); } - @VisibleForTesting - long getCurrentTimeInMillis() { - return clock.getCurrentTimeMillis(); - } - } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java index 0a4553212..67f457c4e 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.java @@ -18,15 +18,12 @@ import com.auth0.android.authentication.AuthenticationException; import com.auth0.android.callback.AuthenticationCallback; import com.auth0.android.callback.BaseCallback; -import com.auth0.android.jwt.JWT; import com.auth0.android.request.ParameterizableRequest; import com.auth0.android.request.internal.GsonProvider; import com.auth0.android.result.Credentials; -import com.auth0.android.util.Clock; import com.google.gson.Gson; -import java.util.Arrays; -import java.util.Date; +import java.util.Locale; import static android.text.TextUtils.isEmpty; @@ -37,7 +34,7 @@ */ @SuppressWarnings({"WeakerAccess", "unused"}) @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) -public class SecureCredentialsManager { +public class SecureCredentialsManager extends BaseCredentialsManager { private static final String TAG = SecureCredentialsManager.class.getSimpleName(); @@ -49,12 +46,8 @@ public class SecureCredentialsManager { @VisibleForTesting static final String KEY_ALIAS = "com.auth0.key"; - private final AuthenticationAPIClient apiClient; - private final Storage storage; private final CryptoUtil crypto; private final Gson gson; - private final JWTDecoder jwtDecoder; - private Clock clock; //Changeable by the user private boolean authenticateBeforeDecrypt; @@ -70,13 +63,10 @@ public class SecureCredentialsManager { @VisibleForTesting SecureCredentialsManager(@NonNull AuthenticationAPIClient apiClient, @NonNull Storage storage, @NonNull CryptoUtil crypto, @NonNull JWTDecoder jwtDecoder) { - this.apiClient = apiClient; - this.storage = storage; + super(apiClient, storage, jwtDecoder); this.crypto = crypto; this.gson = GsonProvider.buildGson(); this.authenticateBeforeDecrypt = false; - this.jwtDecoder = jwtDecoder; - this.clock = new ClockImpl(); } /** @@ -90,17 +80,6 @@ public SecureCredentialsManager(@NonNull Context context, @NonNull Authenticatio this(apiClient, storage, new CryptoUtil(context, storage, KEY_ALIAS), new JWTDecoder()); } - /** - * Updates the clock instance used for expiration verification purposes. - * The use of this method can help on situations where the clock comes from an external synced source. - * The default implementation uses the time returned by {@link System#currentTimeMillis()}. - * - * @param clock the new clock instance to use. - */ - public void setClock(@NonNull Clock clock) { - this.clock = clock; - } - /** * Require the user to authenticate using the configured LockScreen before accessing the credentials. * This feature is disabled by default and will only work if the device is running on Android version 21 or up and if the user @@ -160,6 +139,7 @@ public boolean checkAuthenticationResult(int requestCode, int resultCode) { * @throws CredentialsManagerException if the credentials couldn't be encrypted. Some devices are not compatible at all with the cryptographic * implementation and will have {@link CredentialsManagerException#isDeviceIncompatible()} return true. */ + @Override public void saveCredentials(@NonNull Credentials credentials) throws CredentialsManagerException { if ((isEmpty(credentials.getAccessToken()) && isEmpty(credentials.getIdToken())) || credentials.getExpiresAt() == null) { throw new CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value."); @@ -202,6 +182,7 @@ public void saveCredentials(@NonNull Credentials credentials) throws Credentials * * @param callback the callback to receive the result in. */ + @Override public void getCredentials(@NonNull BaseCallback callback) { getCredentials(null, 0, callback); } @@ -219,6 +200,7 @@ public void getCredentials(@NonNull BaseCallback callback) { if (!hasValidCredentials(minTtl)) { callback.onFailure(new CredentialsManagerException("No Credentials were previously set.")); @@ -239,6 +221,7 @@ public void getCredentials(@Nullable String scope, int minTtl, @NonNull BaseCall /** * Delete the stored credentials */ + @Override public void clearCredentials() { storage.remove(KEY_CREDENTIALS); storage.remove(KEY_EXPIRES_AT); @@ -253,6 +236,7 @@ public void clearCredentials() { * * @return whether this manager contains a valid non-expired pair of credentials or not. */ + @Override public boolean hasValidCredentials() { return hasValidCredentials(0); } @@ -263,9 +247,14 @@ public boolean hasValidCredentials() { * @param minTtl the minimum time in seconds that the access token should last before expiration. * @return whether this manager contains a valid non-expired pair of credentials or not. */ + @Override public boolean hasValidCredentials(long minTtl) { String encryptedEncoded = storage.retrieveString(KEY_CREDENTIALS); Long expiresAt = storage.retrieveLong(KEY_EXPIRES_AT); + if (expiresAt == null) { + // Avoids logging out users when this value was not saved (migration scenario) + expiresAt = 0L; + } Long cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT); Boolean canRefresh = storage.retrieveBoolean(KEY_CAN_REFRESH); String keyAliasUsed = storage.retrieveString(KEY_CRYPTO_ALIAS); @@ -297,7 +286,7 @@ private void continueGetCredentials(@Nullable String scope, final int minTtl, fi } final Credentials credentials = gson.fromJson(json, Credentials.class); Long cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT); - Long expiresAt = credentials.getExpiresAt().getTime(); + long expiresAt = credentials.getExpiresAt().getTime(); boolean hasEmptyCredentials = isEmpty(credentials.getAccessToken()) && isEmpty(credentials.getIdToken()) || cacheExpiresAt == null; if (hasEmptyCredentials) { callback.onFailure(new CredentialsManagerException("No Credentials were previously set.")); @@ -305,10 +294,8 @@ private void continueGetCredentials(@Nullable String scope, final int minTtl, fi return; } - //noinspection ConstantConditions boolean hasEitherExpired = hasExpired(cacheExpiresAt); boolean willAccessTokenExpire = willExpire(expiresAt, minTtl); - //noinspection ConstantConditions boolean scopeChanged = hasScopeChanged(credentials.getScope(), scope); if (!hasEitherExpired && !willAccessTokenExpire && !scopeChanged) { @@ -323,7 +310,7 @@ private void continueGetCredentials(@Nullable String scope, final int minTtl, fi } Log.d(TAG, "Credentials have expired. Renewing them now..."); - ParameterizableRequest request = apiClient.renewAuth(credentials.getRefreshToken()); + ParameterizableRequest request = authenticationClient.renewAuth(credentials.getRefreshToken()); if (scope != null) { request.addParameter("scope", scope); } @@ -334,7 +321,7 @@ public void onSuccess(@Nullable Credentials fresh) { boolean willAccessTokenExpire = willExpire(expiresAt, minTtl); if (willAccessTokenExpire) { long tokenLifetime = (expiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000; - CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); + CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format(Locale.getDefault(), "The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl)); callback.onFailure(wrongTtlException); decryptCallback = null; return; @@ -356,47 +343,4 @@ public void onFailure(@NonNull AuthenticationException error) { }); } - @VisibleForTesting - long getCurrentTimeInMillis() { - return clock.getCurrentTimeMillis(); - } - - private boolean hasScopeChanged(@NonNull String storedScope, @Nullable String requiredScope) { - if (requiredScope == null) { - return false; - } - String[] stored = storedScope.split(" "); - Arrays.sort(stored); - String[] required = requiredScope.split(" "); - Arrays.sort(required); - return !Arrays.equals(stored, required); - } - - private boolean willExpire(@Nullable Long expiresAt, long minTtl) { - if (expiresAt == null || expiresAt <= 0) { - //expiresAt (access token) only considered if it has a positive value, to avoid logging out users - return false; - } - long nextClock = getCurrentTimeInMillis() + minTtl * 1000; - return expiresAt <= nextClock; - } - - private boolean hasExpired(long expiresAt) { - return expiresAt <= getCurrentTimeInMillis(); - } - - private long calculateCacheExpiresAt(@NonNull Credentials credentials) { - long expiresAt = credentials.getExpiresAt().getTime(); - - if (credentials.getIdToken() != null) { - JWT idToken = jwtDecoder.decode(credentials.getIdToken()); - Date idTokenExpiresAtDate = idToken.getExpiresAt(); - - if (idTokenExpiresAtDate != null) { - expiresAt = Math.min(idTokenExpiresAtDate.getTime(), expiresAt); - } - } - return expiresAt; - } - } From 0ba67aab195c709462b8ce92dbcafddc1a963e3f Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 3 Nov 2020 19:06:47 -0300 Subject: [PATCH 17/18] Update auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.java Co-authored-by: Rita Zerrizuela --- .../android/authentication/storage/BaseCredentialsManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.java b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.java index 8da0a22ce..e54e96208 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.java @@ -15,7 +15,7 @@ /** * Base class meant to abstract common logic across Credentials Manager implementations. - * The scope of this class is package-private, as it's not meant to expose + * The scope of this class is package-private, as it's not meant to be exposed */ abstract class BaseCredentialsManager { From 5a161237e9630e39fe388278ab5e40add2ec8087 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 4 Nov 2020 12:21:11 -0300 Subject: [PATCH 18/18] Release 1.29.0 --- CHANGELOG.md | 7 +++++++ README.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 098563ff5..a9cd2dbca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [1.29.0](https://github.com/auth0/Auth0.Android/tree/1.29.0) (2020-11-04) +[Full Changelog](https://github.com/auth0/Auth0.Android/compare/1.28.0...1.29.0) + +**Added** +- SecureCredentialsManager: Allow to pass scope and minTTL [\#369](https://github.com/auth0/Auth0.Android/pull/369) ([lbalmaceda](https://github.com/lbalmaceda)) +- CredentialsManager: Allow to pass scope and minTTL [\#363](https://github.com/auth0/Auth0.Android/pull/363) ([lbalmaceda](https://github.com/lbalmaceda)) + ## [1.28.0](https://github.com/auth0/Auth0.Android/tree/1.28.0) (2020-10-13) [Full Changelog](https://github.com/auth0/Auth0.Android/compare/1.27.0...1.28.0) diff --git a/README.md b/README.md index 5db93cb63..0dd6992d1 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Auth0.android is available through [Gradle](https://gradle.org/). To install it, ```gradle dependencies { - implementation 'com.auth0.android:auth0:1.28.0' + implementation 'com.auth0.android:auth0:1.29.0' } ```