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

BREAKING CHANGE: Updated Android SDK to v3 beta & fixed issues with biometrics Authentication on Android. #940

Merged
merged 23 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d9c8df2
BREAKING CHANGE: added initial changes for using Android v3
desusai7 Jul 23, 2024
5955645
fix: fixed issues with ui thread on android while using local authent…
desusai7 Jul 30, 2024
defc69b
fix: fixed double invocation of callbacks on iOS module
desusai7 Jul 30, 2024
0a11ecb
feat: bumped up the dependency on Android SDK to 3.0.0-beta.0
desusai7 Aug 2, 2024
8329481
chore: removed references to requireLocalAuthentication in Auth0Context
desusai7 Aug 2, 2024
ba24bfb
test: updated unit tests according to latest changes
desusai7 Aug 2, 2024
8ba2ea7
chore: updated sample app to specify permissions for face id usage
desusai7 Aug 2, 2024
b7a3d12
chore: bumped up version of expo to 51.0.24 & react-native to 0.74.4 …
desusai7 Aug 2, 2024
d750721
chore(deps-dev): bump braces from 3.0.2 to 3.0.3 (#939)
dependabot[bot] Aug 2, 2024
3a32884
chore(deps): bump fast-xml-parser from 4.2.7 to 4.4.1 in /example (#936)
dependabot[bot] Aug 2, 2024
bd057f2
chore(deps-dev): bump @testing-library/jest-dom from 6.4.5 to 6.4.8 (…
dependabot[bot] Aug 2, 2024
6f22f9d
chore(deps): bump actions/setup-node from 4.0.2 to 4.0.3 (#929)
dependabot[bot] Aug 2, 2024
f1b8f84
chore(deps): bump ws from 6.2.2 to 6.2.3 in /example (#925)
dependabot[bot] Aug 2, 2024
ed19778
chore(deps-dev): bump ws from 6.2.2 to 6.2.3 (#924)
dependabot[bot] Aug 2, 2024
a8e67e5
chore(deps): bump braces and react-native-codegen in /example (#922)
dependabot[bot] Aug 2, 2024
b5d9b74
chore(deps-dev): bump typedoc-plugin-missing-exports from 2.1.0 to 2.…
dependabot[bot] Aug 2, 2024
adb79fe
Update codeowner file with new GitHub team name (#935)
stevenwong-okta Aug 2, 2024
f12a40e
chore: updated readme and migration guides
desusai7 Aug 2, 2024
4dd6594
Merge branch 'master' into BREAKING_CHANGE_v4
desusai7 Aug 2, 2024
d398d0a
Merge branch 'master' into BREAKING_CHANGE_v4
desusai7 Aug 2, 2024
d09c3e7
chore: passing local authentication options as well to the auth0 inst…
desusai7 Aug 2, 2024
eaa61eb
chore: fixed minor issues in android module due to merge conflicts
desusai7 Aug 2, 2024
af120f9
chore: made evaluationPolicy and authenticationStrategy of LocalAuthe…
desusai7 Aug 5, 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
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
Loading