Skip to content

Commit

Permalink
BREAKING CHANGE: Updated Android SDK to v3 beta & fixed issues with b…
Browse files Browse the repository at this point in the history
…iometrics Authentication on Android. (#940)

Signed-off-by: Sai Venkat Desu <[email protected]>
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: stevenwong-okta <[email protected]>
  • Loading branch information
3 people authored Aug 5, 2024
1 parent 1aa5e70 commit f17a0bf
Show file tree
Hide file tree
Showing 28 changed files with 636 additions and 285 deletions.
56 changes: 56 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,61 @@
# Migration Guide

## Upgrading from v3 -> v4

- **If your project is built with Expo:**
- Run `npx expo prebuild --clean` to ensure the intent-filters in `android` & custom scheme's in iOS are propertly setup. Please note that any manual changes to Android or iOS folders will be lost when this command is executed.

### Breaking Changes:

- `requireLocalAuthentication` method is no longer available as part of the `CredentialsManager` class or the `useAuth0` Hook from v4 of the SDK. Refer below sections on how to enable authentication before obtaining credentials now.

### Changes:

- Updated the `Auth0` class constructor to accept a new parameter, `LocalAuthenticationOptions`, for enabling authentication before obtaining credentials as shown below:

```
const localAuthOptions: LocalAuthenticationOptions = {
title: 'Authenticate to retreive your credentials',
subtitle: 'Please authenticate to continue',
description: 'We need to authenticate you to retrieve your credentials',
cancelTitle: 'Cancel',
evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
fallbackTitle: 'Use Passcode',
authenticationLevel: LocalAuthenticationLevel.strong,
deviceCredentialFallback: true,
}
const auth0 = new Auth0({ domain: config.domain, clientId: config.clientId, localAuthenticationOptions: localAuthOptions });
```

Modified the `Auth0Provider` to accept `LocalAuthenticationOptions` as a parameter to enable authentication before obtaining credentials.

```
const localAuthOptions: LocalAuthenticationOptions = {
title: 'Authenticate to retreive your credentials',
subtitle: 'Please authenticate to continue',
description: 'We need to authenticate you to retrieve your credentials',
cancelTitle: 'Cancel',
evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
fallbackTitle: 'Use Passcode',
authenticationLevel: LocalAuthenticationLevel.strong,
deviceCredentialFallback: true,
};
const App = () => {
return (
<Auth0Provider
domain={config.domain}
clientId={config.clientId}
localAuthenticationOptions={localAuthOptions}
>
{/* YOUR APP */}
</Auth0Provider>
);
};
export default App;
```

## Upgrading from v2 -> v3

### Improvements and changes
Expand Down
83 changes: 79 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,15 +394,90 @@ const credentials = await auth0.credentialsManager.getCredentials();

> 💡 You do not need to call credentialsManager.saveCredentials() afterward. The Credentials Manager automatically persists the renewed credentials.
#### Local authentication
#### Requiring Authentication before obtaining Credentials

> :warning: The `requireLocalAuthentication` method is no longer available as part of the `CredentialsManager` class or the `useAuth0` Hook from v4 of the SDK.
> ℹ️ You need to use at least version `0.59.0` of React Native, as it uses `FragmentActivity` as the base activity, which is required for biometric authentication to work.
You can enable an additional level of user authentication before retrieving credentials using the local authentication supported by the device, for example PIN or fingerprint on Android, and Face ID or Touch ID on iOS.

```js
await auth0.credentialsManager.requireLocalAuthentication();
Refer to the instructions below to understand how to enable authentication before retrieving credentials based on your setup:

**Using Auth0 Class:**

The `Auth0` class constructor now accepts a new parameter, which is an instance of the `LocalAuthenticationOptions` object. This needs to be passed while creating an instance of `Auth0` to enable authentication before obtaining credentials, as shown in the code snippet below:

```tsx
import Auth0 from 'react-native-auth0';
const localAuthOptions: LocalAuthenticationOptions = {
title: 'Authenticate to retreive your credentials',
subtitle: 'Please authenticate to continue',
description: 'We need to authenticate you to retrieve your credentials',
cancelTitle: 'Cancel',
evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
fallbackTitle: 'Use Passcode',
authenticationLevel: LocalAuthenticationLevel.strong,
deviceCredentialFallback: true,
};
const auth0 = new Auth0({
domain: config.domain,
clientId: config.clientId,
localAuthenticationOptions: localAuthOptions,
});
```

**Using Hooks (Auth0Provider):**

`Auth0Provider` now accepts a new parameter, which is an instance of the `LocalAuthenticationOptions` object. This needs to be passed to enable authentication before obtaining credentials, as shown in the code snippet below:

```tsx
import { Auth0Provider } from 'react-native-auth0';

const localAuthOptions: LocalAuthenticationOptions = {
title: 'Authenticate to retreive your credentials',
subtitle: 'Please authenticate to continue',
description: 'We need to authenticate you to retrieve your credentials',
cancelTitle: 'Cancel',
evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
fallbackTitle: 'Use Passcode',
authenticationLevel: LocalAuthenticationLevel.strong,
deviceCredentialFallback: true,
};

const App = () => {
return (
<Auth0Provider
domain={config.domain}
clientId={config.clientId}
localAuthenticationOptions={localAuthOptions}
>
{/* YOUR APP */}
</Auth0Provider>
);
};

export default App;
```

Check the [API documentation](https://auth0.github.io/react-native-auth0/classes/Types.CredentialsManager.html#requireLocalAuthentication) to learn more about the available LocalAuthentication properties.
Detailed information on `LocalAuthenticationOptions` is available [here](#localauthenticationoptions)

**LocalAuthenticationOptions:**

The options for configuring the display of local authentication prompt, authentication level (Android only), and evaluation policy (iOS only).

**Properties:**

| Property | Type | Description | Applicable Platforms |
| -------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------- |
| `title` | `String` | The title of the authentication prompt. | Android, iOS |
| `subtitle` | `String` (optional) | The subtitle of the authentication prompt. | Android |
| `description` | `String` (optional) | The description of the authentication prompt. | Android |
| `cancelTitle` | `String` (optional) | The cancel button title of the authentication prompt. | Android, iOS |
| `evaluationPolicy` | `LocalAuthenticationStrategy` (optional) | The evaluation policy to use when prompting the user for authentication. Defaults to `deviceOwnerWithBiometrics`. | iOS |
| `fallbackTitle` | `String` (optional) | The fallback button title of the authentication prompt. | iOS |
| `authenticationLevel` | `LocalAuthenticationLevel` (optional) | The authentication level to use when prompting the user for authentication. Defaults to `strong`. | Android |
| `deviceCredentialFallback` | `Boolean` (optional) | Should the user be given the option to authenticate with their device PIN, pattern, or password instead of a biometric. | Android |

> :warning: You need a real device to test Local Authentication for iOS. Local Authentication is not available in simulators.
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ repositories {
dependencies {
implementation "com.facebook.react:react-native:${safeExtGet('reactnativeVersion', '+')}"
implementation "androidx.browser:browser:1.2.0"
implementation 'com.auth0.android:auth0:2.10.2'
implementation 'com.auth0.android:auth0:3.0.0-beta.0'
}

def configureReactNativePom(def pom) {
Expand Down
138 changes: 79 additions & 59 deletions android/src/main/java/com/auth0/react/A0Auth0Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import android.content.Intent;

import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;

import com.auth0.android.Auth0;
import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.authentication.AuthenticationException;
import com.auth0.android.authentication.storage.CredentialsManagerException;
import com.auth0.android.authentication.storage.LocalAuthenticationOptions;
import com.auth0.android.authentication.storage.SecureCredentialsManager;
import com.auth0.android.authentication.storage.SharedPreferencesStorage;
import com.auth0.android.provider.WebAuthProvider;
Expand All @@ -19,16 +21,18 @@
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;

import com.facebook.react.bridge.UiThreadUtil;
import java.net.MalformedURLException;
import java.net.URL;

import java.util.HashMap;
import java.util.Map;

public class A0Auth0Module extends ReactContextBaseJavaModule implements ActivityEventListener {

private static final String CREDENTIAL_MANAGER_ERROR_CODE = "a0.invalid_state.credential_manager_exception";
private static final String INVALID_DOMAIN_URL_ERROR_CODE = "a0.invalid_domain_url";
private static final String BIOMETRICS_AUTHENTICATION_ERROR_CODE = "a0.invalid_options_biometrics_authentication";
private static final int LOCAL_AUTH_REQUEST_CODE = 150;
public static final int UNKNOWN_ERROR_RESULT_CODE = 1405;

Expand All @@ -44,14 +48,41 @@ public A0Auth0Module(ReactApplicationContext reactContext) {
}

@ReactMethod
public void initializeAuth0WithConfiguration(String clientId, String domain) {
this.auth0 = new Auth0(clientId, domain);
AuthenticationAPIClient authenticationAPIClient = new AuthenticationAPIClient(auth0);
this.secureCredentialsManager = new SecureCredentialsManager(
public void initializeAuth0WithConfiguration(String clientId, String domain, ReadableMap localAuthenticationOptions, Promise promise) {
this.auth0 = Auth0.getInstance(clientId, domain);
if (localAuthenticationOptions != null) {
Activity activity = getCurrentActivity();
if (activity instanceof FragmentActivity) {
try {
LocalAuthenticationOptions localAuthOptions = LocalAuthenticationOptionsParser.fromMap(localAuthenticationOptions);
this.secureCredentialsManager = new SecureCredentialsManager(
reactContext,
auth0,
new SharedPreferencesStorage(reactContext),
(FragmentActivity) activity,
localAuthOptions);
promise.resolve(true);
return;
} catch (Exception e) {
this.secureCredentialsManager = getSecureCredentialsManagerWithoutBiometrics();
promise.reject(BIOMETRICS_AUTHENTICATION_ERROR_CODE, "Failed to parse the Local Authentication Options, hence proceeding without Biometrics Authentication for handling Credentials");
return;
}
} else {
this.secureCredentialsManager = getSecureCredentialsManagerWithoutBiometrics();
promise.reject(BIOMETRICS_AUTHENTICATION_ERROR_CODE, "Biometrics Authentication for Handling Credentials are supported only on FragmentActivity, since a different activity is supplied, proceeding without it");
return;
}
}
this.secureCredentialsManager = getSecureCredentialsManagerWithoutBiometrics();
promise.resolve(true);
}

private @NonNull SecureCredentialsManager getSecureCredentialsManagerWithoutBiometrics() {
return new SecureCredentialsManager(
reactContext,
authenticationAPIClient,
new SharedPreferencesStorage(reactContext)
);
auth0,
new SharedPreferencesStorage(reactContext));
}

@ReactMethod
Expand All @@ -72,26 +103,28 @@ public void hasValidAuth0InstanceWithConfiguration(String clientId, String domai
}

@ReactMethod
public void getCredentials(String scope, double minTtl, ReadableMap parameters, boolean forceRefresh, Promise promise) {
public void getCredentials(String scope, double minTtl, ReadableMap parameters, boolean forceRefresh,
Promise promise) {
Map<String, String> cleanedParameters = new HashMap<>();
for (Map.Entry<String, Object> entry : parameters.toHashMap().entrySet()) {
if (entry.getValue() != null) {
cleanedParameters.put(entry.getKey(), entry.getValue().toString());
}
}

this.secureCredentialsManager.getCredentials(scope, (int) minTtl, cleanedParameters, forceRefresh, new com.auth0.android.callback.Callback<Credentials, CredentialsManagerException>() {
@Override
public void onSuccess(Credentials credentials) {
ReadableMap map = CredentialsParser.toMap(credentials);
promise.resolve(map);
}
UiThreadUtil.runOnUiThread(() -> secureCredentialsManager.getCredentials(scope, (int) minTtl, cleanedParameters, forceRefresh,
new com.auth0.android.callback.Callback<Credentials, CredentialsManagerException>() {
@Override
public void onSuccess(Credentials credentials) {
ReadableMap map = CredentialsParser.toMap(credentials);
promise.resolve(map);
}

@Override
public void onFailure(@NonNull CredentialsManagerException e) {
promise.reject(CREDENTIAL_MANAGER_ERROR_CODE, e.getMessage(), e);
}
});
@Override
public void onFailure(@NonNull CredentialsManagerException e) {
promise.reject(CREDENTIAL_MANAGER_ERROR_CODE, e.getMessage(), e);
}
}));
}

@ReactMethod
Expand All @@ -104,23 +137,6 @@ public void saveCredentials(ReadableMap credentials, Promise promise) {
}
}

@ReactMethod
public void enableLocalAuthentication(String title, String description, Promise promise) {
Activity activity = reactContext.getCurrentActivity();
if (activity == null) {
promise.reject(CREDENTIAL_MANAGER_ERROR_CODE, "No current activity present");
return;
}
activity.runOnUiThread(() -> {
try {
A0Auth0Module.this.secureCredentialsManager.requireAuthentication(activity, LOCAL_AUTH_REQUEST_CODE, title, description);
promise.resolve(true);
} catch (CredentialsManagerException e) {
promise.reject(CREDENTIAL_MANAGER_ERROR_CODE, e.getMessage(), e);
}
});
}

@ReactMethod
public void clearCredentials(Promise promise) {
this.secureCredentialsManager.clearCredentials();
Expand All @@ -146,7 +162,10 @@ public String getName() {
}

@ReactMethod
public void webAuth(String scheme, String redirectUri, String state, String nonce, String audience, String scope, String connection, int maxAge, String organization, String invitationUrl, int leeway, boolean ephemeralSession, int safariViewControllerPresentationStyle, ReadableMap additionalParameters, Promise promise) {
public void webAuth(String scheme, String redirectUri, String state, String nonce, String audience, String scope,
String connection, int maxAge, String organization, String invitationUrl, int leeway,
boolean ephemeralSession, int safariViewControllerPresentationStyle, ReadableMap additionalParameters,
Promise promise) {
this.webAuthPromise = promise;
Map<String, String> cleanedParameters = new HashMap<>();
for (Map.Entry<String, Object> entry : additionalParameters.toHashMap().entrySet()) {
Expand Down Expand Up @@ -187,13 +206,14 @@ public void webAuth(String scheme, String redirectUri, String state, String nonc
builder.withRedirectUri(redirectUri);
}
builder.withParameters(cleanedParameters);
builder.start(reactContext.getCurrentActivity(), new com.auth0.android.callback.Callback<Credentials, AuthenticationException>() {
@Override
public void onSuccess(Credentials result) {
ReadableMap map = CredentialsParser.toMap(result);
promise.resolve(map);
webAuthPromise = null;
}
builder.start(reactContext.getCurrentActivity(),
new com.auth0.android.callback.Callback<Credentials, AuthenticationException>() {
@Override
public void onSuccess(Credentials result) {
ReadableMap map = CredentialsParser.toMap(result);
promise.resolve(map);
webAuthPromise = null;
}

@Override
public void onFailure(@NonNull AuthenticationException error) {
Expand All @@ -213,17 +233,18 @@ public void webAuthLogout(String scheme, boolean federated, String redirectUri,
if (redirectUri != null) {
builder.withReturnToUrl(redirectUri);
}
builder.start(reactContext.getCurrentActivity(), new com.auth0.android.callback.Callback<Void, AuthenticationException>() {
@Override
public void onSuccess(Void credentials) {
promise.resolve(true);
}
builder.start(reactContext.getCurrentActivity(),
new com.auth0.android.callback.Callback<Void, AuthenticationException>() {
@Override
public void onSuccess(Void credentials) {
promise.resolve(true);
}

@Override
public void onFailure(AuthenticationException e) {
handleError(e, promise);
}
});
@Override
public void onFailure(AuthenticationException e) {
handleError(e, promise);
}
});
}

private void handleError(AuthenticationException error, Promise promise) {
Expand All @@ -249,15 +270,14 @@ private void handleError(AuthenticationException error, Promise promise) {

@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == LOCAL_AUTH_REQUEST_CODE) {
secureCredentialsManager.checkAuthenticationResult(requestCode, resultCode);
}
// No-op
}

@Override
public void onNewIntent(Intent intent) {
if (webAuthPromise != null) {
webAuthPromise.reject("a0.session.browser_terminated", "The browser window was closed by a new instance of the application");
webAuthPromise.reject("a0.session.browser_terminated",
"The browser window was closed by a new instance of the application");
webAuthPromise = null;
}
}
Expand Down
Loading

0 comments on commit f17a0bf

Please sign in to comment.