Skip to content

Commit

Permalink
allow to pass minTTL and scope to the CredentialsManager
Browse files Browse the repository at this point in the history
  • Loading branch information
lbalmaceda committed Oct 30, 2020
1 parent 8d54f58 commit 94304ff
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import com.auth0.android.callback.AuthenticationCallback;
import com.auth0.android.callback.BaseCallback;
import com.auth0.android.jwt.JWT;
import com.auth0.android.request.ParameterizableRequest;
import com.auth0.android.result.Credentials;
import com.auth0.android.util.Clock;

import java.util.Arrays;
import java.util.Date;

import static android.text.TextUtils.isEmpty;
Expand Down Expand Up @@ -73,16 +75,7 @@ public void saveCredentials(@NonNull Credentials credentials) {
throw new CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value.");
}

long expiresAt = credentials.getExpiresAt().getTime();

if (credentials.getIdToken() != null) {
JWT idToken = jwtDecoder.decode(credentials.getIdToken());
Date idTokenExpiresAtDate = idToken.getExpiresAt();

if (idTokenExpiresAtDate != null) {
expiresAt = Math.min(idTokenExpiresAtDate.getTime(), expiresAt);
}
}
long expiresAt = calculateExpiresAt(credentials);

storage.store(KEY_ACCESS_TOKEN, credentials.getAccessToken());
storage.store(KEY_REFRESH_TOKEN, credentials.getRefreshToken());
Expand All @@ -100,34 +93,65 @@ public void saveCredentials(@NonNull Credentials credentials) {
*
* @param callback the callback that will receive a valid {@link Credentials} or the {@link CredentialsManagerException}.
*/
public void getCredentials(@NonNull final BaseCallback<Credentials, CredentialsManagerException> callback) {
public void getCredentials(@NonNull BaseCallback<Credentials, CredentialsManagerException> callback) {
getCredentials(null, 0, callback);
}

/**
* Retrieves the credentials from the storage and refresh them if they have already expired.
* It will fail with {@link CredentialsManagerException} if the saved access_token or id_token is null,
* or if the tokens have already expired and the refresh_token is null.
*
* @param scope the scope to request for the access token. If null is passed, the previous scope will be kept.
* @param minTtl the minimum time in seconds that both the access token and id token should last before expiration.
* @param callback the callback that will receive a valid {@link Credentials} or the {@link CredentialsManagerException}.
*/
public void getCredentials(@Nullable String scope, final int minTtl, @NonNull final BaseCallback<Credentials, CredentialsManagerException> callback) {
String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN);
final String refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN);
String idToken = storage.retrieveString(KEY_ID_TOKEN);
String tokenType = storage.retrieveString(KEY_TOKEN_TYPE);
Long expiresAt = storage.retrieveLong(KEY_EXPIRES_AT);
String scope = storage.retrieveString(KEY_SCOPE);
String storedScope = storage.retrieveString(KEY_SCOPE);
Long cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT);
if (cacheExpiresAt == null) {
cacheExpiresAt = expiresAt;
}

if (isEmpty(accessToken) && isEmpty(idToken) || expiresAt == null) {
boolean hasEmptyCredentials = isEmpty(accessToken) && isEmpty(idToken) || expiresAt == null;
if (hasEmptyCredentials) {
callback.onFailure(new CredentialsManagerException("No Credentials were previously set."));
return;
}
if (cacheExpiresAt > getCurrentTimeInMillis()) {
callback.onSuccess(recreateCredentials(idToken, accessToken, tokenType, refreshToken, new Date(expiresAt), scope));

boolean willExpire = willExpire(cacheExpiresAt, minTtl);
boolean scopeChanged = hasScopeChanged(storedScope, scope);

if (!willExpire && !scopeChanged) {
callback.onSuccess(recreateCredentials(idToken, accessToken, tokenType, refreshToken, new Date(expiresAt), storedScope));
return;
}
if (refreshToken == null) {
callback.onFailure(new CredentialsManagerException("Credentials have expired and no Refresh Token was available to renew them."));
return;
}

authClient.renewAuth(refreshToken).start(new AuthenticationCallback<Credentials>() {
final ParameterizableRequest<Credentials, AuthenticationException> request = authClient.renewAuth(refreshToken);
if (scope != null) {
request.addParameter("scope", scope);
}
request.start(new AuthenticationCallback<Credentials>() {
@Override
public void onSuccess(@Nullable Credentials fresh) {
long nextCacheExpiresAt = calculateExpiresAt(fresh);
boolean willExpire = willExpire(nextCacheExpiresAt, minTtl);
if (willExpire) {
long tokenLifetime = (nextCacheExpiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000;
CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token or Id Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl));
callback.onFailure(wrongTtlException);
return;
}

//non-empty refresh token for refresh token rotation scenarios
String updatedRefreshToken = isEmpty(fresh.getRefreshToken()) ? refreshToken : fresh.getRefreshToken();
Credentials credentials = new Credentials(fresh.getIdToken(), fresh.getAccessToken(), fresh.getType(), updatedRefreshToken, fresh.getExpiresAt(), fresh.getScope());
Expand All @@ -140,6 +164,37 @@ public void onFailure(@NonNull AuthenticationException error) {
callback.onFailure(new CredentialsManagerException("An error occurred while trying to use the Refresh Token to renew the Credentials.", error));
}
});

}

private boolean hasScopeChanged(@NonNull String storedScope, @Nullable String requiredScope) {
if (requiredScope == null) {
return false;
}
String[] stored = storedScope.split(" ");
Arrays.sort(stored);
String[] required = requiredScope.split(" ");
Arrays.sort(required);
return stored != required;
}

private boolean willExpire(long expiresAt, long minTtl) {
long nextClock = getCurrentTimeInMillis() + minTtl * 1000;
return expiresAt <= nextClock;
}

private long calculateExpiresAt(@NonNull Credentials credentials) {
long expiresAt = credentials.getExpiresAt().getTime();

if (credentials.getIdToken() != null) {
JWT idToken = jwtDecoder.decode(credentials.getIdToken());
Date idTokenExpiresAtDate = idToken.getExpiresAt();

if (idTokenExpiresAtDate != null) {
expiresAt = Math.min(idTokenExpiresAtDate.getTime(), expiresAt);
}
}
return expiresAt;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
Expand Down Expand Up @@ -344,6 +345,106 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyAccessTokenIsAvaila
assertThat(retrievedCredentials.getScope(), is("scope"));
}

@Test
public void shouldRenewCredentialsWhenScopeHasChanged() {
when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken");
when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken");
when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken");
when(storage.retrieveString("com.auth0.token_type")).thenReturn("type");
long expirationTime = CredentialsMock.CURRENT_TIME_MS + 123456L * 1000; //non expired credentials
when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime);
when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime);
when(storage.retrieveString("com.auth0.scope")).thenReturn("some new scope");
when(client.renewAuth("refreshToken")).thenReturn(request);

Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000);
JWT jwtMock = mock(JWT.class);
when(jwtMock.getExpiresAt()).thenReturn(newDate);
when(jwtDecoder.decode("newId")).thenReturn(jwtMock);

manager.getCredentials("some scope", 0, callback);
verify(request).start(requestCallbackCaptor.capture());
verify(request).addParameter(eq("scope"), eq("some scope"));

//Trigger success
String newRefresh = null;
Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope");
requestCallbackCaptor.getValue().onSuccess(renewedCredentials);
verify(callback).onSuccess(credentialsCaptor.capture());

// Verify the credentials are property stored
verify(storage).store("com.auth0.id_token", renewedCredentials.getIdToken());
verify(storage).store("com.auth0.access_token", renewedCredentials.getAccessToken());
//RefreshToken should not be replaced
verify(storage, never()).store("com.auth0.refresh_token", newRefresh);
verify(storage).store("com.auth0.refresh_token", "refreshToken");
verify(storage).store("com.auth0.token_type", renewedCredentials.getType());
verify(storage).store("com.auth0.expires_at", renewedCredentials.getExpiresAt().getTime());
verify(storage).store("com.auth0.scope", renewedCredentials.getScope());
verify(storage).store("com.auth0.cache_expires_at", renewedCredentials.getExpiresAt().getTime());
verify(storage, never()).remove(anyString());

//// Verify the returned credentials are the latest
Credentials retrievedCredentials = credentialsCaptor.getValue();
assertThat(retrievedCredentials, is(notNullValue()));
assertThat(retrievedCredentials.getIdToken(), is("newId"));
assertThat(retrievedCredentials.getAccessToken(), is("newAccess"));
assertThat(retrievedCredentials.getType(), is("newType"));
assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken"));
assertThat(retrievedCredentials.getExpiresAt(), is(newDate));
assertThat(retrievedCredentials.getScope(), is("newScope"));
}

@Test
public void shouldRenewCredentialsWithMinTtl() {
when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken");
when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken");
when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken");
when(storage.retrieveString("com.auth0.token_type")).thenReturn("type");
long expirationTime = CredentialsMock.CURRENT_TIME_MS; //Same as current time --> expired
when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime);
when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime);
when(storage.retrieveString("com.auth0.scope")).thenReturn("scope");
when(client.renewAuth("refreshToken")).thenReturn(request);

Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 61 * 1000); //new token expires in minTTL + 1 seconds
JWT jwtMock = mock(JWT.class);
when(jwtMock.getExpiresAt()).thenReturn(newDate);
when(jwtDecoder.decode("newId")).thenReturn(jwtMock);

manager.getCredentials(null, 60, callback); //60 seconds of minTTL
verify(request, never()).addParameter(eq("scope"), anyString());
verify(request).start(requestCallbackCaptor.capture());

//Trigger success
String newRefresh = null;
Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope");
requestCallbackCaptor.getValue().onSuccess(renewedCredentials);
verify(callback).onSuccess(credentialsCaptor.capture());

// Verify the credentials are property stored
verify(storage).store("com.auth0.id_token", renewedCredentials.getIdToken());
verify(storage).store("com.auth0.access_token", renewedCredentials.getAccessToken());
//RefreshToken should not be replaced
verify(storage, never()).store("com.auth0.refresh_token", newRefresh);
verify(storage).store("com.auth0.refresh_token", "refreshToken");
verify(storage).store("com.auth0.token_type", renewedCredentials.getType());
verify(storage).store("com.auth0.expires_at", renewedCredentials.getExpiresAt().getTime());
verify(storage).store("com.auth0.scope", renewedCredentials.getScope());
verify(storage).store("com.auth0.cache_expires_at", renewedCredentials.getExpiresAt().getTime());
verify(storage, never()).remove(anyString());

//// Verify the returned credentials are the latest
Credentials retrievedCredentials = credentialsCaptor.getValue();
assertThat(retrievedCredentials, is(notNullValue()));
assertThat(retrievedCredentials.getIdToken(), is("newId"));
assertThat(retrievedCredentials.getAccessToken(), is("newAccess"));
assertThat(retrievedCredentials.getType(), is("newType"));
assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken"));
assertThat(retrievedCredentials.getExpiresAt(), is(newDate));
assertThat(retrievedCredentials.getScope(), is("newScope"));
}

@Test
public void shouldGetAndSuccessfullyRenewExpiredCredentials() {
when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken");
Expand All @@ -356,12 +457,13 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentials() {
when(storage.retrieveString("com.auth0.scope")).thenReturn("scope");
when(client.renewAuth("refreshToken")).thenReturn(request);

Date newDate = new Date(123412341234L);
Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000);
JWT jwtMock = mock(JWT.class);
when(jwtMock.getExpiresAt()).thenReturn(newDate);
when(jwtDecoder.decode("newId")).thenReturn(jwtMock);

manager.getCredentials(callback);
verify(request, never()).addParameter(eq("scope"), anyString());
verify(request).start(requestCallbackCaptor.capture());

//Trigger success
Expand Down Expand Up @@ -393,6 +495,46 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentials() {
assertThat(retrievedCredentials.getScope(), is("newScope"));
}

@Test
public void shouldGetAndFailToRenewExpiredCredentialsWhenReceivedTokenHasLowerTtl() {
when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken");
when(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken");
when(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken");
when(storage.retrieveString("com.auth0.token_type")).thenReturn("type");
long expirationTime = CredentialsMock.CURRENT_TIME_MS; //Same as current time --> expired
when(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime);
when(storage.retrieveLong("com.auth0.cache_expires_at")).thenReturn(expirationTime);
when(storage.retrieveString("com.auth0.scope")).thenReturn("scope");
when(client.renewAuth("refreshToken")).thenReturn(request);

Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 59 * 1000); //new token expires in minTTL - 1 seconds
JWT jwtMock = mock(JWT.class);
when(jwtMock.getExpiresAt()).thenReturn(newDate);
when(jwtDecoder.decode("newId")).thenReturn(jwtMock);

manager.getCredentials(null, 60, callback); //60 seconds of minTTL
verify(request, never()).addParameter(eq("scope"), anyString());
verify(request).start(requestCallbackCaptor.capture());

//Trigger success
String newRefresh = null;
Credentials renewedCredentials = new Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope");
requestCallbackCaptor.getValue().onSuccess(renewedCredentials);
verify(callback).onFailure(exceptionCaptor.capture());

// Verify the credentials are never stored
verify(storage, never()).store(anyString(), anyInt());
verify(storage, never()).store(anyString(), anyLong());
verify(storage, never()).store(anyString(), anyString());
verify(storage, never()).store(anyString(), anyBoolean());
verify(storage, never()).remove(anyString());

CredentialsManagerException exception = exceptionCaptor.getValue();
assertThat(exception, is(notNullValue()));
assertThat(exception.getCause(), is(nullValue()));
assertThat(exception.getMessage(), is("The lifetime of the renewed Access Token or Id Token (1) is less than the minTTL requested (60). Increase the 'Token Expiration' setting of your Auth0 API or the 'ID Token Expiration' of your Auth0 Application in the dashboard, or request a lower minTTL."));
}

@Test
public void shouldGetAndSuccessfullyRenewExpiredCredentialsWithRefreshTokenRotation() {
when(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken");
Expand All @@ -405,7 +547,7 @@ public void shouldGetAndSuccessfullyRenewExpiredCredentialsWithRefreshTokenRotat
when(storage.retrieveString("com.auth0.scope")).thenReturn("scope");
when(client.renewAuth("refreshToken")).thenReturn(request);

Date newDate = new Date(123412341234L);
Date newDate = new Date(CredentialsMock.CURRENT_TIME_MS + 1234 * 1000);
JWT jwtMock = mock(JWT.class);
when(jwtMock.getExpiresAt()).thenReturn(newDate);
when(jwtDecoder.decode("newId")).thenReturn(jwtMock);
Expand Down

0 comments on commit 94304ff

Please sign in to comment.