Skip to content

Commit

Permalink
feat: implemented biometrics authentication for SecureCredentialsMana…
Browse files Browse the repository at this point in the history
…ger using androidx.biometrics package (#745)

Signed-off-by: Sai Venkat Desu <[email protected]>
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: Igor Wojda <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 1, 2024
1 parent bf38092 commit beaabdf
Show file tree
Hide file tree
Showing 39 changed files with 2,579 additions and 970 deletions.
13 changes: 9 additions & 4 deletions .snyk
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ ignore:
SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135:
- '*':
reason: Latest version of dokka has this vulnerability
expires: 2024-06-27T07:00:56.333Z
created: 2024-05-28T07:00:56.334Z
expires: 2024-08-31T12:08:37.765Z
created: 2024-08-01T12:08:37.770Z
SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744:
- '*':
reason: Latest version of dokka has this vulnerability
expires: 2024-06-27T07:01:24.820Z
created: 2024-05-28T07:01:24.825Z
expires: 2024-08-31T12:08:55.924Z
created: 2024-08-01T12:08:55.927Z
SNYK-JAVA-COMFASTERXMLJACKSONCORE-7569538:
- '*':
reason: Latest version of dokka has this vulnerability
expires: 2024-08-31T12:08:02.966Z
created: 2024-08-01T12:08:02.973Z
patch: {}
128 changes: 78 additions & 50 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,74 +501,81 @@ This version adds encryption to the data storage. Additionally, in those devices
The usage is similar to the previous version, with the slight difference that the manager now requires a valid android `Context` as shown below:

```kotlin
val authentication = AuthenticationAPIClient(account)
val storage = SharedPreferencesStorage(this)
val manager = SecureCredentialsManager(this, authentication, storage)
val manager = SecureCredentialsManager(this, account, storage)
```

<details>
<summary>Using Java</summary>

```java
AuthenticationAPIClient authentication = new AuthenticationAPIClient(account);
Storage storage = new SharedPreferencesStorage(this);
SecureCredentialsManager manager = new SecureCredentialsManager(this, authentication, storage);
SecureCredentialsManager manager = new SecureCredentialsManager(this, account, storage);
```
</details>

#### Requiring Authentication

You can require the user authentication to obtain credentials. This will make the manager prompt the user with the device's configured Lock Screen, which they must pass correctly in order to obtain the credentials. **This feature is only available on devices where the user has setup a secured Lock Screen** (PIN, Pattern, Password or Fingerprint).

To enable authentication you must call the `requireAuthentication` method passing a valid _Activity_ context, a request code that represents the authentication call, and the title and description to display in the Lock Screen. As seen in the snippet below, you can leave these last two parameters with `null` to use the system's default title and description. It's only safe to call this method before the Activity is started.
To enable authentication you must supply an instance of `FragmentActivity` on which the authentication prompt to be shown, and an instance of `LocalAuthenticationOptions` to configure the authentication prompt with details like title and authentication level when creating an instance of `SecureCredentialsManager` as shown in the snippet below.

```kotlin
//You might want to define a constant with the Request Code
companion object {
const val AUTH_REQ_CODE = 111
}

manager.requireAuthentication(this, AUTH_REQ_CODE, null, null)
val localAuthenticationOptions =
LocalAuthenticationOptions.Builder().setTitle("Authenticate").setDescription("Accessing Credentials")
.setAuthenticationLevel(AuthenticationLevel.STRONG).setNegativeButtonText("Cancel")
.setDeviceCredentialFallback(true)
.build()
val storage = SharedPreferencesStorage(this)
val manager = SecureCredentialsManager(
this, account, storage, fragmentActivity,
localAuthenticationOptions
)
```

<details>
<summary>Using Java</summary>

```java
//You might want to define a constant with the Request Code
private static final int AUTH_REQ_CODE = 11;

manager.requireAuthentication(this, AUTH_REQ_CODE, null, null);
LocalAuthenticationOptions localAuthenticationOptions =
new LocalAuthenticationOptions.Builder().setTitle("Authenticate").setDescription("Accessing Credentials")
.setAuthenticationLevel(AuthenticationLevel.STRONG).setNegativeButtonText("Cancel")
.setDeviceCredentialFallback(true)
.build();
Storage storage = new SharedPreferencesStorage(context);
SecureCredentialsManager secureCredentialsManager = new SecureCredentialsManager(
context, auth0, storage, fragmentActivity,
localAuthenticationOptions);
```
</details>

When the above conditions are met and the manager requires the user authentication, it will use the activity context to launch the Lock Screen activity and wait for its result. If your activity is a subclass of `ComponentActivity`, this will be handled automatically for you internally. Otherwise, your activity must override the `onActivityResult` method and pass the request code and result code to the manager's `checkAuthenticationResult` method to verify if this request was successful or not.
**Points to be Noted**:

```kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (manager.checkAuthenticationResult(requestCode, resultCode)) {
return
}
super.onActivityResult(requestCode, resultCode, data)
}
```
On Android API 29 and below, specifying **DEVICE_CREDENTIAL** alone as the authentication level is not supported.
On Android API 28 and 29, specifying **STRONG** as the authentication level along with enabling device credential fallback is not supported.

<details>
<summary>Using Java</summary>

```java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (manager.checkAuthenticationResult(requestCode, resultCode)) {
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
```
</details>
#### Creating LocalAuthenticationOptions object for requiring Authentication while using SecureCredentialsManager

`LocalAuthenticationOptions` class exposes a Builder class to create an instance of it. Details about the methods are explained below:

- **setTitle(title: String): Builder** - Sets the title to be displayed in the Authentication Prompt.
- **setSubTitle(subtitle: String?): Builder** - Sets the subtitle of the Authentication Prompt.
- **setDescription(description: String?): Builder** - Sets the description for the Authentication Prompt.
- **setAuthenticationLevel(authenticationLevel: AuthenticationLevel): Builder** - Sets the authentication level, more on this can be found [here](#authenticationlevel-enum-values)
- **setDeviceCredentialFallback(enableDeviceCredentialFallback: Boolean): Builder** - Enables/disables device credential fallback.
- **setNegativeButtonText(negativeButtonText: String): Builder** - Sets the negative button text, used only when the device credential fallback is disabled (or) the authentication level is not set to `AuthenticationLevel.DEVICE_CREDENTIAL`.
- **build(): LocalAuthenticationOptions** - Constructs the LocalAuthenticationOptions instance.


If the manager consumed the event, it will return true and later invoke the callback's `onSuccess` with the decrypted credentials.
#### AuthenticationLevel Enum Values

AuthenticationLevel is an enum that defines the different levels of authentication strength required for local authentication mechanisms.

**Enum Values**:
- **STRONG**: Any biometric (e.g., fingerprint, iris, or face) on the device that meets or exceeds the requirements for Class 3 (formerly Strong).
- **WEAK**: Any biometric (e.g., fingerprint, iris, or face) on the device that meets or exceeds the requirements for Class 2 (formerly Weak), as defined by the Android CDD.
- **DEVICE_CREDENTIAL**: The non-biometric credential used to secure the device (i.e., PIN, pattern, or password).

### Handling Credentials Manager exceptions

Expand All @@ -579,6 +586,27 @@ In the event that something happened while trying to save or retrieve the creden
- Device's Lock Screen security settings have changed (e.g. the PIN code was changed). Even when `hasCredentials` returns true, the encryption keys will be deemed invalid and until `saveCredentials` is called again it won't be possible to decrypt any previously existing content, since they keys used back then are not the same as the new ones.
- Device is not compatible with some of the algorithms required by the `SecureCredentialsManager` class. This is considered a catastrophic event and might happen when the OEM has modified the Android ROM removing some of the officially included algorithms. Nevertheless, it can be checked in the exception instance itself by calling `isDeviceIncompatible`. By doing so you can decide the fallback for storing the credentials, such as using the regular `CredentialsManager`.

You can access the `code` property of the `CredentialsManagerException` to understand why the operation with `CredentialsManager` has failed and the `message` property of the `CredentialsManagerException` would give you a description of the exception.

Starting from version `3.0.0` you can even pass the exception to a `when` expression and handle the exception accordingly in your app's logic as shown in the below code snippet:

```kotlin
when(credentialsManagerException) {
CredentialsManagerException.NO_CREDENTIALS - > {
// handle no credentials scenario
}

CredentialsManagerException.NO_REFRESH_TOKEN - > {
// handle no refresh token scenario
}

CredentialsManagerException.STORE_FAILED - > {
// handle store failed scenario
}
// ... similarly for other error codes
}
```

## Bot Protection
If you are using the [Bot Protection](https://auth0.com/docs/anomaly-detection/bot-protection) feature and performing database login/signup via the Authentication API, you need to handle the `AuthenticationException#isVerificationRequired()` error. It indicates that the request was flagged as suspicious and an additional verification step is necessary to log the user in. That verification step is web-based, so you need to use Universal Login to complete it.

Expand Down Expand Up @@ -698,7 +726,7 @@ val users = UsersAPIClient(account, "api access token")
<summary>Using Java</summary>

```java
Auth0 account = new Auth0("client id", "domain");
Auth0 account = Auth0.getInstance("client id", "domain");
UsersAPIClient users = new UsersAPIClient(account, "api token");
```
</details>
Expand Down Expand Up @@ -918,7 +946,7 @@ If you are a user of Auth0 Private Cloud with ["Custom Domains"](https://auth0.c

The validation is done automatically for Web Authentication
```kotlin
val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_CUSTOM_DOMAIN}")
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_CUSTOM_DOMAIN}")

WebAuthProvider.login(account)
.withIdTokenVerificationIssuer("https://{YOUR_AUTH0_DOMAIN}/")
Expand All @@ -928,7 +956,7 @@ WebAuthProvider.login(account)
For Authentication Client, the method `validateClaims()` has to be called to enable it.

```kotlin
val auth0 = Auth0("YOUR_CLIENT_ID", "YOUR_DOMAIN")
val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
val client = AuthenticationAPIClient(auth0)
client
.login("{username or email}", "{password}", "{database connection name}")
Expand All @@ -944,7 +972,7 @@ client
<summary>Using coroutines</summary>

```kotlin
val auth0 = Auth0("YOUR_CLIENT_ID", "YOUR_DOMAIN")
val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
val client = AuthenticationAPIClient(auth0)

try {
Expand All @@ -964,7 +992,7 @@ try {
<summary>Using Java</summary>

```java
Auth0 auth0 = new Auth0("client id", "domain");
Auth0 auth0 = Auth0.getInstance("client id", "domain");
AuthenticationAPIClient client = new AuthenticationAPIClient(account);
client
.login("{username or email}", "{password}", "{database connection name}")
Expand Down Expand Up @@ -1039,7 +1067,7 @@ val netClient = DefaultClient(
readTimeout = 30
)

val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
account.networkingClient = netClient
```

Expand All @@ -1051,7 +1079,7 @@ DefaultClient netClient = new DefaultClient(
connectTimeout = 30,
readTimeout = 30
);
Auth0 account = new Auth0("client id", "domain");
Auth0 account = Auth0.getInstance("client id", "domain");
account.networkingClient = netClient;
```
</details>
Expand All @@ -1063,7 +1091,7 @@ val netClient = DefaultClient(
enableLogging = true
)

val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
account.networkingClient = netClient
```

Expand All @@ -1074,7 +1102,7 @@ account.networkingClient = netClient
DefaultClient netClient = new DefaultClient(
enableLogging = true
);
Auth0 account = new Auth0("client id", "domain");
Auth0 account = Auth0.getInstance("client id", "domain");
account.networkingClient = netClient;
```
</details>
Expand All @@ -1086,7 +1114,7 @@ val netClient = DefaultClient(
defaultHeaders = mapOf("{HEADER-NAME}" to "{HEADER-VALUE}")
)

val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
account.networkingClient = netClient
```

Expand All @@ -1100,7 +1128,7 @@ defaultHeaders.put("{HEADER-NAME}", "{HEADER-VALUE}");
DefaultClient netClient = new DefaultClient(
defaultHeaders = defaultHeaders
);
Auth0 account = new Auth0("client id", "domain");
Auth0 account = Auth0.getInstance("client id", "domain");
account.networkingClient = netClient;
```
</details>
Expand All @@ -1120,7 +1148,7 @@ class CustomNetClient : NetworkingClient {
}
}

val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
account.networkingClient = CustomNetClient()
```

Expand All @@ -1139,7 +1167,7 @@ class CustomNetClient extends NetworkingClient {
}
};

Auth0 account = new Auth0("client id", "domain");
Auth0 account = Auth0.getInstance("client id", "domain");
account.networkingClient = new CustomNetClient();
```
</details>
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ Open your app's `AndroidManifest.xml` file and add the following permission.
First, create an instance of `Auth0` with your Application information

```kotlin
val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
```

<details>
<summary>Using Java</summary>

```java
Auth0 account = new Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}");
Auth0 account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}");
```
</details>

Expand All @@ -94,7 +94,7 @@ Alternatively, you can save your Application information in the `strings.xml` fi
You can then create a new Auth0 instance by passing an Android Context:

```kotlin
val account = Auth0(context)
val account = Auth0.getInstance(context)
```
</details>

Expand Down
66 changes: 66 additions & 0 deletions V3_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Migration Guide from SDK v2 to v3

## Breaking Changes

### Auth0 Class
- **Constructor**: The constructor of the `Auth0` class is now private. Use `Auth0.getInstance(clientId, domain)` to get an instance. This method checks if an instance with the given configuration exists; if yes, it returns it, otherwise, it creates a new one.

### BaseCredentialsManager Interface
- **New Methods**: Added multiple overloads of `getCredentials()` and `awaitCredentials()` to the `BaseCredentialsManager` interface. All implementations of this interface must now override these new methods.

### Request Interface
- **await Function**: The `await` function of the `Request` interface is now abstract. All implementations must implement this method.

### Credentials Class
- **Data Class**: The `Credentials` class is now a data class and can no longer be extended. The `currentTimeInMillis` property has been removed.

### SecureCredentialsManager
- **requireAuthentication Method**: The `requireAuthentication` method, used to enable authentication before obtaining credentials, has been removed. Refer to the [Enabling Authentication](#enabling-authentication-before-obtaining-credentials) section for the new approach.

## Changes

### Biometrics Authentication
- **Library Update**: Implementation of biometrics authentication for retrieving credentials securely is now done using the `androidx.biometric.biometric` library.

### CredentialsManagerException
- **Enum Code**: The `CredentialsManagerException` now contains an enum code. You can use a `when` expression to handle different error scenarios:

```kotlin
when (credentialsManagerException) {
CredentialsManagerException.NO_CREDENTIALS -> {
// handle no credentials scenario
}
CredentialsManagerException.NO_REFRESH_TOKEN -> {
// handle no refresh token scenario
}
CredentialsManagerException.STORE_FAILED -> {
// handle store failed scenario
}
// ... similarly for other error codes
}
```

## Enabling Authentication before Obtaining Credentials

To enable authentication before obtaining credentials, you need to pass the below to the constructor of `SecureCredentialsManager`:
- An instance of `FragmentActivity` where the authentication prompt should be shown.
- An instance of `LocalAuthenticationOptions` to configure details like the level of authentication (Strong, Weak), prompt title, etc.

### Example

```kotlin
private val localAuthenticationOptions = LocalAuthenticationOptions.Builder()
.setTitle("Authenticate to Access Credentials")
.setDescription("description")
.setAuthenticationLevel(AuthenticationLevel.STRONG)
.setDeviceCredentialFallback(true)
.build()

val storage = SharedPreferencesStorage(context)
val manager = SecureCredentialsManager(
context, account, storage, fragmentActivity,
localAuthenticationOptions
)
```

If you need more information, please refer to the [examples.md](examples.md#requiring-authentication) file under the section **Requiring Authentication**.
Loading

0 comments on commit beaabdf

Please sign in to comment.