Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CredentialsManager: Allow to pass scope and minTTL #363

Merged
merged 8 commits into from
Oct 30, 2020
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,24 +75,15 @@ 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 cacheExpiresAt = calculateCacheExpiresAt(credentials);

storage.store(KEY_ACCESS_TOKEN, credentials.getAccessToken());
storage.store(KEY_REFRESH_TOKEN, credentials.getRefreshToken());
storage.store(KEY_ID_TOKEN, credentials.getIdToken());
storage.store(KEY_TOKEN_TYPE, credentials.getType());
storage.store(KEY_EXPIRES_AT, credentials.getExpiresAt().getTime());
storage.store(KEY_SCOPE, credentials.getScope());
storage.store(KEY_CACHE_EXPIRES_AT, expiresAt);
storage.store(KEY_CACHE_EXPIRES_AT, cacheExpiresAt);
}

/**
Expand All @@ -100,34 +93,66 @@ 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) {
lbalmaceda marked this conversation as resolved.
Show resolved Hide resolved
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 the access 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 hasEitherExpired = hasExpired(cacheExpiresAt);
boolean willAccessTokenExpire = willExpire(expiresAt, minTtl);
boolean scopeChanged = hasScopeChanged(storedScope, scope);

if (!hasEitherExpired && !willAccessTokenExpire && !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."));
callback.onFailure(new CredentialsManagerException("Credentials need to be renewed but no Refresh Token is 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 expiresAt = fresh.getExpiresAt().getTime();
boolean willAccessTokenExpire = willExpire(expiresAt, minTtl);
if (willAccessTokenExpire) {
long tokenLifetime = (expiresAt - getCurrentTimeInMillis() - minTtl * 1000) / -1000;
CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format("The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", tokenLifetime, minTtl));
callback.onFailure(wrongTtlException);
return;
}

//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 +165,41 @@ 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 !Arrays.equals(stored, required);
}

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

private boolean hasExpired(long expiresAt) {
return expiresAt <= getCurrentTimeInMillis();
}

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

if (credentials.getIdToken() != null) {
Widcket marked this conversation as resolved.
Show resolved Hide resolved
JWT idToken = jwtDecoder.decode(credentials.getIdToken());
Date idTokenExpiresAtDate = idToken.getExpiresAt();

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

/**
Widcket marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -148,14 +208,27 @@ public void onFailure(@NonNull AuthenticationException error) {
* @return whether there are valid credentials stored on this manager.
*/
public boolean hasValidCredentials() {
return hasValidCredentials(0);
}

/**
* Checks if a non-expired pair of credentials can be obtained from this manager.
*
* @param minTtl the minimum time in seconds that the access token should last before expiration.
* @return whether there are valid credentials stored on this manager.
*/
public boolean hasValidCredentials(long minTtl) {
String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN);
String refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN);
String idToken = storage.retrieveString(KEY_ID_TOKEN);
Long expiresAt = storage.retrieveLong(KEY_EXPIRES_AT);
Long cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT);
if (cacheExpiresAt == null) {
cacheExpiresAt = expiresAt;
}

return !(isEmpty(accessToken) && isEmpty(idToken) ||
expiresAt == null ||
expiresAt <= getCurrentTimeInMillis() && refreshToken == null);
boolean emptyCredentials = isEmpty(accessToken) && isEmpty(idToken) || cacheExpiresAt == null || expiresAt == null;
return !(emptyCredentials || (hasExpired(cacheExpiresAt) || willExpire(expiresAt, minTtl)) && refreshToken == null);
}

/**
Expand Down
Loading