Skip to content

Commit beaabdf

Browse files
desusai7igorwojdadependabot[bot]
authored
feat: implemented biometrics authentication for SecureCredentialsManager 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>
1 parent bf38092 commit beaabdf

39 files changed

+2579
-970
lines changed

.snyk

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ ignore:
55
SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135:
66
- '*':
77
reason: Latest version of dokka has this vulnerability
8-
expires: 2024-06-27T07:00:56.333Z
9-
created: 2024-05-28T07:00:56.334Z
8+
expires: 2024-08-31T12:08:37.765Z
9+
created: 2024-08-01T12:08:37.770Z
1010
SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744:
1111
- '*':
1212
reason: Latest version of dokka has this vulnerability
13-
expires: 2024-06-27T07:01:24.820Z
14-
created: 2024-05-28T07:01:24.825Z
13+
expires: 2024-08-31T12:08:55.924Z
14+
created: 2024-08-01T12:08:55.927Z
15+
SNYK-JAVA-COMFASTERXMLJACKSONCORE-7569538:
16+
- '*':
17+
reason: Latest version of dokka has this vulnerability
18+
expires: 2024-08-31T12:08:02.966Z
19+
created: 2024-08-01T12:08:02.973Z
1520
patch: {}

EXAMPLES.md

Lines changed: 78 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -501,74 +501,81 @@ This version adds encryption to the data storage. Additionally, in those devices
501501
The usage is similar to the previous version, with the slight difference that the manager now requires a valid android `Context` as shown below:
502502

503503
```kotlin
504-
val authentication = AuthenticationAPIClient(account)
505504
val storage = SharedPreferencesStorage(this)
506-
val manager = SecureCredentialsManager(this, authentication, storage)
505+
val manager = SecureCredentialsManager(this, account, storage)
507506
```
508507

509508
<details>
510509
<summary>Using Java</summary>
511510

512511
```java
513-
AuthenticationAPIClient authentication = new AuthenticationAPIClient(account);
514512
Storage storage = new SharedPreferencesStorage(this);
515-
SecureCredentialsManager manager = new SecureCredentialsManager(this, authentication, storage);
513+
SecureCredentialsManager manager = new SecureCredentialsManager(this, account, storage);
516514
```
517515
</details>
518516

519517
#### Requiring Authentication
520518

521519
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).
522520

523-
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.
521+
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.
524522

525523
```kotlin
526-
//You might want to define a constant with the Request Code
527-
companion object {
528-
const val AUTH_REQ_CODE = 111
529-
}
530-
531-
manager.requireAuthentication(this, AUTH_REQ_CODE, null, null)
524+
val localAuthenticationOptions =
525+
LocalAuthenticationOptions.Builder().setTitle("Authenticate").setDescription("Accessing Credentials")
526+
.setAuthenticationLevel(AuthenticationLevel.STRONG).setNegativeButtonText("Cancel")
527+
.setDeviceCredentialFallback(true)
528+
.build()
529+
val storage = SharedPreferencesStorage(this)
530+
val manager = SecureCredentialsManager(
531+
this, account, storage, fragmentActivity,
532+
localAuthenticationOptions
533+
)
532534
```
533535

534536
<details>
535537
<summary>Using Java</summary>
536538

537539
```java
538-
//You might want to define a constant with the Request Code
539-
private static final int AUTH_REQ_CODE = 11;
540-
541-
manager.requireAuthentication(this, AUTH_REQ_CODE, null, null);
540+
LocalAuthenticationOptions localAuthenticationOptions =
541+
new LocalAuthenticationOptions.Builder().setTitle("Authenticate").setDescription("Accessing Credentials")
542+
.setAuthenticationLevel(AuthenticationLevel.STRONG).setNegativeButtonText("Cancel")
543+
.setDeviceCredentialFallback(true)
544+
.build();
545+
Storage storage = new SharedPreferencesStorage(context);
546+
SecureCredentialsManager secureCredentialsManager = new SecureCredentialsManager(
547+
context, auth0, storage, fragmentActivity,
548+
localAuthenticationOptions);
542549
```
543550
</details>
544551

545-
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.
552+
**Points to be Noted**:
546553

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

556-
<details>
557-
<summary>Using Java</summary>
558557

559-
```java
560-
@Override
561-
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
562-
if (manager.checkAuthenticationResult(requestCode, resultCode)) {
563-
return;
564-
}
565-
super.onActivityResult(requestCode, resultCode, data);
566-
}
567-
```
568-
</details>
558+
#### Creating LocalAuthenticationOptions object for requiring Authentication while using SecureCredentialsManager
559+
560+
`LocalAuthenticationOptions` class exposes a Builder class to create an instance of it. Details about the methods are explained below:
561+
562+
- **setTitle(title: String): Builder** - Sets the title to be displayed in the Authentication Prompt.
563+
- **setSubTitle(subtitle: String?): Builder** - Sets the subtitle of the Authentication Prompt.
564+
- **setDescription(description: String?): Builder** - Sets the description for the Authentication Prompt.
565+
- **setAuthenticationLevel(authenticationLevel: AuthenticationLevel): Builder** - Sets the authentication level, more on this can be found [here](#authenticationlevel-enum-values)
566+
- **setDeviceCredentialFallback(enableDeviceCredentialFallback: Boolean): Builder** - Enables/disables device credential fallback.
567+
- **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`.
568+
- **build(): LocalAuthenticationOptions** - Constructs the LocalAuthenticationOptions instance.
569+
569570

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

573+
AuthenticationLevel is an enum that defines the different levels of authentication strength required for local authentication mechanisms.
574+
575+
**Enum Values**:
576+
- **STRONG**: Any biometric (e.g., fingerprint, iris, or face) on the device that meets or exceeds the requirements for Class 3 (formerly Strong).
577+
- **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.
578+
- **DEVICE_CREDENTIAL**: The non-biometric credential used to secure the device (i.e., PIN, pattern, or password).
572579

573580
### Handling Credentials Manager exceptions
574581

@@ -579,6 +586,27 @@ In the event that something happened while trying to save or retrieve the creden
579586
- 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.
580587
- 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`.
581588

589+
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.
590+
591+
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:
592+
593+
```kotlin
594+
when(credentialsManagerException) {
595+
CredentialsManagerException.NO_CREDENTIALS - > {
596+
// handle no credentials scenario
597+
}
598+
599+
CredentialsManagerException.NO_REFRESH_TOKEN - > {
600+
// handle no refresh token scenario
601+
}
602+
603+
CredentialsManagerException.STORE_FAILED - > {
604+
// handle store failed scenario
605+
}
606+
// ... similarly for other error codes
607+
}
608+
```
609+
582610
## Bot Protection
583611
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.
584612

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

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

919947
The validation is done automatically for Web Authentication
920948
```kotlin
921-
val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_CUSTOM_DOMAIN}")
949+
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_CUSTOM_DOMAIN}")
922950

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

930958
```kotlin
931-
val auth0 = Auth0("YOUR_CLIENT_ID", "YOUR_DOMAIN")
959+
val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
932960
val client = AuthenticationAPIClient(auth0)
933961
client
934962
.login("{username or email}", "{password}", "{database connection name}")
@@ -944,7 +972,7 @@ client
944972
<summary>Using coroutines</summary>
945973

946974
```kotlin
947-
val auth0 = Auth0("YOUR_CLIENT_ID", "YOUR_DOMAIN")
975+
val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
948976
val client = AuthenticationAPIClient(auth0)
949977

950978
try {
@@ -964,7 +992,7 @@ try {
964992
<summary>Using Java</summary>
965993

966994
```java
967-
Auth0 auth0 = new Auth0("client id", "domain");
995+
Auth0 auth0 = Auth0.getInstance("client id", "domain");
968996
AuthenticationAPIClient client = new AuthenticationAPIClient(account);
969997
client
970998
.login("{username or email}", "{password}", "{database connection name}")
@@ -1039,7 +1067,7 @@ val netClient = DefaultClient(
10391067
readTimeout = 30
10401068
)
10411069

1042-
val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
1070+
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
10431071
account.networkingClient = netClient
10441072
```
10451073

@@ -1051,7 +1079,7 @@ DefaultClient netClient = new DefaultClient(
10511079
connectTimeout = 30,
10521080
readTimeout = 30
10531081
);
1054-
Auth0 account = new Auth0("client id", "domain");
1082+
Auth0 account = Auth0.getInstance("client id", "domain");
10551083
account.networkingClient = netClient;
10561084
```
10571085
</details>
@@ -1063,7 +1091,7 @@ val netClient = DefaultClient(
10631091
enableLogging = true
10641092
)
10651093

1066-
val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
1094+
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
10671095
account.networkingClient = netClient
10681096
```
10691097

@@ -1074,7 +1102,7 @@ account.networkingClient = netClient
10741102
DefaultClient netClient = new DefaultClient(
10751103
enableLogging = true
10761104
);
1077-
Auth0 account = new Auth0("client id", "domain");
1105+
Auth0 account = Auth0.getInstance("client id", "domain");
10781106
account.networkingClient = netClient;
10791107
```
10801108
</details>
@@ -1086,7 +1114,7 @@ val netClient = DefaultClient(
10861114
defaultHeaders = mapOf("{HEADER-NAME}" to "{HEADER-VALUE}")
10871115
)
10881116

1089-
val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
1117+
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
10901118
account.networkingClient = netClient
10911119
```
10921120

@@ -1100,7 +1128,7 @@ defaultHeaders.put("{HEADER-NAME}", "{HEADER-VALUE}");
11001128
DefaultClient netClient = new DefaultClient(
11011129
defaultHeaders = defaultHeaders
11021130
);
1103-
Auth0 account = new Auth0("client id", "domain");
1131+
Auth0 account = Auth0.getInstance("client id", "domain");
11041132
account.networkingClient = netClient;
11051133
```
11061134
</details>
@@ -1120,7 +1148,7 @@ class CustomNetClient : NetworkingClient {
11201148
}
11211149
}
11221150

1123-
val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
1151+
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
11241152
account.networkingClient = CustomNetClient()
11251153
```
11261154

@@ -1139,7 +1167,7 @@ class CustomNetClient extends NetworkingClient {
11391167
}
11401168
};
11411169

1142-
Auth0 account = new Auth0("client id", "domain");
1170+
Auth0 account = Auth0.getInstance("client id", "domain");
11431171
account.networkingClient = new CustomNetClient();
11441172
```
11451173
</details>

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,14 @@ Open your app's `AndroidManifest.xml` file and add the following permission.
6868
First, create an instance of `Auth0` with your Application information
6969

7070
```kotlin
71-
val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
71+
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
7272
```
7373

7474
<details>
7575
<summary>Using Java</summary>
7676

7777
```java
78-
Auth0 account = new Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}");
78+
Auth0 account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}");
7979
```
8080
</details>
8181

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

9696
```kotlin
97-
val account = Auth0(context)
97+
val account = Auth0.getInstance(context)
9898
```
9999
</details>
100100

V3_MIGRATION_GUIDE.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Migration Guide from SDK v2 to v3
2+
3+
## Breaking Changes
4+
5+
### Auth0 Class
6+
- **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.
7+
8+
### BaseCredentialsManager Interface
9+
- **New Methods**: Added multiple overloads of `getCredentials()` and `awaitCredentials()` to the `BaseCredentialsManager` interface. All implementations of this interface must now override these new methods.
10+
11+
### Request Interface
12+
- **await Function**: The `await` function of the `Request` interface is now abstract. All implementations must implement this method.
13+
14+
### Credentials Class
15+
- **Data Class**: The `Credentials` class is now a data class and can no longer be extended. The `currentTimeInMillis` property has been removed.
16+
17+
### SecureCredentialsManager
18+
- **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.
19+
20+
## Changes
21+
22+
### Biometrics Authentication
23+
- **Library Update**: Implementation of biometrics authentication for retrieving credentials securely is now done using the `androidx.biometric.biometric` library.
24+
25+
### CredentialsManagerException
26+
- **Enum Code**: The `CredentialsManagerException` now contains an enum code. You can use a `when` expression to handle different error scenarios:
27+
28+
```kotlin
29+
when (credentialsManagerException) {
30+
CredentialsManagerException.NO_CREDENTIALS -> {
31+
// handle no credentials scenario
32+
}
33+
CredentialsManagerException.NO_REFRESH_TOKEN -> {
34+
// handle no refresh token scenario
35+
}
36+
CredentialsManagerException.STORE_FAILED -> {
37+
// handle store failed scenario
38+
}
39+
// ... similarly for other error codes
40+
}
41+
```
42+
43+
## Enabling Authentication before Obtaining Credentials
44+
45+
To enable authentication before obtaining credentials, you need to pass the below to the constructor of `SecureCredentialsManager`:
46+
- An instance of `FragmentActivity` where the authentication prompt should be shown.
47+
- An instance of `LocalAuthenticationOptions` to configure details like the level of authentication (Strong, Weak), prompt title, etc.
48+
49+
### Example
50+
51+
```kotlin
52+
private val localAuthenticationOptions = LocalAuthenticationOptions.Builder()
53+
.setTitle("Authenticate to Access Credentials")
54+
.setDescription("description")
55+
.setAuthenticationLevel(AuthenticationLevel.STRONG)
56+
.setDeviceCredentialFallback(true)
57+
.build()
58+
59+
val storage = SharedPreferencesStorage(context)
60+
val manager = SecureCredentialsManager(
61+
context, account, storage, fragmentActivity,
62+
localAuthenticationOptions
63+
)
64+
```
65+
66+
If you need more information, please refer to the [examples.md](examples.md#requiring-authentication) file under the section **Requiring Authentication**.

0 commit comments

Comments
 (0)