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

feat: implemented biometrics authentication for SecureCredentialsManager using androidx.biometrics package #745

Merged
merged 31 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0f2ac48
feat: added initial changes for biometrics support via BiometricManager
desusai7 May 31, 2024
0c7cb16
feat: refined support for biometric manager
desusai7 Jun 6, 2024
1c4beca
feat: better handling of the exceptions thrown from CredentialsManager
desusai7 Jun 10, 2024
d846ecc
chore: minor improvements
desusai7 Jun 10, 2024
ed37453
feat: updated sample app to use the getCredentialsWithAuthentication API
desusai7 Jun 10, 2024
fcc7029
chore: included biometric package directly instead of compileOnly
desusai7 Jul 5, 2024
424417e
chore: broke down BaseCredentialManager into different abstract class…
desusai7 Jul 8, 2024
156d472
chore: handled unsupported combinations of authentication levels on A…
desusai7 Jul 8, 2024
ddd9b5e
chore: updated LocalAuthenticationManager to implement Authentication…
desusai7 Jul 8, 2024
bd30ba2
chore: changed LocalAuthenticationManager creation to factory pattern…
desusai7 Jul 8, 2024
1f949fd
chore: updated all the methods in the SecureCredentialsManager to con…
desusai7 Jul 8, 2024
1363d8d
test: added unit tests for LocalAuthenticationManager
desusai7 Jul 8, 2024
4bb0bd7
test: updated unit tests in CredentialsManagerTest and SecureCredenti…
desusai7 Jul 8, 2024
8c06318
chore: updated sample app to use getCredentials with biometric prompt
desusai7 Jul 8, 2024
d9af51a
chore: minor changes to the sample app
desusai7 Jul 8, 2024
829b616
test: fixed clock tests in secure credential manager tests
desusai7 Jul 8, 2024
19e8fcd
docs: Clarify Readme around targets (#744)
igorwojda Jun 12, 2024
08851a8
Bump codecov/codecov-action from 4.4.1 to 4.5.0 (#746)
dependabot[bot] Jul 8, 2024
710561c
chore: updated SecureCredentialsManager to accept fragment activity a…
desusai7 Jul 19, 2024
d8415f6
chore: updated SecureCredentialsManager to return credentials without…
desusai7 Jul 22, 2024
66ab119
chore: minor changes
desusai7 Jul 22, 2024
8d6c33e
chore: minor updates to the setter methods in LocalAuthenticationOpti…
desusai7 Jul 24, 2024
46ad75b
feat: added synchronization to support secure credentials manager fro…
desusai7 Jul 25, 2024
c8b7e11
test: added unit tests for synchronization of activity across multipl…
desusai7 Jul 26, 2024
9ee0052
chore: minor refactoring
desusai7 Jul 30, 2024
1f3b7b3
BREAKING CHANGE: incorporated changes planned for v3 (#751)
desusai7 Jul 31, 2024
7356905
chore: minor changes
desusai7 Aug 1, 2024
9f6a1a6
docs: updated examples and added migration guide
desusai7 Aug 1, 2024
856cebe
chore: updated snyk ignore because of issues with dokka
desusai7 Aug 1, 2024
46d024b
chore: overridden hashcode method of CredentialsManagerException
desusai7 Aug 1, 2024
d5704a0
Merge branch 'main' into feat/biometric
desusai7 Aug 1, 2024
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
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
Loading