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

SecureCredentialsManager: Allow to pass scope and minTTL #369

Merged
merged 6 commits into from
Nov 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.auth0.android.authentication.storage;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;

import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.callback.BaseCallback;
import com.auth0.android.jwt.JWT;
import com.auth0.android.result.Credentials;
import com.auth0.android.util.Clock;

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

/**
* Base class meant to abstract common logic across Credentials Manager implementations.
* The scope of this class is package-private, as it's not meant to be exposed
*/
abstract class BaseCredentialsManager {

protected final AuthenticationAPIClient authenticationClient;
protected final Storage storage;
protected final JWTDecoder jwtDecoder;
protected Clock clock;

BaseCredentialsManager(@NonNull AuthenticationAPIClient authenticationClient, @NonNull Storage storage, @NonNull JWTDecoder jwtDecoder) {
this.authenticationClient = authenticationClient;
this.storage = storage;
this.jwtDecoder = jwtDecoder;
this.clock = new ClockImpl();
}

public abstract void saveCredentials(@NonNull Credentials credentials) throws CredentialsManagerException;

public abstract void getCredentials(@NonNull BaseCallback<Credentials, CredentialsManagerException> callback);

public abstract void getCredentials(@Nullable String scope, int minTtl, @NonNull BaseCallback<Credentials, CredentialsManagerException> callback);

public abstract void clearCredentials();

public abstract boolean hasValidCredentials();

public abstract boolean hasValidCredentials(long minTtl);

/**
* Updates the clock instance used for expiration verification purposes.
* The use of this method can help on situations where the clock comes from an external synced source.
* The default implementation uses the time returned by {@link System#currentTimeMillis()}.
*
* @param clock the new clock instance to use.
*/
public void setClock(@NonNull Clock clock) {
this.clock = clock;
}

@VisibleForTesting
long getCurrentTimeInMillis() {
return clock.getCurrentTimeMillis();
}

/**
* Checks if the stored scope is the same as the requested one.
*
* @param storedScope the stored scope, separated by space characters.
* @param requiredScope the required scope, separated by space characters.
* @return whether the scope are different or not
*/
protected 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);
}

/**
* Checks if given the required minimum time to live, the expiration time can satisfy that value or not.
*
* @param expiresAt the expiration time, in milliseconds.
* @param minTtl the time to live required, in seconds.
* @return whether the value will become expired within the given min TTL or not.
*/
protected boolean willExpire(long expiresAt, long minTtl) {
if (expiresAt <= 0) {
// Avoids logging out users when this value was not saved (migration scenario)
return false;
}
long nextClock = getCurrentTimeInMillis() + minTtl * 1000;
return expiresAt <= nextClock;
}

/**
* Checks whether the given expiration time has been reached or not.
*
* @param expiresAt the expiration time, in milliseconds.
* @return whether the given expiration time has been reached or not.
*/
protected boolean hasExpired(long expiresAt) {
return expiresAt <= getCurrentTimeInMillis();
}

/**
* Takes a credentials object and returns the lowest expiration time, considering
* both the access token and the ID token expiration time.
*
* @param credentials the credentials object to check.
* @return the lowest expiration time between the access token and the ID token.
*/
protected long calculateCacheExpiresAt(@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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@
import com.auth0.android.authentication.AuthenticationException;
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 java.util.Locale;

import static android.text.TextUtils.isEmpty;

/**
* Class that handles credentials and allows to save and retrieve them.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class CredentialsManager {
public class CredentialsManager extends BaseCredentialsManager {
private static final String KEY_ACCESS_TOKEN = "com.auth0.access_token";
private static final String KEY_REFRESH_TOKEN = "com.auth0.refresh_token";
private static final String KEY_ID_TOKEN = "com.auth0.id_token";
Expand All @@ -31,17 +29,9 @@ public class CredentialsManager {
private static final String KEY_SCOPE = "com.auth0.scope";
private static final String KEY_CACHE_EXPIRES_AT = "com.auth0.cache_expires_at";

private final AuthenticationAPIClient authClient;
private final Storage storage;
private final JWTDecoder jwtDecoder;
private Clock clock;

@VisibleForTesting
CredentialsManager(@NonNull AuthenticationAPIClient authenticationClient, @NonNull Storage storage, @NonNull JWTDecoder jwtDecoder) {
this.authClient = authenticationClient;
this.storage = storage;
this.jwtDecoder = jwtDecoder;
this.clock = new ClockImpl();
super(authenticationClient, storage, jwtDecoder);
}

/**
Expand All @@ -54,22 +44,12 @@ public CredentialsManager(@NonNull AuthenticationAPIClient authenticationClient,
this(authenticationClient, storage, new JWTDecoder());
}

/**
* Updates the clock instance used for expiration verification purposes.
* The use of this method can help on situations where the clock comes from an external synced source.
* The default implementation uses the time returned by {@link System#currentTimeMillis()}.
*
* @param clock the new clock instance to use.
*/
public void setClock(@NonNull Clock clock) {
this.clock = clock;
}

/**
* Stores the given credentials in the storage. Must have an access_token or id_token and a expires_in value.
*
* @param credentials the credentials to save in the storage.
*/
@Override
public void saveCredentials(@NonNull Credentials credentials) {
if ((isEmpty(credentials.getAccessToken()) && isEmpty(credentials.getIdToken())) || credentials.getExpiresAt() == null) {
throw new CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value.");
Expand All @@ -93,6 +73,7 @@ public void saveCredentials(@NonNull Credentials credentials) {
*
* @param callback the callback that will receive a valid {@link Credentials} or the {@link CredentialsManagerException}.
*/
@Override
public void getCredentials(@NonNull BaseCallback<Credentials, CredentialsManagerException> callback) {
getCredentials(null, 0, callback);
}
Expand All @@ -106,6 +87,7 @@ public void getCredentials(@NonNull BaseCallback<Credentials, CredentialsManager
* @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}.
*/
@Override
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);
Expand Down Expand Up @@ -137,7 +119,7 @@ public void getCredentials(@Nullable String scope, final int minTtl, @NonNull fi
return;
}

final ParameterizableRequest<Credentials, AuthenticationException> request = authClient.renewAuth(refreshToken);
final ParameterizableRequest<Credentials, AuthenticationException> request = authenticationClient.renewAuth(refreshToken);
if (scope != null) {
request.addParameter("scope", scope);
}
Expand All @@ -148,7 +130,7 @@ public void onSuccess(@Nullable Credentials fresh) {
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));
CredentialsManagerException wrongTtlException = new CredentialsManagerException(String.format(Locale.getDefault(), "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;
}
Expand All @@ -168,45 +150,12 @@ public void onFailure(@NonNull AuthenticationException 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) {
JWT idToken = jwtDecoder.decode(credentials.getIdToken());
Date idTokenExpiresAtDate = idToken.getExpiresAt();

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

/**
* Checks if a non-expired pair of credentials can be obtained from this manager.
*
* @return whether there are valid credentials stored on this manager.
*/
@Override
public boolean hasValidCredentials() {
return hasValidCredentials(0);
}
Expand All @@ -217,6 +166,7 @@ public boolean hasValidCredentials() {
* @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.
*/
@Override
public boolean hasValidCredentials(long minTtl) {
String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN);
String refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN);
Expand All @@ -234,6 +184,7 @@ public boolean hasValidCredentials(long minTtl) {
/**
* Removes the credentials from the storage if present.
*/
@Override
public void clearCredentials() {
storage.remove(KEY_ACCESS_TOKEN);
storage.remove(KEY_REFRESH_TOKEN);
Expand All @@ -249,9 +200,4 @@ Credentials recreateCredentials(String idToken, String accessToken, String token
return new Credentials(idToken, accessToken, tokenType, refreshToken, expiresAt, scope);
}

@VisibleForTesting
long getCurrentTimeInMillis() {
return clock.getCurrentTimeMillis();
}

}
Loading