diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 31096a85..7b8721f0 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -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 ( + + {/* YOUR APP */} + + ); +}; + +export default App; +``` + ## Upgrading from v2 -> v3 ### Improvements and changes diff --git a/README.md b/README.md index 17cda06d..473a5ec3 100644 --- a/README.md +++ b/README.md @@ -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 ( + + {/* YOUR APP */} + + ); +}; + +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. diff --git a/android/build.gradle b/android/build.gradle index e3c22f37..b9c6b2e5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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) { diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.java b/android/src/main/java/com/auth0/react/A0Auth0Module.java index d98ca65d..fcb4700a 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.java +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.java @@ -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; @@ -19,9 +21,10 @@ 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; @@ -29,6 +32,7 @@ public class A0Auth0Module extends ReactContextBaseJavaModule implements Activit 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; @@ -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 @@ -72,7 +103,8 @@ 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 cleanedParameters = new HashMap<>(); for (Map.Entry entry : parameters.toHashMap().entrySet()) { if (entry.getValue() != null) { @@ -80,18 +112,19 @@ public void getCredentials(String scope, double minTtl, ReadableMap parameters, } } - this.secureCredentialsManager.getCredentials(scope, (int) minTtl, cleanedParameters, forceRefresh, new com.auth0.android.callback.Callback() { - @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() { + @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 @@ -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(); @@ -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 cleanedParameters = new HashMap<>(); for (Map.Entry entry : additionalParameters.toHashMap().entrySet()) { @@ -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() { - @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() { + @Override + public void onSuccess(Credentials result) { + ReadableMap map = CredentialsParser.toMap(result); + promise.resolve(map); + webAuthPromise = null; + } @Override public void onFailure(@NonNull AuthenticationException error) { @@ -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() { - @Override - public void onSuccess(Void credentials) { - promise.resolve(true); - } + builder.start(reactContext.getCurrentActivity(), + new com.auth0.android.callback.Callback() { + @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) { @@ -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; } } diff --git a/android/src/main/java/com/auth0/react/LocalAuthenticationOptionsParser.java b/android/src/main/java/com/auth0/react/LocalAuthenticationOptionsParser.java new file mode 100644 index 00000000..32771fdd --- /dev/null +++ b/android/src/main/java/com/auth0/react/LocalAuthenticationOptionsParser.java @@ -0,0 +1,55 @@ +package com.auth0.react; + +import com.auth0.android.authentication.storage.AuthenticationLevel; +import com.auth0.android.authentication.storage.LocalAuthenticationOptions; +import com.facebook.react.bridge.ReadableMap; + +public class LocalAuthenticationOptionsParser { + private static final String TITLE_KEY = "title"; + private static final String SUBTITLE_KEY = "subtitle"; + private static final String DESCRIPTION_KEY = "description"; + private static final String CANCEL_TITLE_KEY = "cancel"; + private static final String AUTHENTICATION_LEVEL_KEY = "authenticationLevel"; + private static final String DEVICE_CREDENTIAL_FALLBACK_KEY = "deviceCredentialFallback"; + + + public static LocalAuthenticationOptions fromMap(ReadableMap map) { + String title = map.getString(TITLE_KEY); + if (title == null) { + throw new IllegalArgumentException("LocalAuthenticationOptionsParser: fromMap: The 'title' field is required"); + } + String subtitle = map.getString(SUBTITLE_KEY); + String description = map.getString(DESCRIPTION_KEY); + String cancelTitle = map.getString(CANCEL_TITLE_KEY); + + boolean deviceCredentialFallback = map.getBoolean(DEVICE_CREDENTIAL_FALLBACK_KEY); + LocalAuthenticationOptions.Builder builder = new LocalAuthenticationOptions.Builder() + .setTitle(title) + .setSubTitle(subtitle) + .setDescription(description) + .setDeviceCredentialFallback(deviceCredentialFallback); + + if (!map.hasKey(AUTHENTICATION_LEVEL_KEY)) { + builder.setAuthenticationLevel(AuthenticationLevel.STRONG); + } else { + AuthenticationLevel level = getAuthenticationLevelFromInt(map.getInt(AUTHENTICATION_LEVEL_KEY)); + builder.setAuthenticationLevel(level); + } + if (cancelTitle != null) { + builder.setNegativeButtonText(cancelTitle); + } + return builder.build(); + } + + static AuthenticationLevel getAuthenticationLevelFromInt(int level) { + switch (level) { + case 0: + return AuthenticationLevel.STRONG; + case 1: + return AuthenticationLevel.WEAK; + default: + return AuthenticationLevel.DEVICE_CREDENTIAL; + } + } +} + diff --git a/example/ios/Auth0Example/Info.plist b/example/ios/Auth0Example/Info.plist index 7d53f236..af55b728 100644 --- a/example/ios/Auth0Example/Info.plist +++ b/example/ios/Auth0Example/Info.plist @@ -2,6 +2,8 @@ + NSFaceIDUsageDescription + Authenticate to retrieve Credentials CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 562abbf0..98da950e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - A0Auth0 (3.2.0): + - A0Auth0 (3.2.1): - Auth0 (= 2.7.2) - JWTDecode (= 3.1.0) - React-Core @@ -596,10 +596,9 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - A0Auth0: 1680e4d82cc31fb573b01d355980eb028e7bdd32 + A0Auth0: 47cb14c87d549b95b92242675ff2198ccd9dcbc5 Auth0: 28cb24cb19ebd51f0b07751f16d83b59f4019532 boost: 57d2868c099736d80fcd648bf211b4431e51a558 - CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: ddb55c55295ea51ed98aa7e2e08add2f826309d5 FBReactNativeSpec: 90fc1a90b4b7a171e0a7c20ea426c1bf6ce4399c diff --git a/example/package-lock.json b/example/package-lock.json index 6e1c0a9f..2ba1527d 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -3750,6 +3750,14 @@ "node": ">=0.10.0" } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deepmerge": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz", @@ -3967,6 +3975,11 @@ "node": ">=6" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -5678,6 +5691,23 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -6854,6 +6884,14 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7242,6 +7280,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-latest-callback": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.1.tgz", + "integrity": "sha512-QWlq8Is8BGWBf883QOEQP5HWYX/kMI+JTbJ5rdtvJLmXTIh9XoHIO3PQcmQl8BU44VKxow1kbQUHa6mQSMALDQ==", + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/example/src/App.tsx b/example/src/App.tsx index 8a5d765a..25edbbc7 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -8,7 +8,13 @@ import React from 'react'; import { Alert, Button, StyleSheet, Text, View } from 'react-native'; -import { useAuth0, Auth0Provider } from 'react-native-auth0'; +import { + useAuth0, + Auth0Provider, + LocalAuthenticationOptions, + LocalAuthenticationLevel, + LocalAuthenticationStrategy, +} from 'react-native-auth0'; import config from './auth0-configuration'; import { NavigationProp, NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; @@ -49,8 +55,23 @@ const Home = ({ navigation }: { navigation: NavigationProp }) => { }; const HomeProvider = ({ navigation }: { navigation: NavigationProp }) => { + 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, + }; + return ( - + ); diff --git a/ios/A0Auth0.m b/ios/A0Auth0.m index a5f64215..4105568c 100644 --- a/ios/A0Auth0.m +++ b/ios/A0Auth0.m @@ -26,8 +26,8 @@ - (dispatch_queue_t)methodQueue } -RCT_EXPORT_METHOD(initializeAuth0WithConfiguration:(NSString *)clientId domain:(NSString *)domain) { - [self tryAndInitializeNativeBridge:clientId domain:domain]; +RCT_EXPORT_METHOD(initializeAuth0WithConfiguration:(NSString *)clientId domain:(NSString *)domain localAuthenticationOptions:(NSDictionary*) options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [self tryAndInitializeNativeBridge:clientId domain:domain withLocalAuthenticationOptions:options resolver:resolve rejecter:reject]; } RCT_EXPORT_METHOD(saveCredentials:(NSDictionary *)credentials resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -79,8 +79,8 @@ - (BOOL)checkHasValidNativeBridgeInstance:(NSString*) clientId domain:(NSString return valid; } -- (void)tryAndInitializeNativeBridge:(NSString *)clientId domain:(NSString *)domain { - NativeBridge *bridge = [[NativeBridge alloc] initWithClientId: clientId domain: domain]; +- (void)tryAndInitializeNativeBridge:(NSString *)clientId domain:(NSString *)domain withLocalAuthenticationOptions:(NSDictionary*) options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { + NativeBridge *bridge = [[NativeBridge alloc] initWithClientId:clientId domain:domain localAuthenticationOptions:options resolve:resolve reject:reject]; self.nativeBridge = bridge; } diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift index 78f4e50e..3f04481f 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -24,18 +24,34 @@ public class NativeBridge: NSObject { static let dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; static let credentialsManagerErrorCode = "a0.invalid_state.credential_manager_exception" + static let biometricsAuthenticationErrorCode = "a0.invalid_options_biometrics_authentication" var credentialsManager: CredentialsManager var clientId: String var domain: String - @objc public init(clientId: String, domain: String) { + @objc public init(clientId: String, domain: String, localAuthenticationOptions: [String: Any]?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let auth0 = Auth0 .authentication(clientId: clientId, domain: domain) self.clientId = clientId self.domain = domain self.credentialsManager = CredentialsManager(authentication: auth0) super.init() + if let localAuthenticationOptions = localAuthenticationOptions { + if let title = localAuthenticationOptions["title"] as? String { + var evaluationPolicy = LAPolicy.deviceOwnerAuthenticationWithBiometrics + if let evaluationPolicyInt = localAuthenticationOptions["evaluationPolicy"] as? Int { + evaluationPolicy = convert(policyInt: evaluationPolicyInt) + } + self.credentialsManager.enableBiometrics(withTitle: title, cancelTitle: localAuthenticationOptions["cancelTitle"] as? String, fallbackTitle: localAuthenticationOptions["fallbackTitle"] as? String, evaluationPolicy: evaluationPolicy) + resolve(true) + return + } else { + reject(NativeBridge.biometricsAuthenticationErrorCode, "Missing mandatory property title in LocalAuthenticationOptions, hence biometrics authentication cannot be enabled", nil) + return + } + } + resolve(true) } @objc public func webAuth(state: String?, redirectUri: String, nonce: String?, audience: String?, scope: String?, connection: String?, maxAge: Int, organization: String?, invitationUrl: String?, leeway: Int, ephemeralSession: Bool, safariViewControllerPresentationStyle: Int, additionalParameters: [String: String], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { diff --git a/src/auth0.ts b/src/auth0.ts index aabcc9b8..16e0e9e6 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -3,6 +3,8 @@ import CredentialsManager from './credentials-manager'; import Users from './management/users'; import { Telemetry } from './networking/telemetry'; import WebAuth from './webauth'; +import LocalAuthenticationOptions from './credentials-manager/localAuthenticationOptions'; +import addDefaultLocalAuthOptions from './utils/addDefaultLocalAuthOptions'; /** * Auth0 for React Native client @@ -21,6 +23,7 @@ class Auth0 { * @param {String} options.telemetry The telemetry information to be sent along with the requests * @param {String} options.token Token to be used for Management APIs * @param {String} options.timeout Timeout to be set for requests. + * @param {LocalAuthenticationOptions} options.localAuthenticationOptions The options for configuring the display of local authentication prompt, authentication level (Android only) and evaluation policy (iOS only). */ constructor(options: { domain: string; @@ -28,11 +31,19 @@ class Auth0 { telemetry?: Telemetry; token?: string; timeout?: number; + localAuthenticationOptions?: LocalAuthenticationOptions; }) { const { domain, clientId, ...extras } = options; + const localAuthenticationOptions = options.localAuthenticationOptions + ? addDefaultLocalAuthOptions(options.localAuthenticationOptions) + : undefined; this.auth = new Auth({ baseUrl: domain, clientId, ...extras }); - this.webAuth = new WebAuth(this.auth); - this.credentialsManager = new CredentialsManager(domain, clientId); + this.webAuth = new WebAuth(this.auth, localAuthenticationOptions); + this.credentialsManager = new CredentialsManager( + domain, + clientId, + localAuthenticationOptions + ); this.options = options; } diff --git a/src/credentials-manager/__tests__/credentials-manager.spec.js b/src/credentials-manager/__tests__/credentials-manager.spec.js index a9a936dd..73c1fd6a 100644 --- a/src/credentials-manager/__tests__/credentials-manager.spec.js +++ b/src/credentials-manager/__tests__/credentials-manager.spec.js @@ -168,24 +168,4 @@ describe('credentials manager tests', () => { newNativeModule.mockRestore(); }); }); - - describe('test enabling local authentication', () => { - it('enable local authentication for iOS', async () => { - Platform.OS = 'ios'; - const newNativeModule = jest - .spyOn(credentialsManager.Auth0Module, 'enableLocalAuthentication') - .mockImplementation(() => {}); - await expect(credentialsManager.requireLocalAuthentication()).resolves; - newNativeModule.mockRestore(); - }); - - it('enable local authentication for Android', async () => { - Platform.OS = 'android'; - const newNativeModule = jest - .spyOn(credentialsManager.Auth0Module, 'enableLocalAuthentication') - .mockImplementation(() => {}); - await expect(credentialsManager.requireLocalAuthentication()).resolves; - newNativeModule.mockRestore(); - }); - }); }); diff --git a/src/credentials-manager/index.ts b/src/credentials-manager/index.ts index 9b4f7fd7..c02a503b 100644 --- a/src/credentials-manager/index.ts +++ b/src/credentials-manager/index.ts @@ -1,21 +1,27 @@ -import { NativeModules, Platform } from 'react-native'; +import { NativeModules } from 'react-native'; import CredentialsManagerError from './credentialsManagerError'; -import LocalAuthenticationStrategy from './localAuthenticationStrategy'; import { Credentials } from '../types'; import { Auth0Module } from 'src/internal-types'; import { _ensureNativeModuleIsInitializedWithConfiguration } from '../utils/nativeHelper'; +import LocalAuthenticationOptions from './localAuthenticationOptions'; class CredentialsManager { private domain; private clientId; private Auth0Module: Auth0Module; + private localAuthenticationOptions?: LocalAuthenticationOptions; /** * @ignore */ - constructor(domain: string, clientId: string) { + constructor( + domain: string, + clientId: string, + localAuthenticationOptions?: LocalAuthenticationOptions + ) { this.domain = domain; this.clientId = clientId; + this.localAuthenticationOptions = localAuthenticationOptions; this.Auth0Module = NativeModules.A0Auth0; } @@ -38,7 +44,8 @@ class CredentialsManager { await _ensureNativeModuleIsInitializedWithConfiguration( this.Auth0Module, this.clientId, - this.domain + this.domain, + this.localAuthenticationOptions ); return await this.Auth0Module.saveCredentials(credentials); } catch (e) { @@ -69,7 +76,8 @@ class CredentialsManager { await _ensureNativeModuleIsInitializedWithConfiguration( this.Auth0Module, this.clientId, - this.domain + this.domain, + this.localAuthenticationOptions ); return this.Auth0Module.getCredentials( scope, @@ -86,47 +94,6 @@ class CredentialsManager { } } - /** - * Enables Local Authentication (PIN, Biometric, Swipe etc) to get the credentials - * - * @param title the text to use as title in the authentication screen. Passing null will result in using the OS's default value in Android and "Please authenticate to continue" in iOS. - * @param description **Android only:** the text to use as description in the authentication screen. On some Android versions it might not be shown. Passing null will result in using the OS's default value. - * @param cancelTitle **iOS only:** the cancel message to display on the local authentication prompt. - * @param fallbackTitle **iOS only:** the fallback message to display on the local authentication prompt after a failed match. - * @param strategy **iOS only:** the evaluation policy to use when accessing the credentials. Defaults to LocalAuthenticationStrategy.deviceOwnerWithBiometrics. - */ - async requireLocalAuthentication( - title?: string, - description?: string, - cancelTitle?: string, - fallbackTitle?: string, - strategy = LocalAuthenticationStrategy.deviceOwnerWithBiometrics - ): Promise { - try { - await _ensureNativeModuleIsInitializedWithConfiguration( - this.Auth0Module, - this.clientId, - this.domain - ); - if (Platform.OS === 'ios') { - await this.Auth0Module.enableLocalAuthentication( - title, - cancelTitle, - fallbackTitle, - strategy - ); - } else { - await this.Auth0Module.enableLocalAuthentication(title, description); - } - } catch (e) { - const json = { - error: 'a0.credential_manager.invalid', - error_description: e.message, - }; - throw new CredentialsManagerError({ json, status: 0 }); - } - } - /** * Returns whether this manager contains a valid non-expired pair of credentials. * @@ -137,7 +104,8 @@ class CredentialsManager { await _ensureNativeModuleIsInitializedWithConfiguration( this.Auth0Module, this.clientId, - this.domain + this.domain, + this.localAuthenticationOptions ); return await this.Auth0Module.hasValidCredentials(minTtl); } @@ -149,7 +117,8 @@ class CredentialsManager { await _ensureNativeModuleIsInitializedWithConfiguration( this.Auth0Module, this.clientId, - this.domain + this.domain, + this.localAuthenticationOptions ); return this.Auth0Module.clearCredentials(); } diff --git a/src/credentials-manager/localAuthenticationLevel.ts b/src/credentials-manager/localAuthenticationLevel.ts new file mode 100644 index 00000000..88628fd3 --- /dev/null +++ b/src/credentials-manager/localAuthenticationLevel.ts @@ -0,0 +1,20 @@ +/** + * **Used for Android only:** The level of local authentication required to access the credentials. Defaults to LocalAuthenticationLevel.strong. + */ + +enum LocalAuthenticationLevel { + /** + * Any biometric (e.g. fingerprint, iris, or face) on the device that meets or exceeds the requirements for Class 3 (formerly Strong), as defined by the Android CDD. + */ + strong = 0, + /** + * 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. + */ + weak, + /** + * The non-biometric credential used to secure the device (i. e. PIN, pattern, or password). + */ + deviceCredential, +} + +export default LocalAuthenticationLevel; diff --git a/src/credentials-manager/localAuthenticationOptions.ts b/src/credentials-manager/localAuthenticationOptions.ts new file mode 100644 index 00000000..ce56a84b --- /dev/null +++ b/src/credentials-manager/localAuthenticationOptions.ts @@ -0,0 +1,43 @@ +import LocalAuthenticationLevel from './localAuthenticationLevel'; +import LocalAuthenticationStrategy from './localAuthenticationStrategy'; + +/** + * The options for configuring the display of local authentication prompt, authentication level (Android only) and evaluation policy (iOS only). + */ + +interface LocalAuthenticationOptions { + /** + * The title of the authentication prompt. **Applicable for both Android and iOS**. + */ + title: String; + /** + * The subtitle of the authentication prompt. **Applicable for Android only.** + */ + subtitle?: String; + /** + * The description of the authentication prompt. **Applicable for Android only.** + */ + description?: String; + /** + * The cancel button title of the authentication prompt. **Applicable for both Android and iOS.** + */ + cancelTitle?: String; + /** + * The evaluation policy to use when prompting the user for authentication. Defaults to LocalAuthenticationStrategy.deviceOwnerWithBiometrics. **Applicable for iOS only.** + */ + evaluationPolicy?: LocalAuthenticationStrategy; + /** + * The fallback button title of the authentication prompt. **Applicable for iOS only.** + */ + fallbackTitle?: String; + /** + * The authentication level to use when prompting the user for authentication. Defaults to LocalAuthenticationLevel.strong. **Applicable for Android only.** + */ + authenticationLevel?: LocalAuthenticationLevel; + /** + * Should the user be given the option to authenticate with their device PIN, pattern, or password instead of a biometric. **Applicable for Android only.** + */ + deviceCredentialFallback?: Boolean; +} + +export default LocalAuthenticationOptions; diff --git a/src/hooks/__tests__/use-auth0.spec.jsx b/src/hooks/__tests__/use-auth0.spec.jsx index a3884d2c..e172a934 100644 --- a/src/hooks/__tests__/use-auth0.spec.jsx +++ b/src/hooks/__tests__/use-auth0.spec.jsx @@ -68,7 +68,6 @@ const mockAuth0 = { }, credentialsManager: { getCredentials: jest.fn().mockResolvedValue(mockCredentials), - requireLocalAuthentication: jest.fn().mockResolvedValue(), clearCredentials: jest.fn().mockResolvedValue(), saveCredentials: jest.fn().mockResolvedValue(), hasValidCredentials: jest.fn(), @@ -1051,61 +1050,6 @@ describe('The useAuth0 hook', () => { expect(result.current.error).toEqual(thrownError); }); - it('can require local authentication', async () => { - const { result } = renderHook(() => useAuth0(), { - wrapper, - }); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - result.current.requireLocalAuthentication(); - - expect( - mockAuth0.credentialsManager.requireLocalAuthentication - ).toHaveBeenCalled(); - }); - - it('can require local authentication with options', async () => { - const { result } = renderHook(() => useAuth0(), { - wrapper, - }); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - result.current.requireLocalAuthentication( - 'title', - 'description', - 'cancel', - 'fallback', - LocalAuthenticationStrategy.deviceOwner - ); - - expect( - mockAuth0.credentialsManager.requireLocalAuthentication - ).toHaveBeenCalledWith( - 'title', - 'description', - 'cancel', - 'fallback', - LocalAuthenticationStrategy.deviceOwner - ); - }); - - it('dispatches an error when requireLocalAuthentication fails', async () => { - const { result } = renderHook(() => useAuth0(), { - wrapper, - }); - const thrownError = new Error('requireLocalAuthentication failed'); - - mockAuth0.credentialsManager.requireLocalAuthentication.mockRejectedValue( - thrownError - ); - - result.current.requireLocalAuthentication(); - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.error).toEqual(thrownError); - }); - it('calls hasValidCredentials with correct parameters', async () => { const { result } = renderHook(() => useAuth0(), { wrapper, diff --git a/src/hooks/auth0-context.ts b/src/hooks/auth0-context.ts index 5c77aa88..fa24ba04 100644 --- a/src/hooks/auth0-context.ts +++ b/src/hooks/auth0-context.ts @@ -15,7 +15,6 @@ import { PasswordlessWithSMSOptions, ClearSessionOptions, } from '../types'; -import LocalAuthenticationStrategy from '../credentials-manager/localAuthenticationStrategy'; export interface Auth0ContextInterface extends AuthState { @@ -106,21 +105,6 @@ export interface Auth0ContextInterface * Clears the user's credentials without clearing their web session and logs them out. */ clearCredentials: () => Promise; - /** - * Enables Local Authentication (PIN, Biometric, Swipe etc) to get the credentials. See {@link CredentialsManager#requireLocalAuthentication} - * @param title the text to use as title in the authentication screen. Passing null will result in using the OS's default value in Android and "Please authenticate to continue" in iOS. - * @param description **Android only:** the text to use as description in the authentication screen. On some Android versions it might not be shown. Passing null will result in using the OS's default value. - * @param cancelTitle **iOS only:** the cancel message to display on the local authentication prompt. - * @param fallbackTitle **iOS only:** the fallback message to display on the local authentication prompt after a failed match. - * @param strategy **iOS only:** the evaluation policy to use when accessing the credentials. Defaults to LocalAuthenticationStrategy.deviceOwnerWithBiometrics. - */ - requireLocalAuthentication: ( - title?: string, - description?: string, - cancelTitle?: string, - fallbackTitle?: string, - strategy?: LocalAuthenticationStrategy - ) => Promise; } export interface AuthState { @@ -159,7 +143,6 @@ const initialContext = { clearSession: stub, getCredentials: stub, clearCredentials: stub, - requireLocalAuthentication: stub, }; const Auth0Context = createContext(initialContext); diff --git a/src/hooks/auth0-provider.tsx b/src/hooks/auth0-provider.tsx index 534a571d..d07df5a3 100644 --- a/src/hooks/auth0-provider.tsx +++ b/src/hooks/auth0-provider.tsx @@ -26,9 +26,9 @@ import { WebAuthorizeOptions, WebAuthorizeParameters, } from '../types'; -import LocalAuthenticationStrategy from '../credentials-manager/localAuthenticationStrategy'; import { CustomJwtPayload } from '../internal-types'; import { convertUser } from '../utils/userConversion'; +import LocalAuthenticationOptions from 'src/credentials-manager/localAuthenticationOptions'; const initialState = { user: null, @@ -71,10 +71,15 @@ const finalizeScopeParam = (inputScopes?: string) => { const Auth0Provider = ({ domain, clientId, + localAuthenticationOptions, children, -}: PropsWithChildren<{ domain: string; clientId: string }>) => { +}: PropsWithChildren<{ + domain: string; + clientId: string; + localAuthenticationOptions?: LocalAuthenticationOptions; +}>) => { const client = useMemo( - () => new Auth0({ domain, clientId }), + () => new Auth0({ domain, clientId, localAuthenticationOptions }), [domain, clientId] ); const [state, dispatch] = useReducer(reducer, initialState); @@ -313,30 +318,6 @@ const Auth0Provider = ({ } }, [client]); - const requireLocalAuthentication = useCallback( - async ( - title?: string, - description?: string, - cancelTitle?: string, - fallbackTitle?: string, - strategy = LocalAuthenticationStrategy.deviceOwnerWithBiometrics - ) => { - try { - await client.credentialsManager.requireLocalAuthentication( - title, - description, - cancelTitle, - fallbackTitle, - strategy - ); - } catch (error) { - dispatch({ type: 'ERROR', error }); - return; - } - }, - [client.credentialsManager] - ); - const contextValue = useMemo( () => ({ ...state, @@ -353,7 +334,6 @@ const Auth0Provider = ({ clearSession, getCredentials, clearCredentials, - requireLocalAuthentication, }), [ state, @@ -370,7 +350,6 @@ const Auth0Provider = ({ clearSession, getCredentials, clearCredentials, - requireLocalAuthentication, ] ); diff --git a/src/index.ts b/src/index.ts index d317099b..1cfc00aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,8 @@ export { TimeoutError } from './utils/fetchWithTimeout'; export { default as useAuth0 } from './hooks/use-auth0'; export { default as Auth0Provider } from './hooks/auth0-provider'; +export { default as LocalAuthenticationOptions } from './credentials-manager/localAuthenticationOptions'; +export { default as LocalAuthenticationLevel } from './credentials-manager/localAuthenticationLevel'; export { default as LocalAuthenticationStrategy } from './credentials-manager/localAuthenticationStrategy'; export * from './types'; diff --git a/src/internal-types.ts b/src/internal-types.ts index c5a8618b..1d065325 100644 --- a/src/internal-types.ts +++ b/src/internal-types.ts @@ -1,6 +1,6 @@ import { JwtPayload } from 'jwt-decode'; -import LocalAuthenticationStrategy from './credentials-manager/localAuthenticationStrategy'; import { Credentials } from './types'; +import LocalAuthenticationOptions from './credentials-manager/localAuthenticationOptions'; export type CredentialsResponse = { id_token: string; @@ -98,14 +98,6 @@ export type Auth0Module = { parameters?: Record, forceRefresh?: boolean ) => Promise; - enableLocalAuthentication: - | (( - title?: string, - cancelTitle?: string, - fallbackTitle?: string, - strategy?: LocalAuthenticationStrategy - ) => Promise) - | ((title?: string, description?: string) => Promise); hasValidCredentials: (minTtl?: number) => Promise; clearCredentials: () => Promise; hasValidAuth0InstanceWithConfiguration: ( @@ -114,7 +106,8 @@ export type Auth0Module = { ) => Promise; initializeAuth0WithConfiguration: ( clientId: string, - domain: string + domain: string, + localAuthenticationOptions?: LocalAuthenticationOptions ) => Promise; }; diff --git a/src/utils/__tests__/addDefaultLocalAuthOptions.spec.js b/src/utils/__tests__/addDefaultLocalAuthOptions.spec.js new file mode 100644 index 00000000..b4dbcf32 --- /dev/null +++ b/src/utils/__tests__/addDefaultLocalAuthOptions.spec.js @@ -0,0 +1,38 @@ +import LocalAuthenticationLevel from '../../credentials-manager/localAuthenticationLevel'; +import addDefaultLocalAuthOptions from '../addDefaultLocalAuthOptions'; +import LocalAuthenticationStrategy from '../../credentials-manager/localAuthenticationStrategy'; + +describe('addDefaultLocalAuthenticationOptions', () => { + it('should return default options when no options are provided', () => { + const localAuthOptions = { title: 'Please authenticate' }; + const result = addDefaultLocalAuthOptions(localAuthOptions); + expect(result).toEqual({ + title: 'Please authenticate', + authenticationLevel: LocalAuthenticationLevel.strong, + evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics, + }); + }); + + it('should override default options with provided options', () => { + const localAuthOptions = { + title: 'Please authenticate', + authenticationLevel: LocalAuthenticationLevel.deviceCredential, + evaluationPolicy: LocalAuthenticationStrategy.deviceOwner, + }; + const result = addDefaultLocalAuthOptions(localAuthOptions); + expect(result).toEqual(localAuthOptions); + }); + + it('should merge default options with partially provided options', () => { + const options = { + title: 'Please authenticate', + authenticationLevel: LocalAuthenticationLevel.deviceCredential, + }; + const result = addDefaultLocalAuthOptions(options); + expect(result).toEqual({ + title: 'Please authenticate', + authenticationLevel: LocalAuthenticationLevel.deviceCredential, + evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics, + }); + }); +}); diff --git a/src/utils/addDefaultLocalAuthOptions.ts b/src/utils/addDefaultLocalAuthOptions.ts new file mode 100644 index 00000000..7c80fef5 --- /dev/null +++ b/src/utils/addDefaultLocalAuthOptions.ts @@ -0,0 +1,19 @@ +import LocalAuthenticationOptions from '../credentials-manager/localAuthenticationOptions'; +import LocalAuthenticationStrategy from '../credentials-manager/localAuthenticationStrategy'; +import LocalAuthenticationLevel from '../credentials-manager/localAuthenticationLevel'; + +const defaultLocalAuthOptions = { + evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics, + authenticationLevel: LocalAuthenticationLevel.strong, +}; + +function addDefaultLocalAuthOptions( + localAuthenticationOptions: LocalAuthenticationOptions +): LocalAuthenticationOptions { + return { + ...defaultLocalAuthOptions, + ...localAuthenticationOptions, + }; +} + +export default addDefaultLocalAuthOptions; diff --git a/src/utils/nativeHelper.ts b/src/utils/nativeHelper.ts index 487b7784..0e3a112b 100644 --- a/src/utils/nativeHelper.ts +++ b/src/utils/nativeHelper.ts @@ -1,16 +1,22 @@ +import LocalAuthenticationOptions from 'src/credentials-manager/localAuthenticationOptions'; import { Auth0Module } from 'src/internal-types'; //private export async function _ensureNativeModuleIsInitializedWithConfiguration( nativeModule: Auth0Module, clientId: string, - domain: string + domain: string, + localAuthenticationOptions?: LocalAuthenticationOptions ) { const hasValid = await nativeModule.hasValidAuth0InstanceWithConfiguration( clientId, domain ); if (!hasValid) { - await nativeModule.initializeAuth0WithConfiguration(clientId, domain); + await nativeModule.initializeAuth0WithConfiguration( + clientId, + domain, + localAuthenticationOptions + ); } } diff --git a/src/webauth/__tests__/agent.spec.js b/src/webauth/__tests__/agent.spec.js index a5b251c0..6c219feb 100644 --- a/src/webauth/__tests__/agent.spec.js +++ b/src/webauth/__tests__/agent.spec.js @@ -2,6 +2,12 @@ import * as nativeUtils from '../../utils/nativeHelper'; import Agent from '../agent'; import { NativeModules, Platform, Linking } from 'react-native'; +const localAuthenticationOptions = { + title: 'Authenticate With Your Biometrics', + evaluationPolicy: 1, + authenticationLevel: 0, +}; + jest.mock('react-native', () => { // Require the original module to not be mocked... return { @@ -58,9 +64,15 @@ describe('Agent', () => { clientId: clientId, domain: domain, }, - { customScheme: 'test' } + { customScheme: 'test' }, + localAuthenticationOptions + ); + expect(mock).toBeCalledWith( + NativeModules.A0Auth0, + clientId, + domain, + localAuthenticationOptions ); - expect(mock).toBeCalledWith(NativeModules.A0Auth0, clientId, domain); }); it('should ensure login is called with proper parameters', async () => { @@ -91,9 +103,15 @@ describe('Agent', () => { ephemeralSession: true, safariViewControllerPresentationStyle: 0, additionalParameters: { test: 'test' }, - } + }, + localAuthenticationOptions + ); + expect(mock).toBeCalledWith( + NativeModules.A0Auth0, + clientId, + domain, + localAuthenticationOptions ); - expect(mock).toBeCalledWith(NativeModules.A0Auth0, clientId, domain); expect(mockLogin).toBeCalledWith( 'test', 'test://test.com/ios/com.my.app/callback', @@ -128,9 +146,15 @@ describe('Agent', () => { }, { redirectUrl: 'redirect://redirect.com', - } + }, + localAuthenticationOptions + ); + expect(mock).toBeCalledWith( + NativeModules.A0Auth0, + clientId, + domain, + localAuthenticationOptions ); - expect(mock).toBeCalledWith(NativeModules.A0Auth0, clientId, domain); expect(mockLogin).toBeCalledWith( 'com.my.app.auth0', 'redirect://redirect.com', @@ -173,9 +197,15 @@ describe('Agent', () => { clientId: clientId, domain: domain, }, - { customScheme: 'test' } + { customScheme: 'test' }, + localAuthenticationOptions + ); + expect(mock).toBeCalledWith( + NativeModules.A0Auth0, + clientId, + domain, + localAuthenticationOptions ); - expect(mock).toBeCalledWith(NativeModules.A0Auth0, clientId, domain); }); it('should ensure logout is called with proper parameters', async () => { @@ -195,9 +225,15 @@ describe('Agent', () => { { customScheme: 'test', federated: true, - } + }, + localAuthenticationOptions + ); + expect(mock).toBeCalledWith( + NativeModules.A0Auth0, + clientId, + domain, + localAuthenticationOptions ); - expect(mock).toBeCalledWith(NativeModules.A0Auth0, clientId, domain); expect(mockLogin).toBeCalledWith( 'test', true, @@ -221,9 +257,15 @@ describe('Agent', () => { }, { returnToUrl: 'redirect://redirect.com', - } + }, + localAuthenticationOptions + ); + expect(mock).toBeCalledWith( + NativeModules.A0Auth0, + clientId, + domain, + localAuthenticationOptions ); - expect(mock).toBeCalledWith(NativeModules.A0Auth0, clientId, domain); expect(mockLogin).toBeCalledWith( 'com.my.app.auth0', false, diff --git a/src/webauth/__tests__/webauth.spec.js b/src/webauth/__tests__/webauth.spec.js index f2098c0d..45092a45 100644 --- a/src/webauth/__tests__/webauth.spec.js +++ b/src/webauth/__tests__/webauth.spec.js @@ -11,7 +11,12 @@ describe('WebAuth', () => { const domain = 'auth0.com'; const baseUrl = 'https://' + domain; const auth = new Auth({ baseUrl: baseUrl, clientId: clientId }); - const webauth = new WebAuth(auth); + const localAuthenticationOptions = { + title: 'Authenticate With Your Biometrics', + evaluationPolicy: 1, + authenticationLevel: 0, + }; + const webauth = new WebAuth(auth, localAuthenticationOptions); describe('authorize', () => { it('should authorize with provided parameters', async () => { @@ -49,7 +54,12 @@ describe('WebAuth', () => { ).resolves.toMatchSnapshot(); expect(showMock).toHaveBeenCalledWith( { clientId, domain }, - { ...parameters, ...options, safariViewControllerPresentationStyle: -2 } + { + ...parameters, + ...options, + safariViewControllerPresentationStyle: -2, + }, + localAuthenticationOptions ); showMock.mockRestore(); }); @@ -88,7 +98,8 @@ describe('WebAuth', () => { ).resolves.toMatchSnapshot(); expect(showMock).toHaveBeenCalledWith( { clientId, domain }, - { ...parameters, ...options, safariViewControllerPresentationStyle: 0 } + { ...parameters, ...options, safariViewControllerPresentationStyle: 0 }, + localAuthenticationOptions ); showMock.mockRestore(); }); @@ -130,7 +141,8 @@ describe('WebAuth', () => { ...parameters, ...options, safariViewControllerPresentationStyle: undefined, - } + }, + localAuthenticationOptions ); showMock.mockRestore(); }); @@ -173,7 +185,8 @@ describe('WebAuth', () => { ...parameters, ...options, safariViewControllerPresentationStyle: undefined, - } + }, + localAuthenticationOptions ); showMock.mockRestore(); }); @@ -216,7 +229,8 @@ describe('WebAuth', () => { ...parameters, ...options, safariViewControllerPresentationStyle: 0, - } + }, + localAuthenticationOptions ); showMock.mockRestore(); }); @@ -234,10 +248,15 @@ describe('WebAuth', () => { const showMock = jest .spyOn(webauth.agent, 'logout') .mockImplementation(() => Promise.resolve()); - await webauth.clearSession(parameters, options); + await webauth.clearSession( + parameters, + options, + localAuthenticationOptions + ); expect(showMock).toHaveBeenCalledWith( { clientId, domain }, - { ...parameters, ...options } + { ...parameters, ...options }, + localAuthenticationOptions ); showMock.mockRestore(); }); diff --git a/src/webauth/agent.ts b/src/webauth/agent.ts index 2eb26cf2..25b6c234 100644 --- a/src/webauth/agent.ts +++ b/src/webauth/agent.ts @@ -12,12 +12,14 @@ import { AgentParameters, Auth0Module, } from 'src/internal-types'; +import LocalAuthenticationOptions from 'src/credentials-manager/localAuthenticationOptions'; const A0Auth0: Auth0Module = NativeModules.A0Auth0; class Agent { async login( parameters: AgentParameters, - options: AgentLoginOptions + options: AgentLoginOptions, + localAuthenticationOptions?: LocalAuthenticationOptions ): Promise { let linkSubscription: EmitterSubscription | null = null; if (!NativeModules.A0Auth0) { @@ -45,7 +47,8 @@ class Agent { await _ensureNativeModuleIsInitializedWithConfiguration( A0Auth0, parameters.clientId, - parameters.domain + parameters.domain, + localAuthenticationOptions ); let scheme = this.getScheme( options.useLegacyCallbackUrl ?? false, @@ -80,7 +83,8 @@ class Agent { async logout( parameters: AgentParameters, - options: AgentLogoutOptions + options: AgentLogoutOptions, + localAuthenticationOptions?: LocalAuthenticationOptions ): Promise { if (!NativeModules.A0Auth0) { return Promise.reject( @@ -99,7 +103,8 @@ class Agent { await _ensureNativeModuleIsInitializedWithConfiguration( NativeModules.A0Auth0, parameters.clientId, - parameters.domain + parameters.domain, + localAuthenticationOptions ); return A0Auth0.webAuthLogout(scheme, federated, redirectUri); } diff --git a/src/webauth/index.ts b/src/webauth/index.ts index c4b22030..b9d7ae38 100644 --- a/src/webauth/index.ts +++ b/src/webauth/index.ts @@ -11,6 +11,7 @@ import { import Auth from '../auth'; import { object } from 'prop-types'; +import LocalAuthenticationOptions from 'src/credentials-manager/localAuthenticationOptions'; /** * Helper to perform Auth against Auth0 hosted login page @@ -24,15 +25,20 @@ class WebAuth { private domain: string; private clientId: string; private agent: Agent; + private localAuthenticationOptions?: LocalAuthenticationOptions; /** * @ignore */ - constructor(auth: Auth) { + constructor( + auth: Auth, + localAuthenticationOptions?: LocalAuthenticationOptions + ) { const { clientId, domain } = auth; this.domain = domain; this.clientId = clientId; this.agent = new Agent(); + this.localAuthenticationOptions = localAuthenticationOptions; } /** @@ -60,7 +66,8 @@ class WebAuth { ...parameters, safariViewControllerPresentationStyle: presentationStyle, ...options, - } + }, + this.localAuthenticationOptions ); } @@ -79,7 +86,8 @@ class WebAuth { { ...parameters, ...options, - } + }, + this.localAuthenticationOptions ); } }