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/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..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 @@ -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; @@ -57,7 +58,7 @@ public class SecureCredentialsManager { this.apiClient = apiClient; this.storage = storage; this.crypto = crypto; - this.gson = new Gson(); + this.gson = GsonProvider.buildGson(); this.authenticateBeforeDecrypt = false; } @@ -92,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; @@ -206,7 +208,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/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..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,19 +1,33 @@ 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'"; + 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(); } + + @VisibleForTesting + static String formatDate(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT, Locale.US); + return sdf.format(date); + } } 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/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..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; @@ -40,6 +41,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; @@ -89,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 @@ -356,13 +361,40 @@ 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())); + assertThat(renewedStoredCredentials.getExpiresAt().getTime(), is(newDate.getTime())); + 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())); @@ -544,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); @@ -588,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..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,38 +1,68 @@ 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; import com.google.gson.GsonBuilder; +import org.hamcrest.Matchers; +import org.junit.Before; 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; 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 Gson gson; - private static final long CURRENT_TIME_MS = 1234567890000L; + @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 { + 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())); - assertThat(credentials.getExpiresAt().getTime(), is(CURRENT_TIME_MS + 86000 * 1000)); + //The hardcoded value comes from the JSON file + assertThat(credentials.getExpiresAt().getTime(), is(expiresAt.getTime())); + assertThat(credentials.getExpiresIn(), is(notNullValue())); + 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 63bd346a1..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,6 +1,9 @@ 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; import org.junit.Before; @@ -11,7 +14,11 @@ 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; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -66,6 +73,21 @@ public void shouldReturnBasic() throws Exception { assertThat(credentials.getScope(), is(nullValue())); } + @Test + public void shouldReturnWithExpiresAt() throws Exception { + 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(expiresAt.getTime())); + } + @Test public void shouldReturnWithIdToken() throws Exception { final Credentials credentials = buildCredentialsFrom(json(OPEN_ID_CREDENTIALS)); @@ -92,8 +114,43 @@ public void shouldReturnWithRefreshToken() throws Exception { assertThat(credentials.getScope(), is("openid profile")); } + @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\"")); + 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\":\"" + expectedExpiresAt + "\"")); + assertThat(expiresInJson, not(containsString("\"scope\""))); + + + 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(expiresInJson, containsString("\"expires_at\":\"" + expectedExpiresAt + "\"")); + assertThat(expiresAtJson, containsString("\"scope\":\"openid\"")); + } + 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 8d00f6389..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("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, 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 0270c406b..416714487 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 = 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,4 +23,10 @@ public CredentialsMock(@Nullable String idToken, @Nullable String accessToken, @ long getCurrentTimeInMillis() { return CURRENT_TIME_MS; } + + private static long calculateCurrentTime() { + Calendar cal = Calendar.getInstance(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + 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