From c02683b4fa369e13c960bb8e8a9a3657436b05f3 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 27 Sep 2017 19:57:52 -0300 Subject: [PATCH 1/4] save the refreshed credentials. --- .../storage/CredentialsManager.java | 9 ++-- .../storage/SecureCredentialsManager.java | 10 +++-- .../storage/CredentialsManagerTest.java | 32 ++++++++++++++- .../storage/SecureCredentialsManagerTest.java | 41 ++++++++++++++++++- 4 files changed, 82 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 abad58626..90b3f9cdd 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 @@ -65,7 +65,7 @@ public void saveCredentials(@NonNull Credentials credentials) { */ public void getCredentials(@NonNull final BaseCallback callback) { String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN); - String refreshToken = storage.retrieveString(KEY_REFRESH_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); @@ -86,8 +86,11 @@ public void getCredentials(@NonNull final BaseCallback() { @Override - public void onSuccess(Credentials freshCredentials) { - callback.onSuccess(freshCredentials); + public void onSuccess(Credentials fresh) { + //RefreshTokens don't expire. It should remain the same + Credentials credentials = new Credentials(fresh.getIdToken(), fresh.getAccessToken(), fresh.getType(), refreshToken, fresh.getExpiresAt(), fresh.getScope()); + saveCredentials(credentials); + callback.onSuccess(credentials); } @Override 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 7bbcef7fc..483b80678 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,6 +18,7 @@ import com.auth0.android.callback.BaseCallback; import com.auth0.android.result.Credentials; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import static android.text.TextUtils.isEmpty; @@ -206,7 +207,7 @@ private void continueGetCredentials(final BaseCallback() { @Override - public void onSuccess(Credentials refreshedCredentials) { - callback.onSuccess(refreshedCredentials); + public void onSuccess(Credentials fresh) { + //RefreshTokens don't expire. It should remain the same + Credentials refreshed = new Credentials(fresh.getIdToken(), fresh.getAccessToken(), fresh.getType(), credentials.getRefreshToken(), fresh.getExpiresAt(), fresh.getScope()); + saveCredentials(refreshed); + callback.onSuccess(refreshed); decryptCallback = 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 ad15ad903..c3298c558 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 @@ -30,10 +30,14 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -287,13 +291,32 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentials() throws Exception { verify(request).start(requestCallbackCaptor.capture()); //Trigger success - Credentials renewedCredentials = mock(Credentials.class); + Date newDate = new Date(); + 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, never()).remove(anyString()); + + //// Verify the returned credentials are the latest Credentials retrievedCredentials = credentialsCaptor.getValue(); assertThat(retrievedCredentials, is(notNullValue())); - assertThat(retrievedCredentials, is(renewedCredentials)); + 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")); } @SuppressWarnings("UnnecessaryLocalVariable") @@ -309,6 +332,11 @@ public void shouldGetAndFailToRenewExpiredCredentials() throws Exception { when(client.renewAuth("refreshToken")).thenReturn(request); manager.getCredentials(callback); + 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()); verify(request).start(requestCallbackCaptor.capture()); //Trigger failure 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 b11c3d518..d744da84d 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 @@ -40,6 +40,10 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +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.doReturn; import static org.mockito.Mockito.mock; @@ -356,13 +360,41 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentials() throws Exception { verify(request).start(requestCallbackCaptor.capture()); //Trigger success - Credentials renewedCredentials = mock(Credentials.class); + Date newDate = new Date(123412341234L); + 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_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, is(renewedCredentials)); + 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())); + //Gson serializes to String dates and strips a few millis. Nothing critical.. + assertThat(renewedStoredCredentials.getExpiresAt().toString(), is(newDate.toString())); + assertThat(renewedStoredCredentials.getScope(), is("newScope")); } @SuppressWarnings("UnnecessaryLocalVariable") @@ -385,6 +417,11 @@ public void shouldGetAndFailToRenewExpiredCredentials() throws Exception { AuthenticationException authenticationException = mock(AuthenticationException.class); requestCallbackCaptor.getValue().onFailure(authenticationException); verify(callback).onFailure(exceptionCaptor.capture()); + verify(storage, never()).store(anyString(), anyLong()); + verify(storage, never()).store(anyString(), anyInt()); + 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())); From 575a1ae9540f0ef9322605a5d35d3c6fc997a518 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 29 Sep 2017 14:55:07 -0300 Subject: [PATCH 2/4] fix java Date serialization --- .../storage/SecureCredentialsManager.java | 3 +- .../internal/CredentialsDeserializer.java | 15 ++++++- .../request/internal/GsonProvider.java | 4 +- .../com/auth0/android/result/Credentials.java | 1 + .../storage/SecureCredentialsManagerTest.java | 10 ++--- .../internal/CredentialsDeserializerMock.java | 19 +++++++++ .../internal/CredentialsDeserializerTest.java | 40 ++++++++++++------ .../request/internal/CredentialsGsonTest.java | 41 +++++++++++++++++++ .../request/internal/UserProfileGsonTest.java | 2 +- .../auth0/android/result/CredentialsMock.java | 9 +++- .../resources/credentials_expires_at.json | 6 +++ 11 files changed, 125 insertions(+), 25 deletions(-) create mode 100644 auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerMock.java create mode 100755 auth0/src/test/resources/credentials_expires_at.json 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 483b80678..3d8376ed4 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 @@ -16,6 +16,7 @@ import com.auth0.android.authentication.AuthenticationException; import com.auth0.android.callback.AuthenticationCallback; import com.auth0.android.callback.BaseCallback; +import com.auth0.android.request.internal.GsonProvider; import com.auth0.android.result.Credentials; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -58,7 +59,7 @@ public class SecureCredentialsManager { this.apiClient = apiClient; this.storage = storage; this.crypto = crypto; - this.gson = new Gson(); + this.gson = GsonProvider.buildGson(); this.authenticateBeforeDecrypt = false; } diff --git a/auth0/src/main/java/com/auth0/android/request/internal/CredentialsDeserializer.java b/auth0/src/main/java/com/auth0/android/request/internal/CredentialsDeserializer.java index 076560ff1..40f4405ce 100755 --- a/auth0/src/main/java/com/auth0/android/request/internal/CredentialsDeserializer.java +++ b/auth0/src/main/java/com/auth0/android/request/internal/CredentialsDeserializer.java @@ -27,12 +27,23 @@ public Credentials deserialize(JsonElement json, Type typeOfT, JsonDeserializati final String refreshToken = context.deserialize(object.remove("refresh_token"), String.class); final Long expiresIn = context.deserialize(object.remove("expires_in"), Long.class); final String scope = context.deserialize(object.remove("scope"), String.class); - final Date expiresAt = expiresIn == null ? null : new Date(getCurrentTimeInMillis() + expiresIn * 1000); - return new Credentials(idToken, accessToken, type, refreshToken, expiresAt, scope); + Date expiresAt = context.deserialize(object.remove("expires_at"), Date.class); + if (expiresAt == null && expiresIn != null) { + expiresAt = new Date(getCurrentTimeInMillis() + expiresIn * 1000); + } + + return createCredentials(idToken, accessToken, type, refreshToken, expiresAt, scope); } @VisibleForTesting long getCurrentTimeInMillis() { return System.currentTimeMillis(); } + + @VisibleForTesting + Credentials createCredentials(String idToken, String accessToken, String type, String refreshToken, Date expiresAt, String scope) { + return new Credentials(idToken, accessToken, type, refreshToken, expiresAt, scope); + } } + + diff --git a/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.java b/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.java index 6d0dec76b..1cd9b9703 100755 --- a/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.java +++ b/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.java @@ -8,12 +8,14 @@ public abstract class GsonProvider { + static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + public static Gson buildGson() { return new GsonBuilder() .registerTypeAdapterFactory(new JsonRequiredTypeAdapterFactory()) .registerTypeAdapter(UserProfile.class, new UserProfileDeserializer()) .registerTypeAdapter(Credentials.class, new CredentialsDeserializer()) - .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .setDateFormat(DATE_FORMAT) .create(); } } diff --git a/auth0/src/main/java/com/auth0/android/result/Credentials.java b/auth0/src/main/java/com/auth0/android/result/Credentials.java index 19152aa9e..c6edc63a5 100755 --- a/auth0/src/main/java/com/auth0/android/result/Credentials.java +++ b/auth0/src/main/java/com/auth0/android/result/Credentials.java @@ -64,6 +64,7 @@ public class Credentials { @SerializedName("scope") private String scope; + @SerializedName("expires_at") private Date expiresAt; //TODO: Deprecate this constructor 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 d744da84d..484b7f92c 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 @@ -13,6 +13,7 @@ import com.auth0.android.authentication.AuthenticationException; import com.auth0.android.callback.BaseCallback; import com.auth0.android.request.ParameterizableRequest; +import com.auth0.android.request.internal.GsonProvider; import com.auth0.android.result.Credentials; import com.auth0.android.result.CredentialsMock; import com.google.gson.Gson; @@ -93,7 +94,7 @@ public void setUp() throws Exception { SecureCredentialsManager secureCredentialsManager = new SecureCredentialsManager(client, storage, crypto); manager = spy(secureCredentialsManager); doReturn(CredentialsMock.CURRENT_TIME_MS).when(manager).getCurrentTimeInMillis(); - gson = new Gson(); + gson = GsonProvider.buildGson(); } @Test @@ -392,8 +393,7 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentials() throws Exception { assertThat(renewedStoredCredentials.getRefreshToken(), is("refreshToken")); assertThat(renewedStoredCredentials.getType(), is("newType")); assertThat(renewedStoredCredentials.getExpiresAt(), is(notNullValue())); - //Gson serializes to String dates and strips a few millis. Nothing critical.. - assertThat(renewedStoredCredentials.getExpiresAt().toString(), is(newDate.toString())); + assertThat(renewedStoredCredentials.getExpiresAt().getTime(), is(newDate.getTime())); assertThat(renewedStoredCredentials.getScope(), is("newScope")); } @@ -581,7 +581,7 @@ public void shouldGetCredentialsAfterAuthentication() throws Exception { when(kService.isKeyguardSecure()).thenReturn(true); Intent confirmCredentialsIntent = mock(Intent.class); when(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")).thenReturn(confirmCredentialsIntent); - boolean willRequireAuthentication = manager.requireAuthentication(activity, 123, "theTitle","theDescription"); + boolean willRequireAuthentication = manager.requireAuthentication(activity, 123, "theTitle", "theDescription"); assertThat(willRequireAuthentication, is(true)); manager.getCredentials(callback); @@ -625,7 +625,7 @@ public void shouldNotGetCredentialsAfterCanceledAuthentication() throws Exceptio when(kService.isKeyguardSecure()).thenReturn(true); Intent confirmCredentialsIntent = mock(Intent.class); when(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")).thenReturn(confirmCredentialsIntent); - boolean willRequireAuthentication = manager.requireAuthentication(activity, 123, "theTitle","theDescription"); + boolean willRequireAuthentication = manager.requireAuthentication(activity, 123, "theTitle", "theDescription"); assertThat(willRequireAuthentication, is(true)); manager.getCredentials(callback); diff --git a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerMock.java b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerMock.java new file mode 100644 index 000000000..48d5389fc --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerMock.java @@ -0,0 +1,19 @@ +package com.auth0.android.request.internal; + +import com.auth0.android.result.Credentials; +import com.auth0.android.result.CredentialsMock; + +import java.util.Date; + +class CredentialsDeserializerMock extends CredentialsDeserializer { + + @Override + long getCurrentTimeInMillis() { + return CredentialsMock.CURRENT_TIME_MS; + } + + @Override + Credentials createCredentials(String idToken, String accessToken, String type, String refreshToken, Date expiresAt, String scope) { + return new CredentialsMock(idToken, accessToken, type, refreshToken, expiresAt, scope); + } +} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerTest.java b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerTest.java index 84e029c98..df5569658 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerTest.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerTest.java @@ -5,6 +5,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import org.hamcrest.Matchers; +import org.junit.Before; import org.junit.Test; import java.io.FileReader; @@ -12,27 +14,39 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; public class CredentialsDeserializerTest { - private static final String OPEN_ID_OFFLINE_ACCESS_CREDENTIALS = "src/test/resources/credentials_openid_refresh_token.json"; + private static final String BASIC_CREDENTIALS = "src/test/resources/credentials.json"; + private static final String EXPIRES_AT_CREDENTIALS = "src/test/resources/credentials_expires_at.json"; - private static final long CURRENT_TIME_MS = 1234567890000L; + private Gson gson; + + @Before + public void setUp() throws Exception { + final CredentialsDeserializerMock deserializer = new CredentialsDeserializerMock(); + gson = new GsonBuilder() + .setDateFormat(GsonProvider.DATE_FORMAT) + .registerTypeAdapter(Credentials.class, deserializer) + .create(); + } @Test public void shouldSetExpiresAtFromExpiresIn() throws Exception { - final CredentialsDeserializer deserializer = new CredentialsDeserializer(); - final CredentialsDeserializer spy = spy(deserializer); - doReturn(CredentialsMock.CURRENT_TIME_MS).when(spy).getCurrentTimeInMillis(); - - final Gson gson = new GsonBuilder() - .registerTypeAdapter(Credentials.class, spy) - .create(); + final Credentials credentials = gson.getAdapter(Credentials.class).fromJson(new FileReader(BASIC_CREDENTIALS)); + assertThat(credentials.getExpiresIn(), is(86000L)); + assertThat(credentials.getExpiresAt(), is(notNullValue())); + assertThat(credentials.getExpiresAt().getTime(), is(CredentialsMock.CURRENT_TIME_MS + 86000 * 1000)); + } - final Credentials credentials = gson.getAdapter(Credentials.class).fromJson(new FileReader(OPEN_ID_OFFLINE_ACCESS_CREDENTIALS)); + @Test + public void shouldSetExpiresInFromExpiresAt() throws Exception { + final Credentials credentials = gson.getAdapter(Credentials.class).fromJson(new FileReader(EXPIRES_AT_CREDENTIALS)); assertThat(credentials.getExpiresAt(), is(notNullValue())); - assertThat(credentials.getExpiresAt().getTime(), is(CURRENT_TIME_MS + 86000 * 1000)); + //The hardcoded value comes from the JSON file + assertThat(credentials.getExpiresAt().getTime(), is(1234691346555L)); + assertThat(credentials.getExpiresIn(), is(notNullValue())); + assertThat(credentials.getExpiresIn(), Matchers.is((1234691346555L - CredentialsMock.CURRENT_TIME_MS) / 1000)); } + } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.java b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.java index 63bd346a1..da0169fe0 100755 --- a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.java @@ -1,6 +1,7 @@ package com.auth0.android.request.internal; import com.auth0.android.result.Credentials; +import com.auth0.android.result.CredentialsMock; import com.google.gson.JsonParseException; import org.junit.Before; @@ -11,7 +12,10 @@ import java.io.IOException; import java.io.Reader; import java.io.StringReader; +import java.util.Date; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -22,6 +26,7 @@ public class CredentialsGsonTest extends GsonBaseTest { private static final String OPEN_ID_OFFLINE_ACCESS_CREDENTIALS = "src/test/resources/credentials_openid_refresh_token.json"; private static final String OPEN_ID_CREDENTIALS = "src/test/resources/credentials_openid.json"; private static final String BASIC_CREDENTIALS = "src/test/resources/credentials.json"; + private static final String EXPIRES_AT_CREDENTIALS = "src/test/resources/credentials_expires_at.json"; @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -66,6 +71,18 @@ public void shouldReturnBasic() throws Exception { assertThat(credentials.getScope(), is(nullValue())); } + @Test + public void shouldReturnWithExpiresAt() throws Exception { + final Credentials credentials = buildCredentialsFrom(json(EXPIRES_AT_CREDENTIALS)); + assertThat(credentials, is(notNullValue())); + assertThat(credentials.getAccessToken(), is(notNullValue())); + assertThat(credentials.getType(), equalTo("bearer")); + assertThat(credentials.getExpiresIn(), is(not(86000L))); + assertThat(credentials.getExpiresAt(), is(notNullValue())); + //The hardcoded value comes from the JSON file + assertThat(credentials.getExpiresAt().getTime(), is(1234691346555L)); + } + @Test public void shouldReturnWithIdToken() throws Exception { final Credentials credentials = buildCredentialsFrom(json(OPEN_ID_CREDENTIALS)); @@ -92,6 +109,30 @@ public void shouldReturnWithRefreshToken() throws Exception { assertThat(credentials.getScope(), is("openid profile")); } + @Test + public void shouldSerializeCredentials() throws Exception { + final Credentials expiresInCredentials = new CredentialsMock("id", "access", "ty", "refresh", 123456L); + final String expiresInJson = gson.toJson(expiresInCredentials); + assertThat(expiresInJson, containsString("\"id_token\":\"id\"")); + assertThat(expiresInJson, containsString("\"access_token\":\"access\"")); + assertThat(expiresInJson, containsString("\"token_type\":\"ty\"")); + assertThat(expiresInJson, containsString("\"refresh_token\":\"refresh\"")); + assertThat(expiresInJson, containsString("\"expires_in\":123456")); + assertThat(expiresInJson, containsString("\"expires_at\":\"2009-02-15T07:49:07.234Z\"")); + assertThat(expiresInJson, not(containsString("\"scope\""))); + + + final Credentials expiresAtCredentials = new CredentialsMock("id", "access", "ty", "refresh", new Date(CredentialsMock.CURRENT_TIME_MS + 123456 * 1000), "openid"); + final String expiresAtJson = gson.toJson(expiresAtCredentials); + assertThat(expiresAtJson, containsString("\"id_token\":\"id\"")); + assertThat(expiresAtJson, containsString("\"access_token\":\"access\"")); + assertThat(expiresAtJson, containsString("\"token_type\":\"ty\"")); + assertThat(expiresAtJson, containsString("\"refresh_token\":\"refresh\"")); + assertThat(expiresAtJson, containsString("\"expires_in\":123456")); + assertThat(expiresAtJson, containsString("\"expires_at\":\"2009-02-15T07:49:07.234Z\"")); + assertThat(expiresAtJson, containsString("\"scope\":\"openid\"")); + } + private Credentials buildCredentialsFrom(Reader json) throws IOException { return pojoFrom(json, Credentials.class); } diff --git a/auth0/src/test/java/com/auth0/android/request/internal/UserProfileGsonTest.java b/auth0/src/test/java/com/auth0/android/request/internal/UserProfileGsonTest.java index 8d00f6389..ee984ac4b 100755 --- a/auth0/src/test/java/com/auth0/android/request/internal/UserProfileGsonTest.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/UserProfileGsonTest.java @@ -182,7 +182,7 @@ public void shouldReturnProfileWithOptionalFields() throws Exception { assertThat(profile.getGivenName(), equalTo("John")); assertThat(profile.getFamilyName(), equalTo("Foobar")); assertThat(profile.isEmailVerified(), is(false)); - assertThat(profile.getCreatedAt(), equalTo(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").parse("2014-07-06T18:33:49.005Z"))); + assertThat(profile.getCreatedAt(), equalTo(new SimpleDateFormat(GsonProvider.DATE_FORMAT).parse("2014-07-06T18:33:49.005Z"))); } @Test 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 0270c406b..58b5e66f9 100644 --- a/auth0/src/test/java/com/auth0/android/result/CredentialsMock.java +++ b/auth0/src/test/java/com/auth0/android/result/CredentialsMock.java @@ -2,12 +2,14 @@ import android.support.annotation.Nullable; +import java.util.Calendar; import java.util.Date; +import java.util.TimeZone; @SuppressWarnings("WeakerAccess") public class CredentialsMock extends Credentials { - public static final long CURRENT_TIME_MS = 1234567890000L; + public static final long CURRENT_TIME_MS = 1234567891234L; public CredentialsMock(@Nullable String idToken, @Nullable String accessToken, @Nullable String type, @Nullable String refreshToken, @Nullable Long expiresIn) { super(idToken, accessToken, type, refreshToken, expiresIn); @@ -19,6 +21,9 @@ public CredentialsMock(@Nullable String idToken, @Nullable String accessToken, @ @Override long getCurrentTimeInMillis() { - return CURRENT_TIME_MS; + Calendar cal = Calendar.getInstance(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + cal.setTimeInMillis(CURRENT_TIME_MS); + return cal.getTimeInMillis(); } } \ No newline at end of file diff --git a/auth0/src/test/resources/credentials_expires_at.json b/auth0/src/test/resources/credentials_expires_at.json new file mode 100755 index 000000000..0ea4864a7 --- /dev/null +++ b/auth0/src/test/resources/credentials_expires_at.json @@ -0,0 +1,6 @@ +{ + "access_token": "s6GS5FGJN2jfd4l6", + "token_type": "bearer", + "expires_in": 86000, + "expires_at": "2009-02-15T07:49:06.555Z" +} \ No newline at end of file From 9d3d32b265a3204b444bcf09d6f397d95fa8b3a2 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Mon, 2 Oct 2017 14:16:28 -0300 Subject: [PATCH 3/4] second attempt to fix Date serialization --- .../request/internal/GsonProvider.java | 12 ++++++++ .../internal/CredentialsDeserializerTest.java | 24 +++++++++++++--- .../request/internal/CredentialsGsonTest.java | 28 +++++++++++++++---- .../request/internal/UserProfileGsonTest.java | 3 +- .../auth0/android/result/CredentialsMock.java | 7 +++-- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.java b/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.java index 1cd9b9703..c58d838a5 100755 --- a/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.java +++ b/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.java @@ -1,11 +1,17 @@ package com.auth0.android.request.internal; +import android.support.annotation.VisibleForTesting; + import com.auth0.android.result.Credentials; import com.auth0.android.result.UserProfile; import com.auth0.android.util.JsonRequiredTypeAdapterFactory; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + public abstract class GsonProvider { static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; @@ -18,4 +24,10 @@ public static Gson buildGson() { .setDateFormat(DATE_FORMAT) .create(); } + + @VisibleForTesting + static String formatDate(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT, Locale.US); + return sdf.format(date); + } } diff --git a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerTest.java b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerTest.java index df5569658..d95ee203a 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerTest.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerTest.java @@ -1,5 +1,7 @@ package com.auth0.android.request.internal; +import android.support.annotation.NonNull; + import com.auth0.android.result.Credentials; import com.auth0.android.result.CredentialsMock; import com.google.gson.Gson; @@ -10,6 +12,8 @@ import org.junit.Test; import java.io.FileReader; +import java.util.Calendar; +import java.util.Date; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.core.Is.is; @@ -18,7 +22,6 @@ public class CredentialsDeserializerTest { private static final String BASIC_CREDENTIALS = "src/test/resources/credentials.json"; - private static final String EXPIRES_AT_CREDENTIALS = "src/test/resources/credentials_expires_at.json"; private Gson gson; @@ -41,12 +44,25 @@ public void shouldSetExpiresAtFromExpiresIn() throws Exception { @Test public void shouldSetExpiresInFromExpiresAt() throws Exception { - final Credentials credentials = gson.getAdapter(Credentials.class).fromJson(new FileReader(EXPIRES_AT_CREDENTIALS)); + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_YEAR, 7); + Date expiresAt = cal.getTime(); + final Credentials credentials = gson.getAdapter(Credentials.class).fromJson(generateExpiresAtCredentialsJSON(expiresAt)); assertThat(credentials.getExpiresAt(), is(notNullValue())); //The hardcoded value comes from the JSON file - assertThat(credentials.getExpiresAt().getTime(), is(1234691346555L)); + assertThat(credentials.getExpiresAt().getTime(), is(expiresAt.getTime())); assertThat(credentials.getExpiresIn(), is(notNullValue())); - assertThat(credentials.getExpiresIn(), Matchers.is((1234691346555L - CredentialsMock.CURRENT_TIME_MS) / 1000)); + assertThat(credentials.getExpiresIn(), Matchers.is((expiresAt.getTime() - CredentialsMock.CURRENT_TIME_MS) / 1000)); + } + + + private String generateExpiresAtCredentialsJSON(@NonNull Date expiresAt) { + return "{\n" + + "\"access_token\": \"s6GS5FGJN2jfd4l6\",\n" + + "\"token_type\": \"bearer\",\n" + + "\"expires_in\": 86000,\n" + + "\"expires_at\": \"" + GsonProvider.formatDate(expiresAt) + "\"\n" + + "}"; } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.java b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.java index da0169fe0..e3d44a6ec 100755 --- a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.java @@ -1,5 +1,7 @@ package com.auth0.android.request.internal; +import android.support.annotation.NonNull; + import com.auth0.android.result.Credentials; import com.auth0.android.result.CredentialsMock; import com.google.gson.JsonParseException; @@ -12,6 +14,7 @@ import java.io.IOException; import java.io.Reader; import java.io.StringReader; +import java.util.Calendar; import java.util.Date; import static org.hamcrest.CoreMatchers.containsString; @@ -26,7 +29,6 @@ public class CredentialsGsonTest extends GsonBaseTest { private static final String OPEN_ID_OFFLINE_ACCESS_CREDENTIALS = "src/test/resources/credentials_openid_refresh_token.json"; private static final String OPEN_ID_CREDENTIALS = "src/test/resources/credentials_openid.json"; private static final String BASIC_CREDENTIALS = "src/test/resources/credentials.json"; - private static final String EXPIRES_AT_CREDENTIALS = "src/test/resources/credentials_expires_at.json"; @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -73,14 +75,17 @@ public void shouldReturnBasic() throws Exception { @Test public void shouldReturnWithExpiresAt() throws Exception { - final Credentials credentials = buildCredentialsFrom(json(EXPIRES_AT_CREDENTIALS)); + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_YEAR, 7); + Date expiresAt = cal.getTime(); + final Credentials credentials = buildCredentialsFrom(new StringReader(generateExpiresAtCredentialsJSON(expiresAt))); assertThat(credentials, is(notNullValue())); assertThat(credentials.getAccessToken(), is(notNullValue())); assertThat(credentials.getType(), equalTo("bearer")); assertThat(credentials.getExpiresIn(), is(not(86000L))); assertThat(credentials.getExpiresAt(), is(notNullValue())); //The hardcoded value comes from the JSON file - assertThat(credentials.getExpiresAt().getTime(), is(1234691346555L)); + assertThat(credentials.getExpiresAt().getTime(), is(expiresAt.getTime())); } @Test @@ -111,6 +116,9 @@ public void shouldReturnWithRefreshToken() throws Exception { @Test public void shouldSerializeCredentials() throws Exception { + Date expiresAt = new Date(CredentialsMock.CURRENT_TIME_MS + 123456 * 1000); + final String expectedExpiresAt = GsonProvider.formatDate(expiresAt); + final Credentials expiresInCredentials = new CredentialsMock("id", "access", "ty", "refresh", 123456L); final String expiresInJson = gson.toJson(expiresInCredentials); assertThat(expiresInJson, containsString("\"id_token\":\"id\"")); @@ -118,18 +126,18 @@ public void shouldSerializeCredentials() throws Exception { assertThat(expiresInJson, containsString("\"token_type\":\"ty\"")); assertThat(expiresInJson, containsString("\"refresh_token\":\"refresh\"")); assertThat(expiresInJson, containsString("\"expires_in\":123456")); - assertThat(expiresInJson, containsString("\"expires_at\":\"2009-02-15T07:49:07.234Z\"")); + assertThat(expiresInJson, containsString("\"expires_at\":\"" + expectedExpiresAt + "\"")); assertThat(expiresInJson, not(containsString("\"scope\""))); - final Credentials expiresAtCredentials = new CredentialsMock("id", "access", "ty", "refresh", new Date(CredentialsMock.CURRENT_TIME_MS + 123456 * 1000), "openid"); + final Credentials expiresAtCredentials = new CredentialsMock("id", "access", "ty", "refresh", expiresAt, "openid"); final String expiresAtJson = gson.toJson(expiresAtCredentials); assertThat(expiresAtJson, containsString("\"id_token\":\"id\"")); assertThat(expiresAtJson, containsString("\"access_token\":\"access\"")); assertThat(expiresAtJson, containsString("\"token_type\":\"ty\"")); assertThat(expiresAtJson, containsString("\"refresh_token\":\"refresh\"")); assertThat(expiresAtJson, containsString("\"expires_in\":123456")); - assertThat(expiresAtJson, containsString("\"expires_at\":\"2009-02-15T07:49:07.234Z\"")); + assertThat(expiresInJson, containsString("\"expires_at\":\"" + expectedExpiresAt + "\"")); assertThat(expiresAtJson, containsString("\"scope\":\"openid\"")); } @@ -137,4 +145,12 @@ private Credentials buildCredentialsFrom(Reader json) throws IOException { return pojoFrom(json, Credentials.class); } + private String generateExpiresAtCredentialsJSON(@NonNull Date expiresAt) { + return "{\n" + + "\"access_token\": \"s6GS5FGJN2jfd4l6\",\n" + + "\"token_type\": \"bearer\",\n" + + "\"expires_in\": 86000,\n" + + "\"expires_at\": \"" + GsonProvider.formatDate(expiresAt) + "\"\n" + + "}"; + } } diff --git a/auth0/src/test/java/com/auth0/android/request/internal/UserProfileGsonTest.java b/auth0/src/test/java/com/auth0/android/request/internal/UserProfileGsonTest.java index ee984ac4b..db4126570 100755 --- a/auth0/src/test/java/com/auth0/android/request/internal/UserProfileGsonTest.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/UserProfileGsonTest.java @@ -12,6 +12,7 @@ import java.io.StringReader; import java.text.SimpleDateFormat; import java.util.Collections; +import java.util.Locale; import static com.auth0.android.util.UserIdentityMatcher.isUserIdentity; import static com.auth0.android.util.UserProfileMatcher.isNormalizedProfile; @@ -182,7 +183,7 @@ public void shouldReturnProfileWithOptionalFields() throws Exception { assertThat(profile.getGivenName(), equalTo("John")); assertThat(profile.getFamilyName(), equalTo("Foobar")); assertThat(profile.isEmailVerified(), is(false)); - assertThat(profile.getCreatedAt(), equalTo(new SimpleDateFormat(GsonProvider.DATE_FORMAT).parse("2014-07-06T18:33:49.005Z"))); + assertThat(profile.getCreatedAt(), equalTo(new SimpleDateFormat(GsonProvider.DATE_FORMAT, Locale.US).parse("2014-07-06T18:33:49.005Z"))); } @Test 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 58b5e66f9..416714487 100644 --- a/auth0/src/test/java/com/auth0/android/result/CredentialsMock.java +++ b/auth0/src/test/java/com/auth0/android/result/CredentialsMock.java @@ -9,7 +9,7 @@ @SuppressWarnings("WeakerAccess") public class CredentialsMock extends Credentials { - public static final long CURRENT_TIME_MS = 1234567891234L; + public static final long CURRENT_TIME_MS = calculateCurrentTime(); public CredentialsMock(@Nullable String idToken, @Nullable String accessToken, @Nullable String type, @Nullable String refreshToken, @Nullable Long expiresIn) { super(idToken, accessToken, type, refreshToken, expiresIn); @@ -21,9 +21,12 @@ public CredentialsMock(@Nullable String idToken, @Nullable String accessToken, @ @Override long getCurrentTimeInMillis() { + return CURRENT_TIME_MS; + } + + private static long calculateCurrentTime() { Calendar cal = Calendar.getInstance(); cal.setTimeZone(TimeZone.getTimeZone("UTC")); - cal.setTimeInMillis(CURRENT_TIME_MS); return cal.getTimeInMillis(); } } \ No newline at end of file From 348ba26742836fc7d47cb2c868faaa5bed1c3dc0 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Mon, 2 Oct 2017 16:59:28 -0300 Subject: [PATCH 4/4] chore martin comments. --- README.md | 6 +++--- .../authentication/storage/SecureCredentialsManager.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 20717892d..0fcb1d5df 100644 --- a/README.md +++ b/README.md @@ -441,11 +441,11 @@ This library ships with two additional classes that help you manage the Credenti ### Basic (Min API 15) -The basic version supports asking for `Credentials` existence, storing them and getting them back. If the credentials have expired and a refresh_token was saved, they are automatically refreshed. The class is called `CredentialsManager`. +The basic version supports asking for `Credentials` existence, storing them and getting them back. If the credentials have expired and a refresh_token was saved, they are automatically refreshed. The class is called `CredentialsManager` and requires at minimum Android API 15. #### Usage 1. **Instantiate the manager:** -You'll need an `AuthenticationAPIClient` instance to renew the credentials when they expire and a `Storage`. We provide a `SharedPreferencesStorage` that uses `SharedPreferences` to create a file in the application's directory with **Context.MODE_PRIVATE** mode. This implementation is thread safe and can either be obtained through a shared method or on demand. +You'll need an `AuthenticationAPIClient` instance to renew the credentials when they expire and a `Storage` object. We provide a `SharedPreferencesStorage` class that makes use of `SharedPreferences` to create a file in the application's directory with **Context.MODE_PRIVATE** mode. This implementation is thread safe and can either be obtained through a shared method or on demand. ```java AuthenticationAPIClient authentication = new AuthenticationAPIClient(account); @@ -507,7 +507,7 @@ manager.clearCredentials(); ### Encryption enforced (Min API 21) -This version expands the minimum version and adds encryption to the data storage. In those devices where a Secure LockScreen has been configured it can require the user authentication before letting them obtain the stored credentials. The class is called `SecureCredentialsManager`. +This version expands the minimum version and adds encryption to the data storage. In those devices where a Secure LockScreen has been configured it can require the user authentication before letting them obtain the stored credentials. The class is called `SecureCredentialsManager` and requires at minimum Android API 21. #### Usage 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 3d8376ed4..cce12a77a 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,7 +19,6 @@ import com.auth0.android.request.internal.GsonProvider; import com.auth0.android.result.Credentials; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import static android.text.TextUtils.isEmpty; @@ -94,8 +93,9 @@ public boolean requireAuthentication(@NonNull Activity activity, int requestCode } KeyguardManager kManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); this.authIntent = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? kManager.createConfirmDeviceCredentialIntent(title, description) : null; - this.authenticateBeforeDecrypt = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && kManager.isDeviceSecure() - || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && kManager.isKeyguardSecure()) && authIntent != null; + this.authenticateBeforeDecrypt = ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && kManager.isDeviceSecure()) + || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && kManager.isKeyguardSecure())) + && authIntent != null; if (authenticateBeforeDecrypt) { this.activity = activity; this.authenticationRequestCode = requestCode;