diff --git a/A0Auth0.podspec b/A0Auth0.podspec
index 8becd40e..39c730a4 100644
--- a/A0Auth0.podspec
+++ b/A0Auth0.podspec
@@ -16,9 +16,7 @@ Pod::Spec.new do |s|
s.source_files = 'ios/**/*.{h,m,mm,swift}'
s.requires_arc = true
- s.dependency 'Auth0', '2.10'
- s.dependency 'JWTDecode', '3.2.0'
- s.dependency 'SimpleKeychain', '1.2.0'
+ s.dependency 'Auth0', '2.13'
install_modules_dependencies(s)
end
diff --git a/README.md b/README.md
index e0185cde..685462e9 100644
--- a/README.md
+++ b/README.md
@@ -625,7 +625,7 @@ The options for configuring the display of local authentication prompt, authenti
> :warning: You need a real device to test Local Authentication for iOS. Local Authentication is not available in simulators.
-#### Credentials Manager errors
+### Credentials Manager errors
The Credentials Manager will only throw `CredentialsManagerError` exceptions. You can find more information in the details property of the exception.
@@ -649,22 +649,81 @@ try {
}
```
-_Note_ : We have platform agnostic error codes available only for `CredentialsManagerError` as of now.
-
-| Generic Error Code | Corresponding Error Code in Android | Corresponding Error Code in iOS |
-| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- |
-| `INVALID_CREDENTIALS` | `INVALID_CREDENTIALS` | |
-| `NO_CREDENTIALS` | `NO_CREDENTIALS` | `noCredentials` |
-| `NO_REFRESH_TOKEN` | `NO_REFRESH_TOKEN` | `noRefreshToken` |
-| `RENEW_FAILED` | `RENEW_FAILED` | `renewFailed` |
-| `STORE_FAILED` | `STORE_FAILED` | `storeFailed` |
-| `REVOKE_FAILED` | `REVOKE_FAILED` | `revokeFailed` |
-| `LARGE_MIN_TTL` | `LARGE_MIN_TTL` | `largeMinTTL` |
-| `INCOMPATIBLE_DEVICE` | `INCOMPATIBLE_DEVICE` | |
-| `CRYPTO_EXCEPTION` | `CRYPTO_EXCEPTION` | |
-| `BIOMETRICS_FAILED` | OneOf
`BIOMETRIC_NO_ACTIVITY`,`BIOMETRIC_ERROR_STATUS_UNKNOWN`,`BIOMETRIC_ERROR_UNSUPPORTED`,
`BIOMETRIC_ERROR_HW_UNAVAILABLE`,`BIOMETRIC_ERROR_NONE_ENROLLED`,`BIOMETRIC_ERROR_NO_HARDWARE`,
`BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED`,`BIOMETRIC_AUTHENTICATION_CHECK_FAILED`,
`BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE` | `biometricsFailed` |
-| `NO_NETWORK` | `NO_NETWORK` | |
-| `API_ERROR` | `API_ERROR` | |
+| Generic Error Code | Android Native Error | iOS Native Error | Web Error Code |
+| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | ----------------------------------------------------------- |
+| `INVALID_CREDENTIALS` | `INVALID_CREDENTIALS` | | |
+| `NO_CREDENTIALS` | `NO_CREDENTIALS` | `noCredentials` | `login_required` |
+| `NO_REFRESH_TOKEN` | `NO_REFRESH_TOKEN` | `noRefreshToken` |
+| `RENEW_FAILED` | `RENEW_FAILED` | `renewFailed` | `missing_refresh_token`, `invalid_grant`,`consent_required` |
+| `STORE_FAILED` | `STORE_FAILED` | `storeFailed` | |
+| `REVOKE_FAILED` | `REVOKE_FAILED` | `revokeFailed` | |
+| `LARGE_MIN_TTL` | `LARGE_MIN_TTL` | `largeMinTTL` | |
+| `INCOMPATIBLE_DEVICE` | `INCOMPATIBLE_DEVICE` | | |
+| `CRYPTO_EXCEPTION` | `CRYPTO_EXCEPTION` | | |
+| `BIOMETRICS_FAILED` | OneOf
`BIOMETRIC_NO_ACTIVITY`,`BIOMETRIC_ERROR_STATUS_UNKNOWN`,`BIOMETRIC_ERROR_UNSUPPORTED`,
`BIOMETRIC_ERROR_HW_UNAVAILABLE`,`BIOMETRIC_ERROR_NONE_ENROLLED`,`BIOMETRIC_ERROR_NO_HARDWARE`,
`BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED`,`BIOMETRIC_AUTHENTICATION_CHECK_FAILED`,
`BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE` | `biometricsFailed` | |
+| `NO_NETWORK` | `NO_NETWORK` | | |
+| `API_ERROR` | `API_ERROR` | | |
+
+### WebAuth errors
+
+**Before (Platform-Specific Codes)**
+
+```javascript
+// Old way: required checking Platform.OS and different error codes
+import { Platform } from 'react-native';
+
+try {
+ await auth0.webAuth.authorize();
+} catch (e) {
+ const isCancelled =
+ Platform.OS === 'ios'
+ ? e.code === 'USER_CANCELLED'
+ : e.code === 'a0.session.user_cancelled';
+
+ if (isCancelled) {
+ console.log('User cancelled the login.');
+ } else {
+ console.error(e);
+ }
+}
+```
+
+**After (Platform-Agnostic and Typed)**
+
+```javascript
+// New way: use 'instanceof' and the 'type' property
+import { WebAuthError } from 'react-native-auth0';
+
+try {
+ await auth0.webAuth.authorize();
+} catch (e) {
+ if (e instanceof WebAuthError && e.type === 'USER_CANCELLED') {
+ console.log('User cancelled the login.');
+ } else {
+ // Handle other errors
+ console.error(e);
+ }
+}
+```
+
+| Platform-Agnostic | Description | Android Native Error | iOS Native Error | Web Error Code |
+| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------- | ------------------------------------ |
+| `USER_CANCELLED` | The user actively cancelled the web authentication flow. | `a0.session.user_cancelled` | `USER_CANCELLED` | `cancelled` |
+| `BROWSER_NOT_AVAILABLE` | No compatible browser application is installed on the device. | `a0.browser_not_available` | - | |
+| `NO_BUNDLE_IDENTIFIER` | The native bundle identifier could not be retrieved, which is required to construct the callback URL. | - | `NO_BUNDLE_IDENTIFIER` | |
+| `FAILED_TO_LOAD_URL` | The authorization URL could not be loaded in the browser. | `a0.session.failed_load` | - | |
+| `BROWSER_TERMINATED` | The browser was closed unexpectedly, likely because the application was relaunched from the home screen while the login was in progress. | `a0.session.browser_terminated` | - | |
+| `INVALID_STATE` | The `state` parameter returned from the server did not match the one sent, indicating a potential Cross-Site Request Forgery (CSRF) attack. | `access_denied` | `OTHER` | `state_mismatch` |
+| `ACCESS_DENIED` | The user or Auth0 denied the authentication request. This can be caused by a user denying consent, a failing Action or Rule, or other authorization policies. | `access_denied` | `OTHER` | `access_denied` |
+| `CONSENT_REQUIRED` | The user needs to explicitly grant consent for the application to access requested scopes or resources. | - | | `consent_required` |
+| `NO_AUTHORIZATION_CODE` | The callback URL from the server is missing the required `code` parameter needed for the token exchange. | - | `NO_AUTHORIZATION_CODE` | |
+| `INVALID_CONFIGURATION` | The Auth0 Application is misconfigured. Common causes include an invalid social connection configuration. | `a0.invalid_configuration` | `OTHER` | |
+| `PKCE_NOT_ALLOWED` | PKCE is required but not enabled for the Auth0 Application. Ensure the "Application Type" is set to "Native" in your Auth0 dashboard. | `a0.pkce_not_available` | `PKCE_NOT_ALLOWED` | |
+| `ID_TOKEN_VALIDATION_FAILED` | The ID token received is invalid and failed one or more validation checks, such as signature, issuer, audience, or nonce verification. | `a0.session.invalid_idtoken` | `ID_TOKEN_VALIDATION_FAILED` | (various validation Errors). |
+| `INVALID_INVITATION_URL` | The organization invitation URL is malformed or missing the required `organization` and `invitation` parameters. | - | `INVALID_INVITATION_URL` | |
+| `NETWORK_ERROR` | A network error occurred, preventing the request from completing. The device may be offline or unable to reach the Auth0 servers. | `a0.network_error` | `OTHER` (with `URLError` cause) | (Network-related fetch exception) |
+| `TIMEOUT_ERROR` | The web authentication flow timed out. | - | - | `timeout` (from `PopupTimeoutError`) |
+| `UNKNOWN_ERROR` | An unexpected or uncategorized error occurred. Check the `message` and `cause` properties for more specific details. | _(various)_ | `UNKNOWN` or `OTHER` | |
## Features and Platform Support
diff --git a/android/build.gradle b/android/build.gradle
index 5c3bd388..5855cfe5 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -96,7 +96,7 @@ dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.browser:browser:1.2.0"
- implementation 'com.auth0.android:auth0:3.2.1'
+ implementation 'com.auth0.android:auth0:3.8.0'
}
if (isNewArchitectureEnabled()) {
diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
index 2c96f8d5..f786cf62 100644
--- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt
+++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
@@ -24,11 +24,8 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
companion object {
const val NAME = "A0Auth0"
- const val UNKNOWN_ERROR_RESULT_CODE = 1405
- private const val CREDENTIAL_MANAGER_ERROR_CODE = "a0.invalid_state.credential_manager_exception"
- private const val INVALID_DOMAIN_URL_ERROR_CODE = "a0.invalid_domain_url"
- private const val BIOMETRICS_AUTHENTICATION_ERROR_CODE = "a0.invalid_options_biometrics_authentication"
- private const val LOCAL_AUTH_REQUEST_CODE = 150
+ private const val CREDENTIAL_MANAGER_ERROR_CODE = "CREDENTIAL_MANAGER_ERROR"
+ private const val BIOMETRICS_AUTHENTICATION_ERROR_CODE = "BIOMETRICS_CONFIGURATION_ERROR"
}
private val errorCodeMap = mapOf(
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 84bcd92e..52eb818b 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,13 +1,12 @@
PODS:
- A0Auth0 (5.0.0-beta.4):
- - Auth0 (= 2.10)
+ - Auth0 (= 2.13)
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- - JWTDecode (= 3.2.0)
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
@@ -28,12 +27,11 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - SimpleKeychain (= 1.2.0)
- SocketRocket
- Yoga
- - Auth0 (2.10.0):
- - JWTDecode (= 3.2.0)
- - SimpleKeychain (= 1.2.0)
+ - Auth0 (2.13.0):
+ - JWTDecode (= 3.3.0)
+ - SimpleKeychain (= 1.3.0)
- boost (1.84.0)
- DoubleConversion (1.1.6)
- fast_float (8.0.0)
@@ -43,7 +41,7 @@ PODS:
- hermes-engine (0.80.1):
- hermes-engine/Pre-built (= 0.80.1)
- hermes-engine/Pre-built (0.80.1)
- - JWTDecode (3.2.0)
+ - JWTDecode (3.3.0)
- RCT-Folly (2024.11.18.00):
- boost
- DoubleConversion
@@ -2248,7 +2246,7 @@ PODS:
- React-perflogger (= 0.80.1)
- React-utils (= 0.80.1)
- SocketRocket
- - RNGestureHandler (2.27.1):
+ - RNGestureHandler (2.27.2):
- boost
- DoubleConversion
- fast_float
@@ -2338,7 +2336,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - SimpleKeychain (1.2.0)
+ - SimpleKeychain (1.3.0)
- SocketRocket (0.7.1)
- Yoga (0.0.0)
@@ -2585,8 +2583,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- A0Auth0: 0b6d471aad41e5dc8fdf4318102613b53665fed9
- Auth0: 2876d0c36857422eda9cb580a6cc896c7d14cb36
+ A0Auth0: 204c6e8804100403eba89743b3e8dae7437f0435
+ Auth0: 8deb8df56dd91516403ec474d968fb9f79189b93
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
@@ -2594,75 +2592,75 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 4f07404533b808de66cf48ac4200463068d0e95a
- JWTDecode: 7dae24cb9bf9b608eae61e5081029ec169bb5527
+ JWTDecode: 1ca6f765844457d0dd8690436860fecee788f631
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: efa5010912100e944a7ac9a93a157e1def1988fe
RCTRequired: bbc4cf999ddc4a4b076e076c74dd1d39d0254630
RCTTypeSafety: d877728097547d0a37786cc9130c43ad71739ac3
React: 4b0b9cb962e694611e5e8a697c1b0300a2510c21
React-callinvoker: 70f125c17c7132811a6b473946ac5e7ae93b5e57
- React-Core: 7cbc3118df2334b2ef597d9a515938b02c82109f
- React-CoreModules: 7d8c14ecb889e7786a04637583b55b7d8f246baf
- React-cxxreact: f32be07cba236c2f20f4e05ca200577ba5358e78
+ React-Core: bab40f5b1f46fe0c5896895a6f333e861a821a81
+ React-CoreModules: 05647d952e521113c128360633896ba7ba652e82
+ React-cxxreact: 2b4bac1ec6eecc6288ac8a6caea6afb42585740e
React-debug: deb3a146ef717fa3e8f4c23e0288369fe53199b7
- React-defaultsnativemodule: 2c13a4240c5f96c42d069d1ba2392de6b4145bbd
- React-domnativemodule: 91349b0b1cb20310cec1341b87cdd461aaa85e57
- React-Fabric: bdfc7ec2481f26d7a9b8f59461f29ba4d903c549
- React-FabricComponents: 47898469543d1bfb4528a9846419ec5568be89b1
- React-FabricImage: ac8fc85ef452e5e9ae935c41118814651bd9e7f3
- React-featureflags: 793b911e4c53e680db4a7d9965d0d6dc87b2fa88
- React-featureflagsnativemodule: 25c9516d0dd004493c9bbafeb97da20bf9bde7dc
- React-graphics: e07281690425dd9eeba3875d1faad28bc1f6da3b
- React-hermes: bc1440d0e0662cc813bbf1c5ffbf9e0db2993a0f
- React-idlecallbacksnativemodule: a2a3bb4a1793280b34d06d00169153b094be8c16
- React-ImageManager: c9fa7461f3cab08e7bc98cbf55455b499e71c8b3
- React-jserrorhandler: 15e591702040afed99cfcd088cf2337a8d09d807
- React-jsi: 512ab3a1a628bc8824c41de8bcbbb81b2ac6fa8d
- React-jsiexecutor: 653ccd2dee1e5ea558eecaf2f27b8bba0f09add8
- React-jsinspector: 9121ccd2676a3f7c079ac01c9f90183422e3190e
- React-jsinspectorcdp: 5c723ff2a09d73f2fdc496a545fb7003e7fdc079
- React-jsinspectornetwork: 9cb0173f69e8405cef33fc79030fad26bbc3c073
- React-jsinspectortracing: 65dc04125dc2392d85a82b6916f8cb088ea77566
- React-jsitooling: 21af93cc98f760dd88d65b06b9317e0d4849fbbc
- React-jsitracing: 4cc1b7de8087ae41c61a0eeee2593bc3362908b6
- React-logger: 2f0d40bc8e648fbb1ff3b6580ad54189a8753290
- React-Mapbuffer: 9a7c65078c6851397c1999068989e4fc239d0c80
- React-microtasksnativemodule: 4f1ef719ba6c7ebbd2d75346ffa2916f9b4771c9
- react-native-safe-area-context: 339885703b6dd1be2bce42d9c0b0350c21180032
- React-NativeModulesApple: f6f696e510b9d89c3c06b7764f56947dc13ae922
+ React-defaultsnativemodule: 11e2948787a15d3cf1b66d7f29f13770a177bff7
+ React-domnativemodule: 2f4b279acdb2963736fb5de2f585811dd90070b5
+ React-Fabric: 6f8d1a303c96f1d078c14d74c4005bf457e5b782
+ React-FabricComponents: b106410970e9a0c4e592da656c7a7e0947306c23
+ React-FabricImage: 1abaf230dfce9b58fdf53c4128f3f40c6e64af6a
+ React-featureflags: f7ef58d91079efde3ad223bcca6d197e845d5bcf
+ React-featureflagsnativemodule: ae5abc9849d1696f4f8f11ee3744bf5715e032cf
+ React-graphics: b306856c6ed9aac32f717a229550406a53b28a6d
+ React-hermes: b6edce8fa19388654b1aea30844497cbeade83bc
+ React-idlecallbacksnativemodule: cb386712842cb9e479c89311edb234d529b64db4
+ React-ImageManager: 8ce94417853eaa22faaad1f4cc1952dd3f8e2275
+ React-jserrorhandler: ab827d67dc270a9c8703eef524230baeafaf6876
+ React-jsi: 545342ec5c78ab1277af5f0dbe8d489e7e73db14
+ React-jsiexecutor: 20210891c7c77255c16dec6762faf68b373f9f74
+ React-jsinspector: 4e73460e488132d70d2b4894e5578cc856f2cb74
+ React-jsinspectorcdp: 8b2bcb5779289cb2b9ca517f2965ed23eb2fd3e0
+ React-jsinspectornetwork: b5e0cb9e488d294eed2d8209dc3dc0f9587210c1
+ React-jsinspectortracing: f3c4036e7b984405ac910f878576d325dd9f2834
+ React-jsitooling: 75bbfd221b6173a5e848ca5a6680506bac064a56
+ React-jsitracing: 11ed7d821864dd988c159d4943e0a1e0937c11b1
+ React-logger: 984ebd897afad067555d081deaf03f57c4315723
+ React-Mapbuffer: 0c045c844ce6d85cde53e85ab163294c6adad349
+ React-microtasksnativemodule: d9499269ad1f484ae71319bac1d9231447f2094e
+ react-native-safe-area-context: 68d1363b8354472a961aa6861ba8451beaf9a810
+ React-NativeModulesApple: 983f3483ef0a3446b56d490f09d579fba2442e17
React-oscompat: 114036cd8f064558c9c1a0c04fc9ae5e1453706a
- React-perflogger: 4b2f88ae059b600daf268528a4a83366338eef05
- React-performancetimeline: e15fd9798123436f99e46898422fe921fecf506b
+ React-perflogger: e7287fee27c16e3c8bd4d470f2361572b63be16b
+ React-performancetimeline: 8ebbaa31d2d0cea680b0a2a567500d3cab8954fc
React-RCTActionSheet: 68c68b0a7a5d2b0cfc255c64889b6e485974e988
- React-RCTAnimation: 6bf502c89c53076f92cd1a254f5ec8d63ee263de
- React-RCTAppDelegate: c90f5732784684c3dd226d812eccb578cd954ad7
- React-RCTBlob: d2905f01749b80efd6d3b86fb15e30ed26d5450b
- React-RCTFabric: 435b3ffaad113fb1f274c2f2a677c9fcc9b5cf55
- React-RCTFBReactNativeSpec: a3178b419f42af196e90ca4bf07710dce5d68301
- React-RCTImage: 8f5ffa03461339180a68820ea452af6e20ace2c7
- React-RCTLinking: 1151646834d31f97580d8a75d768a84b2533b7f9
- React-RCTNetwork: 52008724d0db90a540f4058ed0de0e41c4b7943c
- React-RCTRuntime: 10ce9a7cb27ba307544d29a2a04e6202dc7b3e9a
- React-RCTSettings: f724cacbd892ee18f985e1aebdd97386e49c76f5
- React-RCTText: 6e1b95d9126d808410dfa96e09bc4441ec6f36f7
- React-RCTVibration: 862a4e5b36d49e6299c8cbfb86486fc31f86f6fa
+ React-RCTAnimation: d6c5c728b888a967ce9aff1ff71a8ed71a68d069
+ React-RCTAppDelegate: 0fc048666bda159cd469a6fb9befb04b3fa62be4
+ React-RCTBlob: 12d8c699a1f906840113ee8d8bb575e69a05509f
+ React-RCTFabric: 01e815845ebc185f44205dcbf50eeb712fec23fe
+ React-RCTFBReactNativeSpec: f57927fb0af6ce2f25c19f8b894e2986138aa89f
+ React-RCTImage: a82518168f4ee407913b23ca749ca79ef51959f3
+ React-RCTLinking: 7f343b584c36f024f390fea563483568fe763ef6
+ React-RCTNetwork: 3165eb757ceb62a7cde4cdad043d63314122e8a3
+ React-RCTRuntime: feee590c459c4cb6aaa7a00f3abc8c04709b536f
+ React-RCTSettings: 6bad0ae45d8d872c873059f332f586f99875621f
+ React-RCTText: 657d60f35983062de8f0cea67c279aa7a3ea9858
+ React-RCTVibration: 78f4770515141efb7f55f9b27c49dda95319c3a8
React-rendererconsistency: f7baab26c6d0cd5b2eb7afcecfd2d8b957017b18
- React-renderercss: 62acb8f010a062309e3bd0e203aa14636162e3b3
- React-rendererdebug: 3a89ac44f15c7160735264d585a29525655238d2
+ React-renderercss: bdd2f83a4a054c3e4321fd61305c202b848e471b
+ React-rendererdebug: 9f8865ee038127a9d99d4b034c9da4935d204993
React-rncore: f7438473c4c71ee1963fb06a8635bb96013c9e1c
- React-RuntimeApple: 81f0a9ba81ce7eb203529b0471dc69bf18f5f637
- React-RuntimeCore: 6356e89b2518ba66a989c39a2adb18122a5e3b7b
+ React-RuntimeApple: 4d2ab9f72b9193da86eceded128a67254fc18aeb
+ React-RuntimeCore: 5fd73030438d094975ca0f549d162dd97746ae38
React-runtimeexecutor: 17c70842d5e611130cb66f91e247bc4a609c3508
- React-RuntimeHermes: 0a1d7ce2fe08cf182235de1a9330b51aa6b935cd
- React-runtimescheduler: 10ae98e1417eff159be5df8fdc8fcdaac557aba6
+ React-RuntimeHermes: 3c88e6e1ea7ea0899dcffc77c10d61ea46688cfd
+ React-runtimescheduler: 024500621c7c93d65371498abb4ee26d34f5d47d
React-timing: c3c923df2b86194e1682e01167717481232f1dc7
- React-utils: 7791a96e194eec85cb41dc98a2045b5f07839598
- ReactAppDependencyProvider: ba631a31783569c13056dd57ff39e19764abdd6f
- ReactCodegen: b16d00d43b4e9dc44af53be171b17d93b4b20267
- ReactCommon: 96684b90b235d6ae340d126141edd4563b7a446a
- RNGestureHandler: c202f13fa95347076d8aca4ccb61739b067396cb
- RNScreens: 75074e642b69b086813a943bdf63da7085fb2166
- SimpleKeychain: 768cf43ae778b1c21816e94dddf01bb8ee96a075
+ React-utils: 9154a037543147e1c24098f1a48fc8472602c092
+ ReactAppDependencyProvider: afd905e84ee36e1678016ae04d7370c75ed539be
+ ReactCodegen: f8d5fb047c4cd9d2caade972cad9edac22521362
+ ReactCommon: 17fd88849a174bf9ce45461912291aca711410fc
+ RNGestureHandler: a0c83d8e4422f2ac04d1acb1741866a5184c7b73
+ RNScreens: c63849403489bd068ea160f276fbc8416f19f2f7
+ SimpleKeychain: 9c0f3ca8458fed74e01db864d181c5cbe278603e
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: daa1e4de4b971b977b23bc842aaa3e135324f1f3
diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift
index 702a596f..102abcb6 100644
--- a/ios/NativeBridge.swift
+++ b/ios/NativeBridge.swift
@@ -23,8 +23,8 @@ public class NativeBridge: NSObject {
static let tokenTypeKey = "tokenType";
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"
+ static let credentialsManagerErrorCode = "CREDENTIAL_MANAGER_ERROR"
+ static let biometricsAuthenticationErrorCode = "BIOMETRICS_CONFIGURATION_ERROR"
var credentialsManager: CredentialsManager
var clientId: String
@@ -170,7 +170,11 @@ public class NativeBridge: NSObject {
scope: scope,
recoveryCode: nil
)
- resolve(credentialsManager.store(credentials: credentials))
+ if (credentialsManager.store(credentials: credentials)) {
+ resolve(true)
+ } else {
+ reject("STORE_FAILED", "Failed to store credentials in the Keychain.", nil)
+ }
} else {
reject(NativeBridge.credentialsManagerErrorCode, "Incomplete information provided for credentials - 'expiresIn' not found", NSError.init(domain: NativeBridge.credentialsManagerErrorCode, code: -99999, userInfo: nil));
}
@@ -241,6 +245,7 @@ extension WebAuthError {
var code: String
switch self {
case .noBundleIdentifier: code = "NO_BUNDLE_IDENTIFIER"
+ case .transactionActiveAlready: code = "TRANSACTION_ACTIVE_ALREADY"
case .invalidInvitationURL: code = "INVALID_INVITATION_URL"
case .userCancelled: code = "USER_CANCELLED"
case .noAuthorizationCode: code = "NO_AUTHORIZATION_CODE"
diff --git a/src/core/interfaces/common.ts b/src/core/interfaces/common.ts
new file mode 100644
index 00000000..12deba59
--- /dev/null
+++ b/src/core/interfaces/common.ts
@@ -0,0 +1,4 @@
+export type NativeModuleError = {
+ code: string;
+ message: string;
+};
diff --git a/src/core/interfaces/index.ts b/src/core/interfaces/index.ts
index a94bf033..4cdbbd99 100644
--- a/src/core/interfaces/index.ts
+++ b/src/core/interfaces/index.ts
@@ -1,3 +1,4 @@
+export * from './common';
export * from './IAuth0Client';
export * from './IAuthenticationProvider';
export * from './ICredentialsManager';
diff --git a/src/core/models/CredentialsManagerError.ts b/src/core/models/CredentialsManagerError.ts
index c8174b7c..230152a9 100644
--- a/src/core/models/CredentialsManagerError.ts
+++ b/src/core/models/CredentialsManagerError.ts
@@ -8,6 +8,20 @@ const ERROR_CODE_MAP: Record = {
STORE_FAILED: 'STORE_FAILED',
REVOKE_FAILED: 'REVOKE_FAILED',
LARGE_MIN_TTL: 'LARGE_MIN_TTL',
+ CREDENTIAL_MANAGER_ERROR: 'CREDENTIAL_MANAGER_ERROR',
+ BIOMETRICS_FAILED: 'BIOMETRICS_FAILED',
+ NO_NETWORK: 'NO_NETWORK',
+ API_ERROR: 'API_ERROR',
+
+ // --- Web (@auth0/auth0-spa-js) mappings ---
+ login_required: 'NO_CREDENTIALS',
+ consent_required: 'RENEW_FAILED',
+ mfa_required: 'RENEW_FAILED',
+ invalid_grant: 'RENEW_FAILED',
+ invalid_refresh_token: 'RENEW_FAILED',
+ missing_refresh_token: 'NO_REFRESH_TOKEN',
+
+ // --- Many-to-one mapping for granular Android Biometric errors ---
INCOMPATIBLE_DEVICE: 'INCOMPATIBLE_DEVICE',
CRYPTO_EXCEPTION: 'CRYPTO_EXCEPTION',
BIOMETRIC_NO_ACTIVITY: 'BIOMETRICS_FAILED',
@@ -35,9 +49,6 @@ const ERROR_CODE_MAP: Record = {
BIOMETRIC_ERROR_UNABLE_TO_PROCESS: 'BIOMETRICS_FAILED',
BIOMETRICS_INVALID_USER: 'BIOMETRICS_FAILED',
BIOMETRIC_AUTHENTICATION_FAILED: 'BIOMETRICS_FAILED',
- BIOMETRICS_FAILED: 'BIOMETRICS_FAILED',
- NO_NETWORK: 'NO_NETWORK',
- API_ERROR: 'API_ERROR',
};
export class CredentialsManagerError extends AuthError {
diff --git a/src/core/models/WebAuthError.ts b/src/core/models/WebAuthError.ts
new file mode 100644
index 00000000..494159c2
--- /dev/null
+++ b/src/core/models/WebAuthError.ts
@@ -0,0 +1,57 @@
+import { AuthError } from './AuthError';
+
+const ERROR_CODE_MAP: Record = {
+ // --- Common Codes ---
+ 'a0.session.user_cancelled': 'USER_CANCELLED',
+ 'USER_CANCELLED': 'USER_CANCELLED',
+ 'access_denied': 'ACCESS_DENIED',
+ 'a0.network_error': 'NETWORK_ERROR',
+ 'a0.session.invalid_idtoken': 'ID_TOKEN_VALIDATION_FAILED',
+ 'ID_TOKEN_VALIDATION_FAILED': 'ID_TOKEN_VALIDATION_FAILED',
+ 'BIOMETRICS_CONFIGURATION_ERROR': 'BIOMETRICS_CONFIGURATION_ERROR',
+
+ // --- Android-specific mappings ---
+ 'a0.browser_not_available': 'BROWSER_NOT_AVAILABLE',
+ 'a0.session.failed_load': 'FAILED_TO_LOAD_URL',
+ 'a0.session.browser_terminated': 'BROWSER_TERMINATED',
+
+ // --- iOS-specific mappings ---
+ 'NO_BUNDLE_IDENTIFIER': 'NO_BUNDLE_IDENTIFIER',
+ 'TRANSACTION_ACTIVE_ALREADY': 'TRANSACTION_ACTIVE_ALREADY',
+ 'NO_AUTHORIZATION_CODE': 'NO_AUTHORIZATION_CODE',
+ 'PKCE_NOT_ALLOWED': 'PKCE_NOT_ALLOWED',
+ 'INVALID_INVITATION_URL': 'INVALID_INVITATION_URL',
+
+ // --- Web (@auth0/auth0-spa-js) mappings ---
+ 'cancelled': 'USER_CANCELLED',
+ 'state_mismatch': 'INVALID_STATE',
+ 'login_required': 'ACCESS_DENIED',
+ 'timeout': 'TIMEOUT_ERROR',
+ 'consent_required': 'CONSENT_REQUIRED',
+
+ // --- Generic Fallbacks ---
+ 'a0.invalid_configuration': 'INVALID_CONFIGURATION',
+ 'UNKNOWN': 'UNKNOWN_ERROR',
+ 'OTHER': 'UNKNOWN_ERROR',
+};
+
+export class WebAuthError extends AuthError {
+ public readonly type: string;
+
+ constructor(originalError: AuthError) {
+ super(originalError.name, originalError.message, {
+ status: originalError.status,
+ code: originalError.code,
+ json: originalError.json,
+ });
+
+ if (
+ originalError.message.includes('state is invalid') ||
+ originalError.code === 'state_mismatch'
+ ) {
+ this.type = 'INVALID_STATE';
+ } else {
+ this.type = ERROR_CODE_MAP[originalError.code] || 'UNKNOWN_ERROR';
+ }
+ }
+}
diff --git a/src/core/models/index.ts b/src/core/models/index.ts
index 939656f2..e7ce580c 100644
--- a/src/core/models/index.ts
+++ b/src/core/models/index.ts
@@ -1,3 +1,5 @@
export { AuthError } from './AuthError';
export { Credentials } from './Credentials';
export { Auth0User } from './Auth0User';
+export { CredentialsManagerError } from './CredentialsManagerError';
+export { WebAuthError } from './WebAuthError';
diff --git a/src/platforms/native/adapters/NativeCredentialsManager.ts b/src/platforms/native/adapters/NativeCredentialsManager.ts
index 61a24e7f..46044e89 100644
--- a/src/platforms/native/adapters/NativeCredentialsManager.ts
+++ b/src/platforms/native/adapters/NativeCredentialsManager.ts
@@ -1,6 +1,6 @@
import type { ICredentialsManager } from '../../../core/interfaces';
-import type { AuthError } from '../../../core/models';
-import { CredentialsManagerError } from '../../../core/models/CredentialsManagerError';
+import { AuthError } from '../../../core/models';
+import { CredentialsManagerError } from '../../../core/models';
import type { Credentials } from '../../../types';
import type { INativeBridge } from '../bridge';
diff --git a/src/platforms/native/adapters/NativeWebAuthProvider.ts b/src/platforms/native/adapters/NativeWebAuthProvider.ts
index adb0168a..f323b58f 100644
--- a/src/platforms/native/adapters/NativeWebAuthProvider.ts
+++ b/src/platforms/native/adapters/NativeWebAuthProvider.ts
@@ -11,7 +11,7 @@ import type {
} from '../../../types/platform-specific';
import type { INativeBridge } from '../bridge';
import { finalizeScope } from '../../../core/utils';
-import { AuthError } from '../../../core/models';
+import { AuthError, WebAuthError } from '../../../core/models';
const webAuthNotSupported = 'This Method is only available in web platform.';
@@ -72,7 +72,7 @@ export class NativeWebAuthProvider implements IWebAuthProvider {
} catch (error) {
// On error, always clean up the listener.
linkSubscription?.remove();
- throw error;
+ throw new WebAuthError(error as AuthError);
}
}
@@ -80,32 +80,39 @@ export class NativeWebAuthProvider implements IWebAuthProvider {
parameters: ClearSessionParameters = {},
options: NativeClearSessionOptions = {}
): Promise {
- // 1. Determine the scheme from the `options` object.
- const scheme =
- options.customScheme ??
- (await this.getDefaultScheme(options.useLegacyCallbackUrl));
+ try {
+ // 1. Determine the scheme from the `options` object.
+ const scheme =
+ options.customScheme ??
+ (await this.getDefaultScheme(options.useLegacyCallbackUrl));
- // 2. Determine the returnToUrl from the `parameters` object, providing a default if needed.
- const returnToUrl =
- parameters.returnToUrl ?? (await this.getCallbackUri(scheme));
+ // 2. Determine the returnToUrl from the `parameters` object, providing a default if needed.
+ const returnToUrl =
+ parameters.returnToUrl ?? (await this.getCallbackUri(scheme));
- // 3. Prepare the final parameters and options for the bridge.
- const finalParameters: ClearSessionParameters = {
- ...parameters,
- returnToUrl,
- };
- const finalOptions: NativeClearSessionOptions = {
- ...options,
- customScheme: scheme,
- };
+ // 3. Prepare the final parameters and options for the bridge.
+ const finalParameters: ClearSessionParameters = {
+ ...parameters,
+ returnToUrl,
+ };
+ const finalOptions: NativeClearSessionOptions = {
+ ...options,
+ customScheme: scheme,
+ };
- // 4. Call the bridge with the two separate, finalized objects.
- return this.bridge.clearSession(finalParameters, finalOptions);
+ // 4. Call the bridge with the two separate, finalized objects.
+ return await this.bridge.clearSession(finalParameters, finalOptions);
+ } catch (error) {
+ throw new WebAuthError(error as AuthError);
+ }
}
async cancelWebAuth(): Promise {
- // This is a direct pass-through, as the method signatures match.
- return this.bridge.cancelWebAuth();
+ try {
+ return this.bridge.cancelWebAuth();
+ } catch (error) {
+ throw new WebAuthError(error as AuthError);
+ }
}
private async getDefaultScheme(useLegacy: boolean = false): Promise {
diff --git a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.errors.spec.ts b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.errors.spec.ts
new file mode 100644
index 00000000..23a15982
--- /dev/null
+++ b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.errors.spec.ts
@@ -0,0 +1,215 @@
+import { NativeCredentialsManager } from '../NativeCredentialsManager';
+import { INativeBridge } from '../../bridge';
+import { CredentialsManagerError } from '../../../../core/models';
+
+// Mock the native bridge
+const mockBridge: jest.Mocked = {
+ getCredentials: jest.fn(),
+ saveCredentials: jest.fn(),
+ clearCredentials: jest.fn(),
+ // Stub other methods
+ hasValidInstance: jest.fn(),
+ initialize: jest.fn(),
+ getBundleIdentifier: jest.fn(),
+ authorize: jest.fn(),
+ clearSession: jest.fn(),
+ hasValidCredentials: jest.fn(),
+ cancelWebAuth: jest.fn(),
+ resumeWebAuth: jest.fn(),
+};
+
+describe('Common Error Handling - NativeCredentialsManager', () => {
+ let manager: NativeCredentialsManager;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ manager = new NativeCredentialsManager(mockBridge);
+ });
+ describe('Core Error Types', () => {
+ const coreErrorTestCases = [
+ {
+ code: 'INVALID_CREDENTIALS',
+ message: 'Invalid credentials provided.',
+ expectedType: 'INVALID_CREDENTIALS',
+ method: 'getCredentials',
+ },
+ {
+ code: 'NO_CREDENTIALS',
+ message: 'No credentials stored.',
+ expectedType: 'NO_CREDENTIALS',
+ method: 'getCredentials',
+ },
+ {
+ code: 'NO_REFRESH_TOKEN',
+ message: 'No refresh token available.',
+ expectedType: 'NO_REFRESH_TOKEN',
+ method: 'getCredentials',
+ },
+ {
+ code: 'RENEW_FAILED',
+ message: 'Refresh token renewal failed.',
+ expectedType: 'RENEW_FAILED',
+ method: 'getCredentials',
+ },
+ {
+ code: 'STORE_FAILED',
+ message: 'Failed to store credentials.',
+ expectedType: 'STORE_FAILED',
+ method: 'saveCredentials',
+ args: [{}],
+ },
+ {
+ code: 'REVOKE_FAILED',
+ message: 'Failed to revoke credentials.',
+ expectedType: 'REVOKE_FAILED',
+ method: 'clearCredentials',
+ },
+ {
+ code: 'LARGE_MIN_TTL',
+ message:
+ 'The minTTL requested is greater than the lifetime of the renewed access token. Request a lower minTTL or increase the "Token Expiration" value in the settings page of your Auth0 API.',
+ expectedType: 'LARGE_MIN_TTL',
+ method: 'getCredentials',
+ },
+ {
+ code: 'CREDENTIAL_MANAGER_ERROR',
+ message: 'General credentials manager error occurred.',
+ expectedType: 'CREDENTIAL_MANAGER_ERROR',
+ method: 'getCredentials',
+ },
+ {
+ code: 'BIOMETRICS_FAILED',
+ message: 'Biometric authentication failed.',
+ expectedType: 'BIOMETRICS_FAILED',
+ method: 'getCredentials',
+ },
+ {
+ code: 'NO_NETWORK',
+ message: 'No network connection available.',
+ expectedType: 'NO_NETWORK',
+ method: 'getCredentials',
+ },
+ {
+ code: 'API_ERROR',
+ message: 'API request failed.',
+ expectedType: 'API_ERROR',
+ method: 'getCredentials',
+ },
+ ];
+
+ coreErrorTestCases.forEach(
+ ({ code, message, expectedType, method, args = [] }) => {
+ it(`should handle ${code} error`, async () => {
+ const nativeError = { code, message };
+ (
+ mockBridge[method as keyof typeof mockBridge] as jest.Mock
+ ).mockRejectedValue(nativeError);
+
+ await expect((manager as any)[method](...args)).rejects.toThrow(
+ CredentialsManagerError
+ );
+
+ try {
+ await (manager as any)[method](...args);
+ } catch (e) {
+ const err = e as CredentialsManagerError;
+ expect(err.type).toBe(expectedType);
+ expect(err.message).toBe(message);
+ }
+ });
+ }
+ );
+ });
+
+ describe('Special Error Cases', () => {
+ const specialErrorTestCases = [
+ {
+ code: 'INCOMPATIBLE_DEVICE',
+ message: 'Device is incompatible.',
+ expectedType: 'INCOMPATIBLE_DEVICE',
+ },
+ {
+ code: 'CRYPTO_EXCEPTION',
+ message: 'Cryptographic exception occurred.',
+ expectedType: 'CRYPTO_EXCEPTION',
+ },
+ {
+ code: 'UNKNOWN_PLATFORM_ERROR',
+ message: 'An unknown platform error occurred.',
+ expectedType: 'UNKNOWN_ERROR',
+ },
+ ];
+
+ specialErrorTestCases.forEach(({ code, message, expectedType }) => {
+ it(`should handle ${code} error${
+ expectedType === 'UNKNOWN_ERROR' ? ' with UNKNOWN_ERROR type' : ''
+ }`, async () => {
+ const nativeError = { code, message };
+ mockBridge.getCredentials.mockRejectedValue(nativeError);
+
+ await expect(manager.getCredentials()).rejects.toThrow(
+ CredentialsManagerError
+ );
+
+ try {
+ await manager.getCredentials();
+ } catch (e) {
+ const err = e as CredentialsManagerError;
+ expect(err.type).toBe(expectedType);
+ expect(err.message).toBe(message);
+ }
+ });
+ });
+ });
+
+ describe('Android Biometric Error Mappings', () => {
+ const biometricErrorTestCases = [
+ 'BIOMETRIC_NO_ACTIVITY',
+ 'BIOMETRIC_ERROR_STATUS_UNKNOWN',
+ 'BIOMETRIC_ERROR_UNSUPPORTED',
+ 'BIOMETRIC_ERROR_HW_UNAVAILABLE',
+ 'BIOMETRIC_ERROR_NONE_ENROLLED',
+ 'BIOMETRIC_ERROR_NO_HARDWARE',
+ 'BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED',
+ 'BIOMETRIC_AUTHENTICATION_CHECK_FAILED',
+ 'BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE',
+ 'BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE',
+ 'BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL',
+ 'BIOMETRIC_ERROR_NEGATIVE_BUTTON',
+ 'BIOMETRIC_ERROR_HW_NOT_PRESENT',
+ 'BIOMETRIC_ERROR_NO_BIOMETRICS',
+ 'BIOMETRIC_ERROR_USER_CANCELED',
+ 'BIOMETRIC_ERROR_LOCKOUT_PERMANENT',
+ 'BIOMETRIC_ERROR_VENDOR',
+ 'BIOMETRIC_ERROR_LOCKOUT',
+ 'BIOMETRIC_ERROR_CANCELED',
+ 'BIOMETRIC_ERROR_NO_SPACE',
+ 'BIOMETRIC_ERROR_TIMEOUT',
+ 'BIOMETRIC_ERROR_UNABLE_TO_PROCESS',
+ 'BIOMETRICS_INVALID_USER',
+ 'BIOMETRIC_AUTHENTICATION_FAILED',
+ ];
+
+ biometricErrorTestCases.forEach((errorCode) => {
+ it(`should map ${errorCode} to BIOMETRICS_FAILED`, async () => {
+ const nativeError = {
+ code: errorCode,
+ message: `Biometric error: ${errorCode}`,
+ };
+ mockBridge.getCredentials.mockRejectedValue(nativeError);
+
+ await expect(manager.getCredentials()).rejects.toThrow(
+ CredentialsManagerError
+ );
+
+ try {
+ await manager.getCredentials();
+ } catch (e) {
+ const err = e as CredentialsManagerError;
+ expect(err.type).toBe('BIOMETRICS_FAILED');
+ expect(err.message).toBe(`Biometric error: ${errorCode}`);
+ }
+ });
+ });
+ });
+});
diff --git a/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.errors.spec.ts b/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.errors.spec.ts
new file mode 100644
index 00000000..1b6735d8
--- /dev/null
+++ b/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.errors.spec.ts
@@ -0,0 +1,313 @@
+import { NativeWebAuthProvider } from '../NativeWebAuthProvider';
+import { INativeBridge } from '../../bridge';
+import { WebAuthError } from '../../../../core/models';
+
+// Mock the native bridge
+const mockBridge: jest.Mocked = {
+ authorize: jest.fn(),
+ clearSession: jest.fn(),
+ getBundleIdentifier: jest.fn().mockResolvedValue('com.app.test'),
+ // Stub other methods to satisfy the interface
+ hasValidInstance: jest.fn(),
+ initialize: jest.fn(),
+ saveCredentials: jest.fn(),
+ getCredentials: jest.fn(),
+ hasValidCredentials: jest.fn(),
+ clearCredentials: jest.fn(),
+ cancelWebAuth: jest.fn(),
+ resumeWebAuth: jest.fn(),
+};
+
+describe('NativeWebAuthProvider Error Handling', () => {
+ let provider: NativeWebAuthProvider;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ provider = new NativeWebAuthProvider(mockBridge, 'test.auth0.com');
+ });
+
+ describe('Common Error Types', () => {
+ const commonErrorTestCases = [
+ {
+ code: 'a0.session.user_cancelled',
+ message: 'User cancelled.',
+ expectedType: 'USER_CANCELLED',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'USER_CANCELLED',
+ message: 'User cancelled.',
+ expectedType: 'USER_CANCELLED',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'access_denied',
+ message: 'Access denied.',
+ expectedType: 'ACCESS_DENIED',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'a0.network_error',
+ message: 'Network error occurred.',
+ expectedType: 'NETWORK_ERROR',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'a0.session.invalid_idtoken',
+ message: 'Invalid ID token.',
+ expectedType: 'ID_TOKEN_VALIDATION_FAILED',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'ID_TOKEN_VALIDATION_FAILED',
+ message: 'ID token validation failed.',
+ expectedType: 'ID_TOKEN_VALIDATION_FAILED',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'BIOMETRICS_CONFIGURATION_ERROR',
+ message: 'Biometrics configuration error.',
+ expectedType: 'BIOMETRICS_CONFIGURATION_ERROR',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ ];
+
+ commonErrorTestCases.forEach(
+ ({ code, message, expectedType, method, mockMethod }) => {
+ it(`should handle ${code} error`, async () => {
+ const nativeError = { code, message };
+ (
+ mockBridge[mockMethod as keyof typeof mockBridge] as jest.Mock
+ ).mockRejectedValue(nativeError);
+
+ await expect((provider as any)[method]()).rejects.toThrow(
+ WebAuthError
+ );
+
+ try {
+ await (provider as any)[method]();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe(expectedType);
+ expect(err.message).toBe(message);
+ }
+ });
+ }
+ );
+ });
+
+ describe('Android-Specific Error Mappings', () => {
+ const androidErrorTestCases = [
+ {
+ code: 'a0.browser_not_available',
+ message: 'No browser.',
+ expectedType: 'BROWSER_NOT_AVAILABLE',
+ method: 'clearSession',
+ mockMethod: 'clearSession',
+ },
+ {
+ code: 'a0.session.failed_load',
+ message: 'Failed to load URL.',
+ expectedType: 'FAILED_TO_LOAD_URL',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'a0.session.browser_terminated',
+ message: 'Browser terminated.',
+ expectedType: 'BROWSER_TERMINATED',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ ];
+
+ androidErrorTestCases.forEach(
+ ({ code, message, expectedType, method, mockMethod }) => {
+ it(`should map ${code} to ${expectedType}`, async () => {
+ const nativeError = { code, message };
+ (
+ mockBridge[mockMethod as keyof typeof mockBridge] as jest.Mock
+ ).mockRejectedValue(nativeError);
+
+ await expect((provider as any)[method]()).rejects.toThrow(
+ WebAuthError
+ );
+
+ try {
+ await (provider as any)[method]();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe(expectedType);
+ expect(err.message).toBe(message);
+ }
+ });
+ }
+ );
+ });
+
+ describe('iOS-Specific Error Mappings', () => {
+ const iOSErrorTestCases = [
+ {
+ code: 'NO_BUNDLE_IDENTIFIER',
+ message: 'No bundle identifier.',
+ expectedType: 'NO_BUNDLE_IDENTIFIER',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'TRANSACTION_ACTIVE_ALREADY',
+ message: 'Transaction already active.',
+ expectedType: 'TRANSACTION_ACTIVE_ALREADY',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'NO_AUTHORIZATION_CODE',
+ message: 'No authorization code.',
+ expectedType: 'NO_AUTHORIZATION_CODE',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'PKCE_NOT_ALLOWED',
+ message: 'PKCE not allowed.',
+ expectedType: 'PKCE_NOT_ALLOWED',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'INVALID_INVITATION_URL',
+ message: 'Invalid invitation URL.',
+ expectedType: 'INVALID_INVITATION_URL',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ ];
+
+ iOSErrorTestCases.forEach(
+ ({ code, message, expectedType, method, mockMethod }) => {
+ it(`should map ${code} to ${expectedType}`, async () => {
+ const nativeError = { code, message };
+ (
+ mockBridge[mockMethod as keyof typeof mockBridge] as jest.Mock
+ ).mockRejectedValue(nativeError);
+
+ await expect((provider as any)[method]()).rejects.toThrow(
+ WebAuthError
+ );
+
+ try {
+ await (provider as any)[method]();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe(expectedType);
+ expect(err.message).toBe(message);
+ }
+ });
+ }
+ );
+ });
+
+ describe('Generic Fallback Errors', () => {
+ const fallbackErrorTestCases = [
+ {
+ code: 'a0.invalid_configuration',
+ message: 'Invalid configuration.',
+ expectedType: 'INVALID_CONFIGURATION',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'UNKNOWN',
+ message: 'Unknown error.',
+ expectedType: 'UNKNOWN_ERROR',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'OTHER',
+ message: 'Other error.',
+ expectedType: 'UNKNOWN_ERROR',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ {
+ code: 'SOME_UNMAPPED_ERROR',
+ message: 'Some unmapped error.',
+ expectedType: 'UNKNOWN_ERROR',
+ method: 'authorize',
+ mockMethod: 'authorize',
+ },
+ ];
+
+ fallbackErrorTestCases.forEach(
+ ({ code, message, expectedType, method, mockMethod }) => {
+ it(`should map ${code} to ${expectedType}`, async () => {
+ const nativeError = { code, message };
+ (
+ mockBridge[mockMethod as keyof typeof mockBridge] as jest.Mock
+ ).mockRejectedValue(nativeError);
+
+ await expect((provider as any)[method]()).rejects.toThrow(
+ WebAuthError
+ );
+
+ try {
+ await (provider as any)[method]();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe(expectedType);
+ expect(err.message).toBe(message);
+ }
+ });
+ }
+ );
+ });
+
+ describe('Special State Validation', () => {
+ it('should handle state_mismatch error with special validation', async () => {
+ const nativeError = {
+ code: 'state_mismatch',
+ message: 'state is invalid',
+ };
+ mockBridge.authorize.mockRejectedValue(nativeError);
+
+ await expect(provider.authorize()).rejects.toThrow(WebAuthError);
+
+ try {
+ await provider.authorize();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe('INVALID_STATE');
+ expect(err.message).toBe('state is invalid');
+ }
+ });
+
+ it('should handle error with "state is invalid" in message', async () => {
+ const nativeError = {
+ code: 'some_other_code',
+ message: 'The state is invalid and cannot be processed',
+ };
+ mockBridge.authorize.mockRejectedValue(nativeError);
+
+ await expect(provider.authorize()).rejects.toThrow(WebAuthError);
+
+ try {
+ await provider.authorize();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe('INVALID_STATE');
+ expect(err.message).toBe(
+ 'The state is invalid and cannot be processed'
+ );
+ }
+ });
+ });
+});
diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts
index c95d688a..3804a531 100644
--- a/src/platforms/native/bridge/NativeBridgeManager.ts
+++ b/src/platforms/native/bridge/NativeBridgeManager.ts
@@ -15,11 +15,7 @@ import {
Credentials as CredentialsModel,
} from '../../../core/models';
import Auth0NativeModule from '../../../specs/NativeA0Auth0';
-
-type NativeModuleError = {
- code: string;
- message: string;
-};
+import type { NativeModuleError } from '../../../core/interfaces';
/**
* Manages the direct communication with the native Auth0 module.
@@ -37,7 +33,7 @@ export class NativeBridgeManager implements INativeBridge {
return await nativeMethod(...args);
} catch (e) {
const { code, message } = e as NativeModuleError;
- throw new AuthError(code, message, { code });
+ throw new AuthError(code, message, { code, json: e });
}
}
diff --git a/src/platforms/web/adapters/WebCredentialsManager.ts b/src/platforms/web/adapters/WebCredentialsManager.ts
index f9044913..b29b6e75 100644
--- a/src/platforms/web/adapters/WebCredentialsManager.ts
+++ b/src/platforms/web/adapters/WebCredentialsManager.ts
@@ -2,6 +2,7 @@ import type { ICredentialsManager } from '../../../core/interfaces';
import type { Credentials } from '../../../types';
import {
AuthError,
+ CredentialsManagerError,
Credentials as CredentialsModel,
} from '../../../core/models';
import type { Auth0Client } from '@auth0/auth0-spa-js';
@@ -32,7 +33,7 @@ export class WebCredentialsManager implements ICredentialsManager {
const claims = await this.client.getIdTokenClaims();
if (!claims || !claims.exp) {
throw new AuthError(
- 'IdTokenMissing',
+ 'ID_TOKEN_CLAIM_VALIDATION_FAILED',
'ID token or expiration claim is missing.'
);
}
@@ -45,18 +46,12 @@ export class WebCredentialsManager implements ICredentialsManager {
scope: tokenResponse.scope,
});
} catch (e: any) {
- if (e.error === 'login_required' || e.error === 'consent_required') {
- throw new AuthError(
- e.error,
- 'User interaction is required for login or consent.',
- { json: e }
- );
- }
- throw new AuthError(
- e.error ?? 'GetCredentialsFailed',
- e.error_description ?? e.message,
- { json: e }
- );
+ const code = e.error ?? 'GetCredentialsFailed';
+ const authError = new AuthError(code, e.error_description ?? e.message, {
+ json: e,
+ code,
+ });
+ throw new CredentialsManagerError(authError);
}
}
@@ -65,6 +60,15 @@ export class WebCredentialsManager implements ICredentialsManager {
}
async clearCredentials(): Promise {
- await this.client.logout({ openUrl: false });
+ try {
+ await this.client.logout({ openUrl: false });
+ } catch (e: any) {
+ const code = e.error ?? 'ClearCredentialsFailed';
+ const authError = new AuthError(code, e.error_description ?? e.message, {
+ json: e,
+ code,
+ });
+ throw new CredentialsManagerError(authError);
+ }
}
}
diff --git a/src/platforms/web/adapters/WebWebAuthProvider.ts b/src/platforms/web/adapters/WebWebAuthProvider.ts
index cc85bd63..0be8bc40 100644
--- a/src/platforms/web/adapters/WebWebAuthProvider.ts
+++ b/src/platforms/web/adapters/WebWebAuthProvider.ts
@@ -4,9 +4,9 @@ import type {
WebAuthorizeParameters,
ClearSessionParameters,
} from '../../../types';
-import { AuthError } from '../../../core/models';
+import { AuthError, WebAuthError } from '../../../core/models';
import { finalizeScope } from '../../../core/utils';
-import type { Auth0Client } from '@auth0/auth0-spa-js';
+import type { Auth0Client, PopupCancelledError } from '@auth0/auth0-spa-js';
export class WebWebAuthProvider implements IWebAuthProvider {
constructor(private client: Auth0Client) {}
@@ -16,29 +16,35 @@ export class WebWebAuthProvider implements IWebAuthProvider {
): Promise {
const finalScope = finalizeScope(parameters.scope);
const { redirectUrl, ...restParams } = parameters;
- await this.client.loginWithRedirect({
- authorizationParams: {
- ...restParams,
- scope: finalScope,
- redirect_uri: redirectUrl,
- },
- });
+ try {
+ await this.client.loginWithRedirect({
+ authorizationParams: {
+ ...restParams,
+ scope: finalScope,
+ redirect_uri: redirectUrl,
+ },
+ });
- // NOTE: loginWithRedirect does not resolve with a value, as it triggers a full
- // page navigation. The user session is recovered by `handleRedirectCallback`
- // when the app reloads. We return a Promise that never resolves to match the
- // interface, but in practice, the application context will be lost.
- return new Promise(() => {});
+ // NOTE: loginWithRedirect does not resolve with a value, as it triggers a full
+ // page navigation. The user session is recovered by `handleRedirectCallback`
+ // when the app reloads. We return a Promise that never resolves to match the
+ // interface, but in practice, the application context will be lost.
+ return new Promise(() => {});
+ } catch (e: any) {
+ const code = e.error ?? 'AuthorizeFailed';
+ throw new WebAuthError(
+ new AuthError(code, e.error_description ?? e.message, { json: e, code })
+ );
+ }
}
async handleRedirectCallback(url?: string): Promise {
try {
await this.client.handleRedirectCallback(url);
} catch (e: any) {
- throw new AuthError(
- e.error ?? 'RedirectCallbackError',
- e.error_description ?? e.message,
- { json: e }
+ const code = e.error ?? 'RedirectCallbackError';
+ throw new WebAuthError(
+ new AuthError(code, e.error_description ?? e.message, { json: e, code })
);
}
}
@@ -52,10 +58,23 @@ export class WebWebAuthProvider implements IWebAuthProvider {
},
});
} catch (e: any) {
- throw new AuthError(
- e.error ?? 'LogoutFailed',
- e.error_description ?? e.message,
- { json: e }
+ const code = e.error ?? 'LogoutFailed';
+ if ((e as PopupCancelledError).error === 'cancelled') {
+ throw new WebAuthError(
+ new AuthError(
+ 'cancelled',
+ e.error_description ??
+ e.message ??
+ 'User cancelled the logout popup.',
+ {
+ json: e,
+ code,
+ }
+ )
+ );
+ }
+ throw new WebAuthError(
+ new AuthError(code, e.error_description ?? e.message, { json: e, code })
);
}
}
diff --git a/src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts b/src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts
new file mode 100644
index 00000000..62b4b31d
--- /dev/null
+++ b/src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts
@@ -0,0 +1,73 @@
+import { Auth0Client } from '@auth0/auth0-spa-js';
+import { WebCredentialsManager } from '../WebCredentialsManager';
+import { CredentialsManagerError } from '../../../../core/models';
+
+jest.mock('@auth0/auth0-spa-js');
+
+describe('WebCredentialsManager Error Handling', () => {
+ let mockSpaClient: jest.Mocked;
+ let manager: WebCredentialsManager;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockSpaClient = new (Auth0Client as jest.Mock)({
+ domain: 'test.auth0.com',
+ clientId: 'test-client-id',
+ });
+ manager = new WebCredentialsManager(mockSpaClient);
+ });
+
+ describe('Web Error Mappings', () => {
+ const webErrorTestCases = [
+ {
+ code: 'login_required',
+ message: 'Login is required.',
+ expectedType: 'NO_CREDENTIALS',
+ },
+ {
+ code: 'consent_required',
+ message: 'Consent is required.',
+ expectedType: 'RENEW_FAILED',
+ },
+ {
+ code: 'mfa_required',
+ message: 'Multi-factor authentication is required.',
+ expectedType: 'RENEW_FAILED',
+ },
+ {
+ code: 'invalid_grant',
+ message: 'Invalid grant provided.',
+ expectedType: 'RENEW_FAILED',
+ },
+ {
+ code: 'invalid_refresh_token',
+ message: 'Invalid refresh token.',
+ expectedType: 'RENEW_FAILED',
+ },
+ {
+ code: 'missing_refresh_token',
+ message: 'Missing refresh token.',
+ expectedType: 'NO_REFRESH_TOKEN',
+ },
+ ];
+
+ webErrorTestCases.forEach(({ code, message, expectedType }) => {
+ it(`should map ${code} to ${expectedType}`, async () => {
+ const spaJsError = { error: code, error_description: message };
+ mockSpaClient.getTokenSilently.mockRejectedValue(spaJsError);
+
+ await expect(manager.getCredentials()).rejects.toThrow(
+ CredentialsManagerError
+ );
+
+ try {
+ await manager.getCredentials();
+ } catch (e) {
+ const err = e as CredentialsManagerError;
+ expect(err.type).toBe(expectedType);
+ expect(err.message).toBe(message);
+ }
+ });
+ });
+ });
+});
diff --git a/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts b/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts
index 905a61b4..5aa0f9a1 100644
--- a/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts
+++ b/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts
@@ -12,6 +12,14 @@ jest.mock('../../../../core/models', () => ({
if (details) Object.assign(error, details);
return error;
}),
+ CredentialsManagerError: jest
+ .fn()
+ .mockImplementation(function (originalError) {
+ const error = new Error(originalError.message);
+ error.name = originalError.name;
+ (error as any).type = 'MOCKED_TYPE';
+ return error;
+ }),
Credentials: jest.fn().mockImplementation(function (data) {
return Object.assign(this, data);
}),
@@ -26,10 +34,10 @@ describe('WebCredentialsManager', () => {
jest.clearAllMocks();
mockSpaClient = {
- getTokenSilently: jest.fn(),
- getIdTokenClaims: jest.fn(),
- isAuthenticated: jest.fn(),
- logout: jest.fn(),
+ getTokenSilently: jest.fn().mockResolvedValue({}),
+ getIdTokenClaims: jest.fn().mockResolvedValue({}),
+ isAuthenticated: jest.fn().mockResolvedValue(false),
+ logout: jest.fn().mockResolvedValue(undefined),
} as any;
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
@@ -71,6 +79,7 @@ describe('WebCredentialsManager', () => {
iat: 1234567800,
aud: 'test-client-id',
iss: 'https://test.auth0.com/',
+ __raw: 'raw-token-string',
};
it('should get credentials successfully with default parameters', async () => {
@@ -149,30 +158,6 @@ describe('WebCredentialsManager', () => {
);
});
- it('should handle login_required error specifically', async () => {
- const loginRequiredError = {
- error: 'login_required',
- error_description: 'User interaction is required',
- };
- mockSpaClient.getTokenSilently.mockRejectedValue(loginRequiredError);
-
- await expect(credentialsManager.getCredentials()).rejects.toThrow(
- 'User interaction is required for login or consent.'
- );
- });
-
- it('should handle consent_required error specifically', async () => {
- const consentRequiredError = {
- error: 'consent_required',
- error_description: 'Consent is required',
- };
- mockSpaClient.getTokenSilently.mockRejectedValue(consentRequiredError);
-
- await expect(credentialsManager.getCredentials()).rejects.toThrow(
- 'User interaction is required for login or consent.'
- );
- });
-
it('should handle other errors with error and error_description', async () => {
const genericError = {
error: 'access_denied',
diff --git a/src/platforms/web/adapters/__tests__/WebWebAuthProvider.errors.spec.ts b/src/platforms/web/adapters/__tests__/WebWebAuthProvider.errors.spec.ts
new file mode 100644
index 00000000..dc14a71d
--- /dev/null
+++ b/src/platforms/web/adapters/__tests__/WebWebAuthProvider.errors.spec.ts
@@ -0,0 +1,153 @@
+import { Auth0Client } from '@auth0/auth0-spa-js';
+import { WebWebAuthProvider } from '../WebWebAuthProvider';
+import { WebAuthError } from '../../../../core/models';
+
+jest.mock('@auth0/auth0-spa-js');
+
+describe('WebWebAuthProvider Error Handling', () => {
+ let mockSpaClient: jest.Mocked;
+ let provider: WebWebAuthProvider;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockSpaClient = new (Auth0Client as jest.Mock)({
+ domain: 'test.auth0.com',
+ clientId: 'test-client-id',
+ });
+ provider = new WebWebAuthProvider(mockSpaClient);
+ });
+
+ describe('Web-Specific Error Mappings', () => {
+ const webErrorTestCases = [
+ {
+ error: 'access_denied',
+ error_description: 'Logout denied',
+ expectedType: 'ACCESS_DENIED',
+ method: 'clearSession',
+ mockMethod: 'logout',
+ },
+ {
+ error: 'cancelled',
+ error_description: 'Popup closed',
+ expectedType: 'USER_CANCELLED',
+ method: 'clearSession',
+ mockMethod: 'logout',
+ },
+ {
+ error: 'state_mismatch',
+ error_description: 'Invalid state',
+ expectedType: 'INVALID_STATE',
+ method: 'handleRedirectCallback',
+ mockMethod: 'handleRedirectCallback',
+ },
+ {
+ error: 'login_required',
+ error_description: 'Login is required',
+ expectedType: 'ACCESS_DENIED',
+ method: 'authorize',
+ mockMethod: 'loginWithRedirect',
+ },
+ {
+ error: 'timeout',
+ error_description: 'Request timed out',
+ expectedType: 'TIMEOUT_ERROR',
+ method: 'authorize',
+ mockMethod: 'loginWithRedirect',
+ },
+ {
+ error: 'consent_required',
+ error_description: 'Consent is required',
+ expectedType: 'CONSENT_REQUIRED',
+ method: 'authorize',
+ mockMethod: 'loginWithRedirect',
+ },
+ ];
+
+ webErrorTestCases.forEach(
+ ({ error, error_description, expectedType, method, mockMethod }) => {
+ it(`should map ${error} to ${expectedType}`, async () => {
+ const spaJsError = { error, error_description };
+ (
+ mockSpaClient[mockMethod as keyof typeof mockSpaClient] as jest.Mock
+ ).mockRejectedValue(spaJsError);
+
+ await expect((provider as any)[method]()).rejects.toThrow(
+ WebAuthError
+ );
+
+ try {
+ await (provider as any)[method]();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe(expectedType);
+ expect(err.message).toBe(error_description);
+ }
+ });
+ }
+ );
+ });
+
+ describe('Special State Validation for Web', () => {
+ it('should handle state_mismatch error with special validation', async () => {
+ const spaJsError = {
+ error: 'state_mismatch',
+ error_description: 'state is invalid',
+ };
+ mockSpaClient.handleRedirectCallback.mockRejectedValue(spaJsError);
+
+ await expect(provider.handleRedirectCallback()).rejects.toThrow(
+ WebAuthError
+ );
+
+ try {
+ await provider.handleRedirectCallback();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe('INVALID_STATE');
+ expect(err.message).toBe('state is invalid');
+ }
+ });
+
+ it('should handle error with "state is invalid" in message', async () => {
+ const spaJsError = {
+ error: 'some_other_error',
+ error_description: 'The state is invalid and cannot be processed',
+ };
+ mockSpaClient.handleRedirectCallback.mockRejectedValue(spaJsError);
+
+ await expect(provider.handleRedirectCallback()).rejects.toThrow(
+ WebAuthError
+ );
+
+ try {
+ await provider.handleRedirectCallback();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe('INVALID_STATE');
+ expect(err.message).toBe(
+ 'The state is invalid and cannot be processed'
+ );
+ }
+ });
+ });
+
+ describe('Unknown Error Handling', () => {
+ it('should handle unknown error codes with UNKNOWN_ERROR type', async () => {
+ const spaJsError = {
+ error: 'unknown_spa_error',
+ error_description: 'An unknown SPA error occurred',
+ };
+ mockSpaClient.loginWithRedirect.mockRejectedValue(spaJsError);
+
+ await expect(provider.authorize()).rejects.toThrow(WebAuthError);
+
+ try {
+ await provider.authorize();
+ } catch (e) {
+ const err = e as WebAuthError;
+ expect(err.type).toBe('UNKNOWN_ERROR');
+ expect(err.message).toBe('An unknown SPA error occurred');
+ }
+ });
+ });
+});