From 056ce89098b93e82d508968d515368d7034ea49b Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 30 Jul 2025 13:25:47 +0530 Subject: [PATCH 01/11] update(deps): update Auth0 dependency for android and iOS --- A0Auth0.podspec | 4 +- android/build.gradle | 2 +- example/ios/Podfile.lock | 132 +++++++++++++++++++-------------------- 3 files changed, 67 insertions(+), 71 deletions(-) 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/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/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 From c7c427c92a3276872b0788cf71cb86fc8c3d47a2 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 30 Jul 2025 19:06:12 +0530 Subject: [PATCH 02/11] refactor(auth): streamline error handling in CredentialsManager and update error codes --- .../java/com/auth0/react/A0Auth0Module.kt | 49 +------------------ ios/NativeBridge.swift | 12 +++-- src/core/models/CredentialsManagerError.ts | 15 ++++-- 3 files changed, 20 insertions(+), 56 deletions(-) diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index 2c96f8d5..5e210685 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -25,50 +25,11 @@ 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 val errorCodeMap = mapOf( - CredentialsManagerException.INVALID_CREDENTIALS to "INVALID_CREDENTIALS", - CredentialsManagerException.NO_CREDENTIALS to "NO_CREDENTIALS", - CredentialsManagerException.NO_REFRESH_TOKEN to "NO_REFRESH_TOKEN", - CredentialsManagerException.RENEW_FAILED to "RENEW_FAILED", - CredentialsManagerException.STORE_FAILED to "STORE_FAILED", - CredentialsManagerException.REVOKE_FAILED to "REVOKE_FAILED", - CredentialsManagerException.LARGE_MIN_TTL to "LARGE_MIN_TTL", - CredentialsManagerException.INCOMPATIBLE_DEVICE to "INCOMPATIBLE_DEVICE", - CredentialsManagerException.CRYPTO_EXCEPTION to "CRYPTO_EXCEPTION", - CredentialsManagerException.BIOMETRIC_ERROR_NO_ACTIVITY to "BIOMETRIC_NO_ACTIVITY", - CredentialsManagerException.BIOMETRIC_ERROR_STATUS_UNKNOWN to "BIOMETRIC_ERROR_STATUS_UNKNOWN", - CredentialsManagerException.BIOMETRIC_ERROR_UNSUPPORTED to "BIOMETRIC_ERROR_UNSUPPORTED", - CredentialsManagerException.BIOMETRIC_ERROR_HW_UNAVAILABLE to "BIOMETRIC_ERROR_HW_UNAVAILABLE", - CredentialsManagerException.BIOMETRIC_ERROR_NONE_ENROLLED to "BIOMETRIC_ERROR_NONE_ENROLLED", - CredentialsManagerException.BIOMETRIC_ERROR_NO_HARDWARE to "BIOMETRIC_ERROR_NO_HARDWARE", - CredentialsManagerException.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to "BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED", - CredentialsManagerException.BIOMETRIC_AUTHENTICATION_CHECK_FAILED to "BIOMETRIC_AUTHENTICATION_CHECK_FAILED", - CredentialsManagerException.BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE to "BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE", - CredentialsManagerException.BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE to "BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE", - CredentialsManagerException.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL to "BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL", - CredentialsManagerException.BIOMETRIC_ERROR_NEGATIVE_BUTTON to "BIOMETRIC_ERROR_NEGATIVE_BUTTON", - CredentialsManagerException.BIOMETRIC_ERROR_HW_NOT_PRESENT to "BIOMETRIC_ERROR_HW_NOT_PRESENT", - CredentialsManagerException.BIOMETRIC_ERROR_NO_BIOMETRICS to "BIOMETRIC_ERROR_NO_BIOMETRICS", - CredentialsManagerException.BIOMETRIC_ERROR_USER_CANCELED to "BIOMETRIC_ERROR_USER_CANCELED", - CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT_PERMANENT to "BIOMETRIC_ERROR_LOCKOUT_PERMANENT", - CredentialsManagerException.BIOMETRIC_ERROR_VENDOR to "BIOMETRIC_ERROR_VENDOR", - CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT to "BIOMETRIC_ERROR_LOCKOUT", - CredentialsManagerException.BIOMETRIC_ERROR_CANCELED to "BIOMETRIC_ERROR_CANCELED", - CredentialsManagerException.BIOMETRIC_ERROR_NO_SPACE to "BIOMETRIC_ERROR_NO_SPACE", - CredentialsManagerException.BIOMETRIC_ERROR_TIMEOUT to "BIOMETRIC_ERROR_TIMEOUT", - CredentialsManagerException.BIOMETRIC_ERROR_UNABLE_TO_PROCESS to "BIOMETRIC_ERROR_UNABLE_TO_PROCESS", - CredentialsManagerException.BIOMETRICS_INVALID_USER to "BIOMETRICS_INVALID_USER", - CredentialsManagerException.BIOMETRIC_AUTHENTICATION_FAILED to "BIOMETRIC_AUTHENTICATION_FAILED", - CredentialsManagerException.API_ERROR to "API_ERROR", - CredentialsManagerException.NO_NETWORK to "NO_NETWORK" - ) - private var auth0: Auth0? = null private lateinit var secureCredentialsManager: SecureCredentialsManager private var webAuthPromise: Promise? = null @@ -222,8 +183,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } override fun onFailure(e: CredentialsManagerException) { - val errorCode = deduceErrorCode(e) - promise.reject(errorCode, e.message, e) + promise.reject(e.code, e.message, e) } } ) @@ -236,8 +196,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 secureCredentialsManager.saveCredentials(CredentialsParser.fromMap(credentials)) promise.resolve(true) } catch (e: CredentialsManagerException) { - val errorCode = deduceErrorCode(e) - promise.reject(errorCode, e.message, e) + promise.reject(e.code, e.message, e) } } @@ -312,10 +271,6 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 ) } - private fun deduceErrorCode(e: CredentialsManagerException): String { - return errorCodeMap[e] ?: CREDENTIAL_MANAGER_ERROR_CODE - } - private fun handleError(error: AuthenticationException, promise: Promise) { when { error.isBrowserAppNotAvailable -> { diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift index 702a596f..04198f2e 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)); } @@ -276,7 +280,7 @@ extension CredentialsManagerError { code = "REVOKE_FAILED" } case .largeMinTTL: code = "LARGE_MIN_TTL" - default: code = "UNKNOWN" + default: code = NativeBridge.credentialsManagerErrorCode } return code } diff --git a/src/core/models/CredentialsManagerError.ts b/src/core/models/CredentialsManagerError.ts index c8174b7c..026b4e3a 100644 --- a/src/core/models/CredentialsManagerError.ts +++ b/src/core/models/CredentialsManagerError.ts @@ -1,6 +1,11 @@ import { AuthError } from './AuthError'; const ERROR_CODE_MAP: Record = { + // --- Android-only codes that have no iOS equivalent --- + INCOMPATIBLE_DEVICE: 'INCOMPATIBLE_DEVICE', + CRYPTO_EXCEPTION: 'CRYPTO_EXCEPTION', + + // --- Self-mappings (acts as an allow-list of known codes) --- INVALID_CREDENTIALS: 'INVALID_CREDENTIALS', NO_CREDENTIALS: 'NO_CREDENTIALS', NO_REFRESH_TOKEN: 'NO_REFRESH_TOKEN', @@ -8,8 +13,11 @@ const ERROR_CODE_MAP: Record = { STORE_FAILED: 'STORE_FAILED', REVOKE_FAILED: 'REVOKE_FAILED', LARGE_MIN_TTL: 'LARGE_MIN_TTL', - INCOMPATIBLE_DEVICE: 'INCOMPATIBLE_DEVICE', - CRYPTO_EXCEPTION: 'CRYPTO_EXCEPTION', + BIOMETRICS_FAILED: 'BIOMETRICS_FAILED', + NO_NETWORK: 'NO_NETWORK', + API_ERROR: 'API_ERROR', + + // --- Many-to-one mapping for granular Android Biometric errors --- BIOMETRIC_NO_ACTIVITY: 'BIOMETRICS_FAILED', BIOMETRIC_ERROR_STATUS_UNKNOWN: 'BIOMETRICS_FAILED', BIOMETRIC_ERROR_UNSUPPORTED: 'BIOMETRICS_FAILED', @@ -35,9 +43,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 { From 9e5ebb746e9d1cc3ebaae4b123b0966ceb521f49 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Sat, 9 Aug 2025 13:49:05 +0530 Subject: [PATCH 03/11] feat(auth): implement platform-agnostic error handling with WebAuthError class --- README.md | 63 ++++++++++++++++++- src/core/models/WebAuthError.ts | 45 +++++++++++++ src/core/models/index.ts | 2 + .../adapters/NativeCredentialsManager.ts | 2 +- .../native/adapters/NativeWebAuthProvider.ts | 51 ++++++++------- .../native/bridge/NativeBridgeManager.ts | 2 + 6 files changed, 139 insertions(+), 26 deletions(-) create mode 100644 src/core/models/WebAuthError.ts diff --git a/README.md b/README.md index e0185cde..7dee022e 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,8 +649,6 @@ 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` | | @@ -666,6 +664,65 @@ _Note_ : We have platform agnostic error codes available only for `CredentialsMa | `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 | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------- | +| `USER_CANCELLED` | The user actively cancelled the web authentication flow. | `a0.session.user_cancelled` | `USER_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` | +| `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` | +| `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` | +| `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) | +| `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 This library provides a unified API across Native (iOS/Android) and Web platforms. However, due to security models and underlying technology, not all features are available on every platform. diff --git a/src/core/models/WebAuthError.ts b/src/core/models/WebAuthError.ts new file mode 100644 index 00000000..dc45aeb7 --- /dev/null +++ b/src/core/models/WebAuthError.ts @@ -0,0 +1,45 @@ +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', + + // --- 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', + 'NO_AUTHORIZATION_CODE': 'NO_AUTHORIZATION_CODE', + 'PKCE_NOT_ALLOWED': 'PKCE_NOT_ALLOWED', + 'INVALID_INVITATION_URL': 'INVALID_INVITATION_URL', + + // --- 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')) { + 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..8328c9c4 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 { 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..e587cca5 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 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/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts index c95d688a..464300f3 100644 --- a/src/platforms/native/bridge/NativeBridgeManager.ts +++ b/src/platforms/native/bridge/NativeBridgeManager.ts @@ -13,6 +13,8 @@ import { import { AuthError, Credentials as CredentialsModel, + CredentialsManagerError, + WebAuthError, } from '../../../core/models'; import Auth0NativeModule from '../../../specs/NativeA0Auth0'; From d9694f766c5a2598ebfe19751886619482cfffeb Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Sat, 9 Aug 2025 20:28:33 +0530 Subject: [PATCH 04/11] feat(errors): enhance error handling with additional mappings for web --- README.md | 61 ++++++++--------- src/core/models/CredentialsManagerError.ts | 6 ++ src/core/models/WebAuthError.ts | 11 +++- .../web/adapters/WebCredentialsManager.ts | 24 ++++--- .../web/adapters/WebWebAuthProvider.ts | 65 ++++++++++++------- 5 files changed, 104 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 7dee022e..026e6883 100644 --- a/README.md +++ b/README.md @@ -649,20 +649,20 @@ try { } ``` -| 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` | +| `STORE_FAILED` | `STORE_FAILED` | `storeFailed` | `invalid_grant`, `consent_required` | +| `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 @@ -706,22 +706,23 @@ try { } ``` -| Platform-Agnostic | Description | Android Native Error | iOS Native Error | -| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------- | -| `USER_CANCELLED` | The user actively cancelled the web authentication flow. | `a0.session.user_cancelled` | `USER_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` | -| `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` | -| `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` | -| `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) | -| `UNKNOWN_ERROR` | An unexpected or uncategorized error occurred. Check the `message` and `cause` properties for more specific details. | _(various)_ | `UNKNOWN` or `OTHER` | +| 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` | +| `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/src/core/models/CredentialsManagerError.ts b/src/core/models/CredentialsManagerError.ts index 026b4e3a..8644a43b 100644 --- a/src/core/models/CredentialsManagerError.ts +++ b/src/core/models/CredentialsManagerError.ts @@ -17,6 +17,12 @@ const ERROR_CODE_MAP: Record = { NO_NETWORK: 'NO_NETWORK', API_ERROR: 'API_ERROR', + // --- Web (@auth0/auth0-spa-js) mappings --- + login_required: 'NO_CREDENTIALS', + consent_required: 'RENEW_FAILED', + invalid_grant: 'RENEW_FAILED', + missing_refresh_token: 'NO_REFRESH_TOKEN', + // --- Many-to-one mapping for granular Android Biometric errors --- BIOMETRIC_NO_ACTIVITY: 'BIOMETRICS_FAILED', BIOMETRIC_ERROR_STATUS_UNKNOWN: 'BIOMETRICS_FAILED', diff --git a/src/core/models/WebAuthError.ts b/src/core/models/WebAuthError.ts index dc45aeb7..ef4e8199 100644 --- a/src/core/models/WebAuthError.ts +++ b/src/core/models/WebAuthError.ts @@ -20,6 +20,12 @@ const ERROR_CODE_MAP: Record = { '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', + // --- Generic Fallbacks --- 'a0.invalid_configuration': 'INVALID_CONFIGURATION', 'UNKNOWN': 'UNKNOWN_ERROR', @@ -36,7 +42,10 @@ export class WebAuthError extends AuthError { json: originalError.json, }); - if (originalError.message.includes('state is invalid')) { + 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/platforms/web/adapters/WebCredentialsManager.ts b/src/platforms/web/adapters/WebCredentialsManager.ts index f9044913..3cae6d2e 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( + const authError = new AuthError( e.error ?? 'GetCredentialsFailed', e.error_description ?? e.message, { json: e } ); + 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 authError = new AuthError( + e.error ?? 'ClearCredentialsFailed', + e.error_description ?? e.message, + { json: e } + ); + throw new CredentialsManagerError(authError); + } } } diff --git a/src/platforms/web/adapters/WebWebAuthProvider.ts b/src/platforms/web/adapters/WebWebAuthProvider.ts index cc85bd63..1219c55d 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,41 @@ 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) { + throw new WebAuthError( + new AuthError( + e.error ?? 'AuthorizeFailed', + e.error_description ?? e.message, + { json: e } + ) + ); + } } 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 } + throw new WebAuthError( + new AuthError( + e.error ?? 'RedirectCallbackError', + e.error_description ?? e.message, + { json: e } + ) ); } } @@ -52,10 +64,19 @@ export class WebWebAuthProvider implements IWebAuthProvider { }, }); } catch (e: any) { - throw new AuthError( - e.error ?? 'LogoutFailed', - e.error_description ?? e.message, - { json: e } + if ((e as PopupCancelledError).error === 'cancelled') { + throw new WebAuthError( + new AuthError('cancelled', 'User cancelled the logout popup.', { + json: e, + }) + ); + } + throw new WebAuthError( + new AuthError( + e.error ?? 'LogoutFailed', + e.error_description ?? e.message, + { json: e } + ) ); } } From f52b0b7e19186a7dc3f016e44896e9d3eb83bcc9 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Sat, 9 Aug 2025 21:02:41 +0530 Subject: [PATCH 05/11] refactor(tests): remove redundant error handling tests and update mock implementations --- .../native/bridge/NativeBridgeManager.ts | 2 - .../__tests__/WebCredentialsManager.spec.ts | 41 ++++++------------- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts index 464300f3..c95d688a 100644 --- a/src/platforms/native/bridge/NativeBridgeManager.ts +++ b/src/platforms/native/bridge/NativeBridgeManager.ts @@ -13,8 +13,6 @@ import { import { AuthError, Credentials as CredentialsModel, - CredentialsManagerError, - WebAuthError, } from '../../../core/models'; import Auth0NativeModule from '../../../specs/NativeA0Auth0'; 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', From e1598b3ab35e0844326da745a4383b3f4ec9730f Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Tue, 12 Aug 2025 22:31:03 +0530 Subject: [PATCH 06/11] Revert "refactor(auth): streamline error handling in CredentialsManager and update error codes" This reverts commit c7c427c92a3276872b0788cf71cb86fc8c3d47a2. --- .../java/com/auth0/react/A0Auth0Module.kt | 49 ++++++++++++++++++- ios/NativeBridge.swift | 2 +- src/core/models/CredentialsManagerError.ts | 7 +-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index 5e210685..2c96f8d5 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -25,11 +25,50 @@ 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 val errorCodeMap = mapOf( + CredentialsManagerException.INVALID_CREDENTIALS to "INVALID_CREDENTIALS", + CredentialsManagerException.NO_CREDENTIALS to "NO_CREDENTIALS", + CredentialsManagerException.NO_REFRESH_TOKEN to "NO_REFRESH_TOKEN", + CredentialsManagerException.RENEW_FAILED to "RENEW_FAILED", + CredentialsManagerException.STORE_FAILED to "STORE_FAILED", + CredentialsManagerException.REVOKE_FAILED to "REVOKE_FAILED", + CredentialsManagerException.LARGE_MIN_TTL to "LARGE_MIN_TTL", + CredentialsManagerException.INCOMPATIBLE_DEVICE to "INCOMPATIBLE_DEVICE", + CredentialsManagerException.CRYPTO_EXCEPTION to "CRYPTO_EXCEPTION", + CredentialsManagerException.BIOMETRIC_ERROR_NO_ACTIVITY to "BIOMETRIC_NO_ACTIVITY", + CredentialsManagerException.BIOMETRIC_ERROR_STATUS_UNKNOWN to "BIOMETRIC_ERROR_STATUS_UNKNOWN", + CredentialsManagerException.BIOMETRIC_ERROR_UNSUPPORTED to "BIOMETRIC_ERROR_UNSUPPORTED", + CredentialsManagerException.BIOMETRIC_ERROR_HW_UNAVAILABLE to "BIOMETRIC_ERROR_HW_UNAVAILABLE", + CredentialsManagerException.BIOMETRIC_ERROR_NONE_ENROLLED to "BIOMETRIC_ERROR_NONE_ENROLLED", + CredentialsManagerException.BIOMETRIC_ERROR_NO_HARDWARE to "BIOMETRIC_ERROR_NO_HARDWARE", + CredentialsManagerException.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to "BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED", + CredentialsManagerException.BIOMETRIC_AUTHENTICATION_CHECK_FAILED to "BIOMETRIC_AUTHENTICATION_CHECK_FAILED", + CredentialsManagerException.BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE to "BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE", + CredentialsManagerException.BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE to "BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE", + CredentialsManagerException.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL to "BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL", + CredentialsManagerException.BIOMETRIC_ERROR_NEGATIVE_BUTTON to "BIOMETRIC_ERROR_NEGATIVE_BUTTON", + CredentialsManagerException.BIOMETRIC_ERROR_HW_NOT_PRESENT to "BIOMETRIC_ERROR_HW_NOT_PRESENT", + CredentialsManagerException.BIOMETRIC_ERROR_NO_BIOMETRICS to "BIOMETRIC_ERROR_NO_BIOMETRICS", + CredentialsManagerException.BIOMETRIC_ERROR_USER_CANCELED to "BIOMETRIC_ERROR_USER_CANCELED", + CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT_PERMANENT to "BIOMETRIC_ERROR_LOCKOUT_PERMANENT", + CredentialsManagerException.BIOMETRIC_ERROR_VENDOR to "BIOMETRIC_ERROR_VENDOR", + CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT to "BIOMETRIC_ERROR_LOCKOUT", + CredentialsManagerException.BIOMETRIC_ERROR_CANCELED to "BIOMETRIC_ERROR_CANCELED", + CredentialsManagerException.BIOMETRIC_ERROR_NO_SPACE to "BIOMETRIC_ERROR_NO_SPACE", + CredentialsManagerException.BIOMETRIC_ERROR_TIMEOUT to "BIOMETRIC_ERROR_TIMEOUT", + CredentialsManagerException.BIOMETRIC_ERROR_UNABLE_TO_PROCESS to "BIOMETRIC_ERROR_UNABLE_TO_PROCESS", + CredentialsManagerException.BIOMETRICS_INVALID_USER to "BIOMETRICS_INVALID_USER", + CredentialsManagerException.BIOMETRIC_AUTHENTICATION_FAILED to "BIOMETRIC_AUTHENTICATION_FAILED", + CredentialsManagerException.API_ERROR to "API_ERROR", + CredentialsManagerException.NO_NETWORK to "NO_NETWORK" + ) + private var auth0: Auth0? = null private lateinit var secureCredentialsManager: SecureCredentialsManager private var webAuthPromise: Promise? = null @@ -183,7 +222,8 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } override fun onFailure(e: CredentialsManagerException) { - promise.reject(e.code, e.message, e) + val errorCode = deduceErrorCode(e) + promise.reject(errorCode, e.message, e) } } ) @@ -196,7 +236,8 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 secureCredentialsManager.saveCredentials(CredentialsParser.fromMap(credentials)) promise.resolve(true) } catch (e: CredentialsManagerException) { - promise.reject(e.code, e.message, e) + val errorCode = deduceErrorCode(e) + promise.reject(errorCode, e.message, e) } } @@ -271,6 +312,10 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 ) } + private fun deduceErrorCode(e: CredentialsManagerException): String { + return errorCodeMap[e] ?: CREDENTIAL_MANAGER_ERROR_CODE + } + private fun handleError(error: AuthenticationException, promise: Promise) { when { error.isBrowserAppNotAvailable -> { diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift index 04198f2e..0987998a 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -280,7 +280,7 @@ extension CredentialsManagerError { code = "REVOKE_FAILED" } case .largeMinTTL: code = "LARGE_MIN_TTL" - default: code = NativeBridge.credentialsManagerErrorCode + default: code = "UNKNOWN" } return code } diff --git a/src/core/models/CredentialsManagerError.ts b/src/core/models/CredentialsManagerError.ts index 8644a43b..a57c348f 100644 --- a/src/core/models/CredentialsManagerError.ts +++ b/src/core/models/CredentialsManagerError.ts @@ -1,11 +1,6 @@ import { AuthError } from './AuthError'; const ERROR_CODE_MAP: Record = { - // --- Android-only codes that have no iOS equivalent --- - INCOMPATIBLE_DEVICE: 'INCOMPATIBLE_DEVICE', - CRYPTO_EXCEPTION: 'CRYPTO_EXCEPTION', - - // --- Self-mappings (acts as an allow-list of known codes) --- INVALID_CREDENTIALS: 'INVALID_CREDENTIALS', NO_CREDENTIALS: 'NO_CREDENTIALS', NO_REFRESH_TOKEN: 'NO_REFRESH_TOKEN', @@ -24,6 +19,8 @@ const ERROR_CODE_MAP: Record = { 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', BIOMETRIC_ERROR_STATUS_UNKNOWN: 'BIOMETRICS_FAILED', BIOMETRIC_ERROR_UNSUPPORTED: 'BIOMETRICS_FAILED', From 27949a230b7304aac80c269770234aed2595d988 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Tue, 19 Aug 2025 09:59:11 +0530 Subject: [PATCH 07/11] feat(errors): update error codes for CredentialsManager and WebAuth to improve clarity --- android/src/main/java/com/auth0/react/A0Auth0Module.kt | 7 ++----- src/core/models/CredentialsManagerError.ts | 1 + src/core/models/WebAuthError.ts | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) 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/src/core/models/CredentialsManagerError.ts b/src/core/models/CredentialsManagerError.ts index a57c348f..0fc17f2e 100644 --- a/src/core/models/CredentialsManagerError.ts +++ b/src/core/models/CredentialsManagerError.ts @@ -8,6 +8,7 @@ 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', diff --git a/src/core/models/WebAuthError.ts b/src/core/models/WebAuthError.ts index ef4e8199..60add3a1 100644 --- a/src/core/models/WebAuthError.ts +++ b/src/core/models/WebAuthError.ts @@ -8,6 +8,7 @@ const ERROR_CODE_MAP: Record = { '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', From ed0a338eabef62afa1ef90126512c9059991b0fd Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 20 Aug 2025 18:10:57 +0530 Subject: [PATCH 08/11] feat(errors): add new error codes for transaction handling and MFA requirements --- README.md | 27 +++++++++++----------- ios/NativeBridge.swift | 1 + src/core/models/CredentialsManagerError.ts | 2 ++ src/core/models/WebAuthError.ts | 2 ++ 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 026e6883..685462e9 100644 --- a/README.md +++ b/README.md @@ -649,20 +649,20 @@ try { } ``` -| Generic Error Code | Android Native Error | iOS Native Error | Web Error Code | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | ----------------------------------- | -| `INVALID_CREDENTIALS` | `INVALID_CREDENTIALS` | | | -| `NO_CREDENTIALS` | `NO_CREDENTIALS` | `noCredentials` | `login_required` | +| 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` | -| `STORE_FAILED` | `STORE_FAILED` | `storeFailed` | `invalid_grant`, `consent_required` | -| `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` | | | +| `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 @@ -715,6 +715,7 @@ try { | `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` | | diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift index 0987998a..102abcb6 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -245,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/models/CredentialsManagerError.ts b/src/core/models/CredentialsManagerError.ts index 0fc17f2e..230152a9 100644 --- a/src/core/models/CredentialsManagerError.ts +++ b/src/core/models/CredentialsManagerError.ts @@ -16,7 +16,9 @@ const ERROR_CODE_MAP: Record = { // --- 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 --- diff --git a/src/core/models/WebAuthError.ts b/src/core/models/WebAuthError.ts index 60add3a1..494159c2 100644 --- a/src/core/models/WebAuthError.ts +++ b/src/core/models/WebAuthError.ts @@ -17,6 +17,7 @@ const ERROR_CODE_MAP: Record = { // --- 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', @@ -26,6 +27,7 @@ const ERROR_CODE_MAP: Record = { 'state_mismatch': 'INVALID_STATE', 'login_required': 'ACCESS_DENIED', 'timeout': 'TIMEOUT_ERROR', + 'consent_required': 'CONSENT_REQUIRED', // --- Generic Fallbacks --- 'a0.invalid_configuration': 'INVALID_CONFIGURATION', From 5323a44d78b55c1a0927a2a73fcc355687fb630f Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 20 Aug 2025 22:46:45 +0530 Subject: [PATCH 09/11] refactor(errors): streamline error handling in WebCredentialsManager and WebWebAuthProvider --- .../web/adapters/WebCredentialsManager.ts | 20 ++++++++--------- .../web/adapters/WebWebAuthProvider.ts | 22 ++++++------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/platforms/web/adapters/WebCredentialsManager.ts b/src/platforms/web/adapters/WebCredentialsManager.ts index 3cae6d2e..b29b6e75 100644 --- a/src/platforms/web/adapters/WebCredentialsManager.ts +++ b/src/platforms/web/adapters/WebCredentialsManager.ts @@ -46,11 +46,11 @@ export class WebCredentialsManager implements ICredentialsManager { scope: tokenResponse.scope, }); } catch (e: any) { - const authError = 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); } } @@ -63,11 +63,11 @@ export class WebCredentialsManager implements ICredentialsManager { try { await this.client.logout({ openUrl: false }); } catch (e: any) { - const authError = new AuthError( - e.error ?? 'ClearCredentialsFailed', - e.error_description ?? e.message, - { json: e } - ); + 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 1219c55d..029ca47b 100644 --- a/src/platforms/web/adapters/WebWebAuthProvider.ts +++ b/src/platforms/web/adapters/WebWebAuthProvider.ts @@ -31,12 +31,9 @@ export class WebWebAuthProvider implements IWebAuthProvider { // 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( - e.error ?? 'AuthorizeFailed', - e.error_description ?? e.message, - { json: e } - ) + new AuthError(code, e.error_description ?? e.message, { json: e, code }) ); } } @@ -45,12 +42,9 @@ export class WebWebAuthProvider implements IWebAuthProvider { try { await this.client.handleRedirectCallback(url); } catch (e: any) { + const code = e.error ?? 'RedirectCallbackError'; throw new WebAuthError( - new AuthError( - e.error ?? 'RedirectCallbackError', - e.error_description ?? e.message, - { json: e } - ) + new AuthError(code, e.error_description ?? e.message, { json: e, code }) ); } } @@ -64,19 +58,17 @@ export class WebWebAuthProvider implements IWebAuthProvider { }, }); } catch (e: any) { + const code = e.error ?? 'LogoutFailed'; if ((e as PopupCancelledError).error === 'cancelled') { throw new WebAuthError( new AuthError('cancelled', 'User cancelled the logout popup.', { json: e, + code, }) ); } throw new WebAuthError( - new AuthError( - e.error ?? 'LogoutFailed', - e.error_description ?? e.message, - { json: e } - ) + new AuthError(code, e.error_description ?? e.message, { json: e, code }) ); } } From 3180ee82c071d1ff0db9178376c16bb99bdaf878 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 20 Aug 2025 23:58:58 +0530 Subject: [PATCH 10/11] feat(errors): add NativeModuleError type and implement error handling in NativeCredentialsManager and NativeWebAuthProvider --- src/core/interfaces/common.ts | 4 + src/core/interfaces/index.ts | 1 + .../adapters/NativeCredentialsManager.ts | 2 +- .../native/adapters/NativeWebAuthProvider.ts | 2 +- .../NativeCredentialsManager.errors.spec.ts | 84 +++++++++++++++++++ .../NativeWebAuthProvider.errors.spec.ts | 74 ++++++++++++++++ .../native/bridge/NativeBridgeManager.ts | 8 +- .../WebCredentialsManager.errors.spec.ts | 71 ++++++++++++++++ .../WebWebAuthProvider.errors.spec.ts | 67 +++++++++++++++ 9 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 src/core/interfaces/common.ts create mode 100644 src/platforms/native/adapters/__tests__/NativeCredentialsManager.errors.spec.ts create mode 100644 src/platforms/native/adapters/__tests__/NativeWebAuthProvider.errors.spec.ts create mode 100644 src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts create mode 100644 src/platforms/web/adapters/__tests__/WebWebAuthProvider.errors.spec.ts 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/platforms/native/adapters/NativeCredentialsManager.ts b/src/platforms/native/adapters/NativeCredentialsManager.ts index 8328c9c4..46044e89 100644 --- a/src/platforms/native/adapters/NativeCredentialsManager.ts +++ b/src/platforms/native/adapters/NativeCredentialsManager.ts @@ -1,5 +1,5 @@ import type { ICredentialsManager } from '../../../core/interfaces'; -import type { AuthError } from '../../../core/models'; +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 e587cca5..f323b58f 100644 --- a/src/platforms/native/adapters/NativeWebAuthProvider.ts +++ b/src/platforms/native/adapters/NativeWebAuthProvider.ts @@ -101,7 +101,7 @@ export class NativeWebAuthProvider implements IWebAuthProvider { }; // 4. Call the bridge with the two separate, finalized objects. - return this.bridge.clearSession(finalParameters, finalOptions); + return await this.bridge.clearSession(finalParameters, finalOptions); } catch (error) { throw new WebAuthError(error as AuthError); } 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..0075549e --- /dev/null +++ b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.errors.spec.ts @@ -0,0 +1,84 @@ +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('NativeCredentialsManager Error Handling', () => { + let manager: NativeCredentialsManager; + + beforeEach(() => { + jest.clearAllMocks(); + manager = new NativeCredentialsManager(mockBridge); + }); + + it('should convert an Android "NO_CREDENTIALS" error from getCredentials into a CredentialsManagerError', async () => { + const nativeError = { + code: 'NO_CREDENTIALS', + message: 'No credentials stored.', + }; + 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('NO_CREDENTIALS'); + expect(err.message).toBe('No credentials stored.'); + } + }); + + it('should convert an iOS "noRefreshToken" error from getCredentials into a CredentialsManagerError', async () => { + const nativeError = { + code: 'NO_REFRESH_TOKEN', + message: 'No refresh token available.', + }; + mockBridge.getCredentials.mockRejectedValue(nativeError); + + await expect(manager.getCredentials()).rejects.toThrow( + CredentialsManagerError + ); + try { + await manager.getCredentials(); + } catch (e) { + const err = e as CredentialsManagerError; + // Note: The mapping for iOS's 'noRefreshToken' should resolve to 'NO_REFRESH_TOKEN' + expect(err.type).toBe('NO_REFRESH_TOKEN'); + } + }); + + it('should convert an Android "BIOMETRIC_ERROR_LOCKOUT" error from getCredentials into a CredentialsManagerError with type "BIOMETRICS_FAILED"', async () => { + const nativeError = { + code: 'BIOMETRIC_ERROR_LOCKOUT', + message: 'Biometrics are locked.', + }; + 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'); + } + }); +}); 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..bd758984 --- /dev/null +++ b/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.errors.spec.ts @@ -0,0 +1,74 @@ +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'); + }); + + it('should convert an Android "user_cancelled" error from authorize into a WebAuthError', async () => { + const nativeError = { + code: 'a0.session.user_cancelled', + message: 'User cancelled.', + }; + 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('USER_CANCELLED'); + expect(err.message).toBe('User cancelled.'); + } + }); + + it('should convert an iOS "USER_CANCELLED" error from authorize into a WebAuthError', async () => { + const nativeError = { code: 'USER_CANCELLED', message: 'User cancelled.' }; + 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('USER_CANCELLED'); + } + }); + + it('should convert a "BROWSER_NOT_AVAILABLE" error from clearSession into a WebAuthError', async () => { + const nativeError = { + code: 'a0.browser_not_available', + message: 'No browser.', + }; + mockBridge.clearSession.mockRejectedValue(nativeError); + + await expect(provider.clearSession()).rejects.toThrow(WebAuthError); + try { + await provider.clearSession(); + } catch (e) { + const err = e as WebAuthError; + expect(err.type).toBe('BROWSER_NOT_AVAILABLE'); + } + }); +}); 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/__tests__/WebCredentialsManager.errors.spec.ts b/src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts new file mode 100644 index 00000000..eeafd2c5 --- /dev/null +++ b/src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts @@ -0,0 +1,71 @@ +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); + }); + + it('should convert a "login_required" error into a CredentialsManagerError with type "NO_CREDENTIALS"', async () => { + const spaJsError = { + error: 'login_required', + error_description: 'Login is required', + }; + 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('NO_CREDENTIALS'); + expect(err.message).toBe('Login is required'); + } + }); + + it('should convert an "invalid_grant" error into a CredentialsManagerError with type "RENEW_FAILED"', async () => { + const spaJsError = { + error: 'invalid_grant', + error_description: 'Invalid refresh token', + }; + 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('RENEW_FAILED'); + } + }); + + it('should convert a "consent_required" error into a CredentialsManagerError with type "RENEW_FAILED"', async () => { + const spaJsError = { + error: 'consent_required', + error_description: 'User consent is required', + }; + 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('RENEW_FAILED'); + } + }); +}); 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..1a9e3001 --- /dev/null +++ b/src/platforms/web/adapters/__tests__/WebWebAuthProvider.errors.spec.ts @@ -0,0 +1,67 @@ +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); + }); + + it('should convert an "access_denied" error from clearSession into a WebAuthError with type "ACCESS_DENIED"', async () => { + const spaJsError = { + error: 'access_denied', + error_description: 'Logout denied', + }; + mockSpaClient.logout.mockRejectedValue(spaJsError); + await expect(provider.clearSession()).rejects.toThrow(WebAuthError); + try { + await provider.clearSession(); + } catch (e) { + const err = e as WebAuthError; + expect(err.type).toBe('ACCESS_DENIED'); + expect(err.message).toBe('Logout denied'); + } + }); + + it('should convert a "cancelled" error from clearSession into a WebAuthError with type "USER_CANCELLED"', async () => { + const spaJsError = { + error: 'cancelled', + error_description: 'Popup closed', + }; + mockSpaClient.logout.mockRejectedValue(spaJsError); + await expect(provider.clearSession()).rejects.toThrow(WebAuthError); + try { + await provider.clearSession(); + } catch (e) { + const err = e as WebAuthError; + expect(err.type).toBe('USER_CANCELLED'); + } + }); + + it('should convert a "state_mismatch" error from handleRedirectCallback into a WebAuthError with type "INVALID_STATE"', async () => { + const spaJsError = { + error: 'state_mismatch', + error_description: 'Invalid state', + }; + 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'); + } + }); +}); From 9cfd123e1972b58756e5be54caf03ddeb1d5a0c2 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Sun, 24 Aug 2025 20:58:40 +0530 Subject: [PATCH 11/11] adding test cases to validate each and every error from credentialmanager and webautherror --- .../NativeCredentialsManager.errors.spec.ts | 225 +++++++++--- .../NativeWebAuthProvider.errors.spec.ts | 319 +++++++++++++++--- .../web/adapters/WebWebAuthProvider.ts | 14 +- .../WebCredentialsManager.errors.spec.ts | 98 +++--- .../WebWebAuthProvider.errors.spec.ts | 170 +++++++--- 5 files changed, 645 insertions(+), 181 deletions(-) diff --git a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.errors.spec.ts b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.errors.spec.ts index 0075549e..23a15982 100644 --- a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.errors.spec.ts +++ b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.errors.spec.ts @@ -18,67 +18,198 @@ const mockBridge: jest.Mocked = { resumeWebAuth: jest.fn(), }; -describe('NativeCredentialsManager Error Handling', () => { +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', + }, + ]; - it('should convert an Android "NO_CREDENTIALS" error from getCredentials into a CredentialsManagerError', async () => { - const nativeError = { - code: 'NO_CREDENTIALS', - message: 'No credentials stored.', - }; - mockBridge.getCredentials.mockRejectedValue(nativeError); + 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.getCredentials()).rejects.toThrow( - CredentialsManagerError + 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); + } + }); + } ); - try { - await manager.getCredentials(); - } catch (e) { - const err = e as CredentialsManagerError; - expect(err.type).toBe('NO_CREDENTIALS'); - expect(err.message).toBe('No credentials stored.'); - } }); - it('should convert an iOS "noRefreshToken" error from getCredentials into a CredentialsManagerError', async () => { - const nativeError = { - code: 'NO_REFRESH_TOKEN', - message: 'No refresh token available.', - }; - mockBridge.getCredentials.mockRejectedValue(nativeError); + 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', + }, + ]; - await expect(manager.getCredentials()).rejects.toThrow( - CredentialsManagerError - ); - try { - await manager.getCredentials(); - } catch (e) { - const err = e as CredentialsManagerError; - // Note: The mapping for iOS's 'noRefreshToken' should resolve to 'NO_REFRESH_TOKEN' - expect(err.type).toBe('NO_REFRESH_TOKEN'); - } + 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); + } + }); + }); }); - it('should convert an Android "BIOMETRIC_ERROR_LOCKOUT" error from getCredentials into a CredentialsManagerError with type "BIOMETRICS_FAILED"', async () => { - const nativeError = { - code: 'BIOMETRIC_ERROR_LOCKOUT', - message: 'Biometrics are locked.', - }; - mockBridge.getCredentials.mockRejectedValue(nativeError); + 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', + ]; - await expect(manager.getCredentials()).rejects.toThrow( - CredentialsManagerError - ); - try { - await manager.getCredentials(); - } catch (e) { - const err = e as CredentialsManagerError; - expect(err.type).toBe('BIOMETRICS_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 index bd758984..1b6735d8 100644 --- a/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.errors.spec.ts +++ b/src/platforms/native/adapters/__tests__/NativeWebAuthProvider.errors.spec.ts @@ -26,49 +26,288 @@ describe('NativeWebAuthProvider Error Handling', () => { provider = new NativeWebAuthProvider(mockBridge, 'test.auth0.com'); }); - it('should convert an Android "user_cancelled" error from authorize into a WebAuthError', async () => { - const nativeError = { - code: 'a0.session.user_cancelled', - message: 'User cancelled.', - }; - 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('USER_CANCELLED'); - expect(err.message).toBe('User cancelled.'); - } + 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); + } + }); + } + ); }); - it('should convert an iOS "USER_CANCELLED" error from authorize into a WebAuthError', async () => { - const nativeError = { code: 'USER_CANCELLED', message: 'User cancelled.' }; - 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('USER_CANCELLED'); - } + 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); + } + }); + } + ); }); - it('should convert a "BROWSER_NOT_AVAILABLE" error from clearSession into a WebAuthError', async () => { - const nativeError = { - code: 'a0.browser_not_available', - message: 'No browser.', - }; - mockBridge.clearSession.mockRejectedValue(nativeError); - - await expect(provider.clearSession()).rejects.toThrow(WebAuthError); - try { - await provider.clearSession(); - } catch (e) { - const err = e as WebAuthError; - expect(err.type).toBe('BROWSER_NOT_AVAILABLE'); - } + 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/web/adapters/WebWebAuthProvider.ts b/src/platforms/web/adapters/WebWebAuthProvider.ts index 029ca47b..0be8bc40 100644 --- a/src/platforms/web/adapters/WebWebAuthProvider.ts +++ b/src/platforms/web/adapters/WebWebAuthProvider.ts @@ -61,10 +61,16 @@ export class WebWebAuthProvider implements IWebAuthProvider { const code = e.error ?? 'LogoutFailed'; if ((e as PopupCancelledError).error === 'cancelled') { throw new WebAuthError( - new AuthError('cancelled', 'User cancelled the logout popup.', { - json: e, - code, - }) + new AuthError( + 'cancelled', + e.error_description ?? + e.message ?? + 'User cancelled the logout popup.', + { + json: e, + code, + } + ) ); } throw new WebAuthError( diff --git a/src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts b/src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts index eeafd2c5..62b4b31d 100644 --- a/src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts +++ b/src/platforms/web/adapters/__tests__/WebCredentialsManager.errors.spec.ts @@ -17,55 +17,57 @@ describe('WebCredentialsManager Error Handling', () => { manager = new WebCredentialsManager(mockSpaClient); }); - it('should convert a "login_required" error into a CredentialsManagerError with type "NO_CREDENTIALS"', async () => { - const spaJsError = { - error: 'login_required', - error_description: 'Login is required', - }; - 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('NO_CREDENTIALS'); - expect(err.message).toBe('Login is required'); - } - }); + 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', + }, + ]; - it('should convert an "invalid_grant" error into a CredentialsManagerError with type "RENEW_FAILED"', async () => { - const spaJsError = { - error: 'invalid_grant', - error_description: 'Invalid refresh token', - }; - 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('RENEW_FAILED'); - } - }); + webErrorTestCases.forEach(({ code, message, expectedType }) => { + it(`should map ${code} to ${expectedType}`, async () => { + const spaJsError = { error: code, error_description: message }; + mockSpaClient.getTokenSilently.mockRejectedValue(spaJsError); - it('should convert a "consent_required" error into a CredentialsManagerError with type "RENEW_FAILED"', async () => { - const spaJsError = { - error: 'consent_required', - error_description: 'User consent is required', - }; - 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('RENEW_FAILED'); - } + 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__/WebWebAuthProvider.errors.spec.ts b/src/platforms/web/adapters/__tests__/WebWebAuthProvider.errors.spec.ts index 1a9e3001..dc14a71d 100644 --- a/src/platforms/web/adapters/__tests__/WebWebAuthProvider.errors.spec.ts +++ b/src/platforms/web/adapters/__tests__/WebWebAuthProvider.errors.spec.ts @@ -17,51 +17,137 @@ describe('WebWebAuthProvider Error Handling', () => { provider = new WebWebAuthProvider(mockSpaClient); }); - it('should convert an "access_denied" error from clearSession into a WebAuthError with type "ACCESS_DENIED"', async () => { - const spaJsError = { - error: 'access_denied', - error_description: 'Logout denied', - }; - mockSpaClient.logout.mockRejectedValue(spaJsError); - await expect(provider.clearSession()).rejects.toThrow(WebAuthError); - try { - await provider.clearSession(); - } catch (e) { - const err = e as WebAuthError; - expect(err.type).toBe('ACCESS_DENIED'); - expect(err.message).toBe('Logout denied'); - } + 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); + } + }); + } + ); }); - it('should convert a "cancelled" error from clearSession into a WebAuthError with type "USER_CANCELLED"', async () => { - const spaJsError = { - error: 'cancelled', - error_description: 'Popup closed', - }; - mockSpaClient.logout.mockRejectedValue(spaJsError); - await expect(provider.clearSession()).rejects.toThrow(WebAuthError); - try { - await provider.clearSession(); - } catch (e) { - const err = e as WebAuthError; - expect(err.type).toBe('USER_CANCELLED'); - } + 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' + ); + } + }); }); - it('should convert a "state_mismatch" error from handleRedirectCallback into a WebAuthError with type "INVALID_STATE"', async () => { - const spaJsError = { - error: 'state_mismatch', - error_description: 'Invalid state', - }; - 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'); - } + 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'); + } + }); }); });