Skip to content

Commit

Permalink
Merge pull request #96 from auth0/credentials-storage
Browse files Browse the repository at this point in the history
Add CredentialsManager and generic Storage
  • Loading branch information
lbalmaceda committed Jun 30, 2017
2 parents fd71481 + 4c7dbe0 commit 918b403
Show file tree
Hide file tree
Showing 17 changed files with 1,113 additions and 75 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,56 @@ WebAuthProvider.init(account)
```


## Credentials Manager
This library ships with a `CredentialsManager` class to easily store and retrieve fresh Credentials from a given `Storage`.

### Usage
1. **Instantiate the manager**
You'll need an `AuthenticationAPIClient` instance used to renew the credentials when they expire and a `Storage`. The Storage implementation is up to you. 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 Singleton like method or be created every time it's needed.

```java
AuthenticationAPIClient authentication = new AuthenticationAPIClient(account);
Storage storage = new SharedPreferencesStorage(this);
CredentialsManager manager = new CredentialsManager(authentication, storage);
```

2. **Save credentials**
The credentials to save **must have** `expires_in` and at least an `access_token` or `id_token` value. If one of the values is missing when trying to set the credentials, the method will throw a `CredentialsManagerException`. If you want the manager to successfully renew the credentials when expired you must also request the `offline_access` scope when logging in in order to receive a `refresh_token` value along with the rest of the tokens. i.e. Logging in with a database connection and saving the credentials:

```java
authentication
.login("info@auth0.com", "a secret password", "my-database-connection")
.setScope("openid offline_access")
.start(new BaseCallback<Credentials>() {
@Override
public void onSuccess(Credentials credentials) {
//Save the credentials
manager.saveCredentials(credentials);
}

@Override
public void onFailure(AuthenticationException error) {
//Error!
}
});
```

3. **Retrieve credentials**
Existing credentials will be returned if they are still valid, otherwise the `refresh_token` will be used to attempt to renew them. If the `expires_in` or both the `access_token` and `id_token` values are missing, the method will throw a `CredentialsManagerException`. The same will happen if the credentials have expired and there's no `refresh_token` available.

```java
manager.getCredentials(new BaseCallback<Credentials, CredentialsManagerException>(){
public void onSuccess(Credentials credentials){
//Use the Credentials
}

public void onFailure(CredentialsManagerException error){
//Error!
}
});
```


## FAQ

* Why is the Android Lint _error_ `'InvalidPackage'` considered a _warning_?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.auth0.android.authentication.storage;

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

import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.authentication.AuthenticationException;
import com.auth0.android.callback.AuthenticationCallback;
import com.auth0.android.callback.BaseCallback;
import com.auth0.android.result.Credentials;

import java.util.Date;

import static android.text.TextUtils.isEmpty;

/**
* Class that handles credentials and allows to save and retrieve them.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class CredentialsManager {
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";
private static final String KEY_TOKEN_TYPE = "com.auth0.token_type";
private static final String KEY_EXPIRES_AT = "com.auth0.expires_at";
private static final String KEY_SCOPE = "com.auth0.scope";

private final AuthenticationAPIClient authClient;
private final Storage storage;

/**
* Creates a new instance of the manager that will store the credentials in the given Storage.
*
* @param authenticationClient the Auth0 Authentication client to refresh credentials with.
* @param storage the storage to use for the credentials.
*/
public CredentialsManager(@NonNull AuthenticationAPIClient authenticationClient, @NonNull Storage storage) {
this.authClient = authenticationClient;
this.storage = storage;
}

/**
* 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.
*/
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.");
}
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());
}

/**
* 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 callback the callback that will receive a valid {@link Credentials} or the {@link CredentialsManagerException}.
*/
public void getCredentials(@NonNull final BaseCallback<Credentials, CredentialsManagerException> callback) {
String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN);
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);

if (isEmpty(accessToken) && isEmpty(idToken) || expiresAt == null) {
callback.onFailure(new CredentialsManagerException("No Credentials were previously set."));
return;
}
if (expiresAt > getCurrentTimeInMillis()) {
callback.onSuccess(recreateCredentials(idToken, accessToken, tokenType, refreshToken, new Date(expiresAt), scope));
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>() {
@Override
public void onSuccess(Credentials freshCredentials) {
callback.onSuccess(freshCredentials);
}

@Override
public void onFailure(AuthenticationException error) {
callback.onFailure(new CredentialsManagerException("An error occurred while trying to use the Refresh Token to renew the Credentials.", error));
}
});
}

@VisibleForTesting
Credentials recreateCredentials(String idToken, String accessToken, String tokenType, String refreshToken, Date expiresAt, String scope) {
return new Credentials(idToken, accessToken, tokenType, refreshToken, expiresAt, scope);
}

@VisibleForTesting
long getCurrentTimeInMillis() {
return System.currentTimeMillis();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.auth0.android.authentication.storage;


import com.auth0.android.Auth0Exception;

/**
* Represents an error raised by the {@link CredentialsManager}.
*/
@SuppressWarnings("WeakerAccess")
public class CredentialsManagerException extends Auth0Exception {
public CredentialsManagerException(String message, Throwable cause) {
super(message, cause);
}

public CredentialsManagerException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.auth0.android.authentication.storage;


import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;

/**
* An implementation of {@link Storage} that uses {@link android.content.SharedPreferences} in Context.MODE_PRIVATE to store the values.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class SharedPreferencesStorage implements Storage {

private static final String SHARED_PREFERENCES_NAME = "com.auth0.authentication.storage";

private final SharedPreferences sp;

/**
* Creates a new {@link Storage} that uses {@link SharedPreferences} in Context.MODE_PRIVATE to store values.
*
* @param context a valid context
*/
public SharedPreferencesStorage(@NonNull Context context) {
this(context, SHARED_PREFERENCES_NAME);
}

/**
* Creates a new {@link Storage} that uses {@link SharedPreferences} in Context.MODE_PRIVATE to store values.
*
* @param context a valid context
* @param sharedPreferencesName the preferences file name
*/
public SharedPreferencesStorage(@NonNull Context context, @NonNull String sharedPreferencesName) {
if (TextUtils.isEmpty(sharedPreferencesName)) {
throw new IllegalArgumentException("The SharedPreferences name is invalid.");
}
sp = context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
}

@Override
public void store(@NonNull String name, @Nullable Long value) {
if (value == null) {
sp.edit().remove(name).apply();
} else {
sp.edit().putLong(name, value).apply();
}
}

@Override
public void store(@NonNull String name, @Nullable Integer value) {
if (value == null) {
sp.edit().remove(name).apply();
} else {
sp.edit().putInt(name, value).apply();
}
}

@Override
public void store(@NonNull String name, @Nullable String value) {
if (value == null) {
sp.edit().remove(name).apply();
} else {
sp.edit().putString(name, value).apply();
}
}

@Nullable
@Override
public Long retrieveLong(@NonNull String name) {
if (!sp.contains(name)) {
return null;
}
return sp.getLong(name, 0);
}

@Nullable
@Override
public String retrieveString(@NonNull String name) {
if (!sp.contains(name)) {
return null;
}
return sp.getString(name, null);
}

@Nullable
@Override
public Integer retrieveInteger(@NonNull String name) {
if (!sp.contains(name)) {
return null;
}
return sp.getInt(name, 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.auth0.android.authentication.storage;

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

/**
* Represents a Storage of key-value data.
* Supported classes are String, Long and Integer.
*/
@SuppressWarnings("WeakerAccess")
public interface Storage {

/**
* Store a given value in the Storage.
*
* @param name the name of the value to store.
* @param value the value to store. Can be null.
*/
void store(@NonNull String name, @Nullable Long value);

/**
* Store a given value in the Storage.
*
* @param name the name of the value to store.
* @param value the value to store. Can be null.
*/
void store(@NonNull String name, @Nullable Integer value);

/**
* Store a given value in the Storage.
*
* @param name the name of the value to store.
* @param value the value to store. Can be null.
*/
void store(@NonNull String name, @Nullable String value);

/**
* Retrieve a value from the Storage.
*
* @param name the name of the value to retrieve.
* @return the value that was previously saved. Can be null.
*/
@Nullable
Long retrieveLong(@NonNull String name);

/**
* Retrieve a value from the Storage.
*
* @param name the name of the value to retrieve.
* @return the value that was previously saved. Can be null.
*/
@Nullable
String retrieveString(@NonNull String name);

/**
* Retrieve a value from the Storage.
*
* @param name the name of the value to retrieve.
* @return the value that was previously saved. Can be null.
*/
@Nullable
Integer retrieveInteger(@NonNull String name);
}
Loading

0 comments on commit 918b403

Please sign in to comment.