Skip to content

Commit

Permalink
Merge pull request #118 from auth0/save-refreshed-credentials
Browse files Browse the repository at this point in the history
Make Credential Managers save the refreshed value
  • Loading branch information
lbalmaceda authored Oct 11, 2017
2 parents dac05f1 + 348ba26 commit d7f97b3
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 37 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public void saveCredentials(@NonNull Credentials credentials) {
*/
public void getCredentials(@NonNull final BaseCallback<Credentials, CredentialsManagerException> 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);
Expand All @@ -86,8 +86,11 @@ public void getCredentials(@NonNull final BaseCallback<Credentials, CredentialsM

authClient.renewAuth(refreshToken).start(new AuthenticationCallback<Credentials>() {
@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -206,7 +208,7 @@ private void continueGetCredentials(final BaseCallback<Credentials, CredentialsM
callback.onFailure(new CredentialsManagerException("An error occurred while decrypting the existing credentials.", e));
return;
}
Credentials credentials = gson.fromJson(json, Credentials.class);
final Credentials credentials = gson.fromJson(json, Credentials.class);
if (isEmpty(credentials.getAccessToken()) && isEmpty(credentials.getIdToken()) || credentials.getExpiresAt() == null) {
callback.onFailure(new CredentialsManagerException("No Credentials were previously set."));
decryptCallback = null;
Expand All @@ -226,8 +228,11 @@ private void continueGetCredentials(final BaseCallback<Credentials, CredentialsM
Log.d(TAG, "Credentials have expired. Renewing them now...");
apiClient.renewAuth(credentials.getRefreshToken()).start(new AuthenticationCallback<Credentials>() {
@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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}


Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class Credentials {
@SerializedName("scope")
private String scope;

@SerializedName("expires_at")
private Date expiresAt;

//TODO: Deprecate this constructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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()));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit d7f97b3

Please sign in to comment.