From 3ee4310abded6e257a33b689ffbd805d3a011ab4 Mon Sep 17 00:00:00 2001 From: Kevin van der Burg Date: Wed, 23 Aug 2023 11:31:27 +0200 Subject: [PATCH 1/9] Added a more descriptive error message for AuthorizationExceptions --- android/src/main/java/com/rnappauth/RNAppAuthModule.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/rnappauth/RNAppAuthModule.java b/android/src/main/java/com/rnappauth/RNAppAuthModule.java index b775886e..5b503f29 100644 --- a/android/src/main/java/com/rnappauth/RNAppAuthModule.java +++ b/android/src/main/java/com/rnappauth/RNAppAuthModule.java @@ -1019,7 +1019,12 @@ private void handleAuthorizationException(final String fallbackErrorCode, final if (ex.getLocalizedMessage() == null) { promise.reject(fallbackErrorCode, ex.error, ex); } else { - promise.reject(ex.error != null ? ex.error: fallbackErrorCode, ex.getLocalizedMessage(), ex); + String message = ex.getLocalizedMessage(); + if(ex.getCause() != null) { + message += "- Cause: " + ex.getCause().getLocalizedMessage(); + } + + promise.reject(ex.error != null ? ex.error: fallbackErrorCode, message, ex); } } From 96656a6c801583e69c815ae34b6db75d10dcd7a1 Mon Sep 17 00:00:00 2001 From: Kevin van der Burg Date: Wed, 23 Aug 2023 11:35:37 +0200 Subject: [PATCH 2/9] Added extra space before - Cause --- android/src/main/java/com/rnappauth/RNAppAuthModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/rnappauth/RNAppAuthModule.java b/android/src/main/java/com/rnappauth/RNAppAuthModule.java index 5b503f29..a96602bb 100644 --- a/android/src/main/java/com/rnappauth/RNAppAuthModule.java +++ b/android/src/main/java/com/rnappauth/RNAppAuthModule.java @@ -1021,7 +1021,7 @@ private void handleAuthorizationException(final String fallbackErrorCode, final } else { String message = ex.getLocalizedMessage(); if(ex.getCause() != null) { - message += "- Cause: " + ex.getCause().getLocalizedMessage(); + message += " - Cause: " + ex.getCause().getLocalizedMessage(); } promise.reject(ex.error != null ? ex.error: fallbackErrorCode, message, ex); From ac5f7922032371726e33eb3aebb71708aa7efb0b Mon Sep 17 00:00:00 2001 From: bryceknz <25199713+bryceknz@users.noreply.github.com> Date: Tue, 19 May 2026 16:57:02 +0100 Subject: [PATCH 3/9] fix(android): include authorization exception causes --- .../src/main/java/com/rnappauth/RNAppAuthModule.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java b/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java index d9de8130..2a4415ad 100644 --- a/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java +++ b/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java @@ -1044,7 +1044,12 @@ private void handleAuthorizationException(final String fallbackErrorCode, final if (ex.getLocalizedMessage() == null) { promise.reject(fallbackErrorCode, ex.error, ex); } else { - promise.reject(ex.error != null ? ex.error : fallbackErrorCode, ex.getLocalizedMessage(), ex); + String message = ex.getLocalizedMessage(); + if (ex.getCause() != null) { + message += " - Cause: " + ex.getCause().getLocalizedMessage(); + } + + promise.reject(ex.error != null ? ex.error : fallbackErrorCode, message, ex); } } From 7ee5b630d2cdb7d13be0242d349bb01a5d7b21f2 Mon Sep 17 00:00:00 2001 From: bryceknz <25199713+bryceknz@users.noreply.github.com> Date: Tue, 19 May 2026 16:58:26 +0100 Subject: [PATCH 4/9] fix(ios): include underlying authorization error causes --- packages/react-native-app-auth/ios/RNAppAuth.m | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/react-native-app-auth/ios/RNAppAuth.m b/packages/react-native-app-auth/ios/RNAppAuth.m index ed51141d..8a6e94ee 100644 --- a/packages/react-native-app-auth/ios/RNAppAuth.m +++ b/packages/react-native-app-auth/ios/RNAppAuth.m @@ -730,14 +730,22 @@ - (NSString*)getErrorCode: (NSError*) error defaultCode: (NSString *) defaultCod - (NSString*)getErrorMessage: (NSError*) error { NSDictionary * userInfo = [error userInfo]; + NSString *message; if (userInfo && userInfo[OIDOAuthErrorResponseErrorKey] && userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldErrorDescription]) { - return userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldErrorDescription]; + message = userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldErrorDescription]; } else { - return [error localizedDescription]; + message = [error localizedDescription]; } + + NSError *underlyingError = userInfo[NSUnderlyingErrorKey]; + if (underlyingError && [underlyingError isKindOfClass:[NSError class]] && [underlyingError localizedDescription]) { + message = [message stringByAppendingFormat:@" - Cause: %@", [underlyingError localizedDescription]]; + } + + return message; } - (id)getExternalUserAgentWithPresentingViewController: (UIViewController *)presentingViewController From ec7ea88ef3a9e19eba7454cfdb2810397be6a80f Mon Sep 17 00:00:00 2001 From: bryceknz <25199713+bryceknz@users.noreply.github.com> Date: Tue, 19 May 2026 16:59:04 +0100 Subject: [PATCH 5/9] chore: add changeset for clearer native errors --- .changeset/fuzzy-ravens-explain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fuzzy-ravens-explain.md diff --git a/.changeset/fuzzy-ravens-explain.md b/.changeset/fuzzy-ravens-explain.md new file mode 100644 index 00000000..e715cf4a --- /dev/null +++ b/.changeset/fuzzy-ravens-explain.md @@ -0,0 +1,5 @@ +--- +"react-native-app-auth": patch +--- + +Include underlying native error causes in Android and iOS authorization error messages. From 2b88f1a967491cbd635fc7fed4706295be44ff2f Mon Sep 17 00:00:00 2001 From: bryceknz <25199713+bryceknz@users.noreply.github.com> Date: Wed, 20 May 2026 10:47:26 +0100 Subject: [PATCH 6/9] fix: expose native authorization errors separately Keep user-facing auth error messages unchanged while surfacing underlying native diagnostics through error.nativeError. --- .../java/com/rnappauth/RNAppAuthModule.java | 17 +++-- packages/react-native-app-auth/index.d.ts | 1 + packages/react-native-app-auth/index.js | 24 +++++-- packages/react-native-app-auth/index.spec.js | 13 ++++ .../react-native-app-auth/ios/RNAppAuth.m | 67 ++++++++++++++----- 5 files changed, 95 insertions(+), 27 deletions(-) diff --git a/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java b/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java index 2a4415ad..6463c583 100644 --- a/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java +++ b/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java @@ -17,6 +17,7 @@ import androidx.browser.customtabs.TrustedWebUtils; import com.facebook.react.bridge.ActivityEventListener; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -1041,15 +1042,19 @@ private AuthorizationServiceConfiguration getServiceConfiguration(@Nullable Stri private void handleAuthorizationException(final String fallbackErrorCode, final AuthorizationException ex, final Promise promise) { + final String code = ex.error != null ? ex.error : fallbackErrorCode; if (ex.getLocalizedMessage() == null) { - promise.reject(fallbackErrorCode, ex.error, ex); + promise.reject(code, ex.error, ex); } else { - String message = ex.getLocalizedMessage(); - if (ex.getCause() != null) { - message += " - Cause: " + ex.getCause().getLocalizedMessage(); + final String message = ex.getLocalizedMessage(); + final Throwable cause = ex.getCause(); + if (cause != null && cause.getLocalizedMessage() != null) { + WritableMap userInfo = Arguments.createMap(); + userInfo.putString("nativeError", cause.getLocalizedMessage()); + promise.reject(code, message, userInfo); + } else { + promise.reject(code, message, ex); } - - promise.reject(ex.error != null ? ex.error : fallbackErrorCode, message, ex); } } diff --git a/packages/react-native-app-auth/index.d.ts b/packages/react-native-app-auth/index.d.ts index 0c59626f..e9ff392a 100644 --- a/packages/react-native-app-auth/index.d.ts +++ b/packages/react-native-app-auth/index.d.ts @@ -199,4 +199,5 @@ type ErrorCode = export interface AppAuthError extends Error { code: ErrorCode; + nativeError?: string; } diff --git a/packages/react-native-app-auth/index.js b/packages/react-native-app-auth/index.js index 7b8f4a4a..8ebc7e77 100644 --- a/packages/react-native-app-auth/index.js +++ b/packages/react-native-app-auth/index.js @@ -4,6 +4,22 @@ import base64 from 'react-native-base64'; const { RNAppAuth } = NativeModules; +const normalizeNativeAuthError = error => { + if (!error || error.nativeError) { + return error; + } + + const nativeError = error.userInfo && error.userInfo.nativeError; + if (nativeError) { + error.nativeError = nativeError; + } + + return error; +}; + +const wrapNativeAuthPromise = promise => + Promise.resolve(promise).catch(error => Promise.reject(normalizeNativeAuthError(error))); + const validateIssuer = issuer => typeof issuer === 'string' && issuer.length; const validateIssuerOrServiceConfigurationEndpoints = (issuer, serviceConfiguration) => { invariant( @@ -191,7 +207,7 @@ export const register = ({ nativeMethodArguments.push(additionalHeaders); } - return RNAppAuth.register(...nativeMethodArguments); + return wrapNativeAuthPromise(RNAppAuth.register(...nativeMethodArguments)); }; export const authorize = ({ @@ -253,7 +269,7 @@ export const authorize = ({ nativeMethodArguments.push(iosPrefersEphemeralSession); } - return RNAppAuth.authorize(...nativeMethodArguments); + return wrapNativeAuthPromise(RNAppAuth.authorize(...nativeMethodArguments)); }; export const refresh = ( @@ -308,7 +324,7 @@ export const refresh = ( nativeMethodArguments.push(iosCustomBrowser); } - return RNAppAuth.refresh(...nativeMethodArguments); + return wrapNativeAuthPromise(RNAppAuth.refresh(...nativeMethodArguments)); }; export const revoke = async ( @@ -389,5 +405,5 @@ export const logout = ( nativeMethodArguments.push(iosPrefersEphemeralSession); } - return RNAppAuth.logout(...nativeMethodArguments); + return wrapNativeAuthPromise(RNAppAuth.logout(...nativeMethodArguments)); }; diff --git a/packages/react-native-app-auth/index.spec.js b/packages/react-native-app-auth/index.spec.js index 7ed3ba19..bc2c278b 100644 --- a/packages/react-native-app-auth/index.spec.js +++ b/packages/react-native-app-auth/index.spec.js @@ -575,6 +575,19 @@ describe('AppAuth', () => { ); }); + it('exposes native auth errors without changing the message', async () => { + const error = new Error('Network error'); + error.userInfo = { + nativeError: 'Unacceptable certificate', + }; + mockAuthorize.mockRejectedValue(error); + + await expect(authorize(config)).rejects.toMatchObject({ + message: 'Network error', + nativeError: 'Unacceptable certificate', + }); + }); + describe('iOS-specific', () => { beforeEach(() => { require('react-native').Platform.OS = 'ios'; diff --git a/packages/react-native-app-auth/ios/RNAppAuth.m b/packages/react-native-app-auth/ios/RNAppAuth.m index 8a6e94ee..c4816342 100644 --- a/packages/react-native-app-auth/ios/RNAppAuth.m +++ b/packages/react-native-app-auth/ios/RNAppAuth.m @@ -311,8 +311,9 @@ - (void)registerWithConfiguration: (OIDServiceConfiguration *) configuration if (response) { resolve([self formatRegistrationResponse:response]); } else { - reject([self getErrorCode: error defaultCode:@"registration_failed"], - [self getErrorMessage: error], error); + [self rejectPromise:reject + defaultCode:@"registration_failed" + error:error]; } }]; } @@ -389,8 +390,9 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration if (authorizationResponse) { resolve([self formatAuthorizationResponse:authorizationResponse withCodeVerifier:codeVerifier]); } else { - reject([self getErrorCode: error defaultCode:@"authentication_failed"], - [self getErrorMessage: error], error); + [self rejectPromise:reject + defaultCode:@"authentication_failed" + error:error]; } }; @@ -425,8 +427,9 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration resolve([self formatResponse:authState.lastTokenResponse withAuthResponse:authState.lastAuthorizationResponse]); } else { - reject([self getErrorCode: error defaultCode:@"authentication_failed"], - [self getErrorMessage: error], error); + [self rejectPromise:reject + defaultCode:@"authentication_failed" + error:error]; } }; @@ -482,8 +485,9 @@ - (void)refreshWithConfiguration: (OIDServiceConfiguration *)configuration if (response) { resolve([self formatResponse:response]); } else { - reject([self getErrorCode: error defaultCode:@"token_refresh_failed"], - [self getErrorMessage: error], error); + [self rejectPromise:reject + defaultCode:@"token_refresh_failed" + error:error]; } }]; } @@ -535,8 +539,9 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration if (response) { resolve([self formatEndSessionResponse:response]); } else { - reject([self getErrorCode: error defaultCode:@"end_session_failed"], - [self getErrorMessage: error], error); + [self rejectPromise:reject + defaultCode:@"end_session_failed" + error:error]; } }]; } @@ -730,22 +735,50 @@ - (NSString*)getErrorCode: (NSError*) error defaultCode: (NSString *) defaultCod - (NSString*)getErrorMessage: (NSError*) error { NSDictionary * userInfo = [error userInfo]; - NSString *message; if (userInfo && userInfo[OIDOAuthErrorResponseErrorKey] && userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldErrorDescription]) { - message = userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldErrorDescription]; - } else { - message = [error localizedDescription]; + return userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldErrorDescription]; + } + + return [error localizedDescription]; +} + +- (NSString *)getNativeErrorFromError:(NSError *)error { + NSDictionary *userInfo = [error userInfo]; + if (!userInfo) { + return nil; } NSError *underlyingError = userInfo[NSUnderlyingErrorKey]; - if (underlyingError && [underlyingError isKindOfClass:[NSError class]] && [underlyingError localizedDescription]) { - message = [message stringByAppendingFormat:@" - Cause: %@", [underlyingError localizedDescription]]; + if ([underlyingError isKindOfClass:[NSError class]] && [underlyingError localizedDescription]) { + return [underlyingError localizedDescription]; } - return message; + return nil; +} + +- (void)rejectPromise:(RCTPromiseRejectBlock)reject + defaultCode:(NSString *)defaultCode + error:(NSError *)error { + NSString *code = [self getErrorCode:error defaultCode:defaultCode]; + NSString *message = [self getErrorMessage:error]; + NSString *nativeError = [self getNativeErrorFromError:error]; + + if (nativeError) { + NSMutableDictionary *userInfo = [[error userInfo] mutableCopy]; + if (!userInfo) { + userInfo = [NSMutableDictionary dictionary]; + } + userInfo[@"nativeError"] = nativeError; + NSError *rejectionError = [NSError errorWithDomain:error.domain + code:error.code + userInfo:userInfo]; + reject(code, message, rejectionError); + } else { + reject(code, message, error); + } } - (id)getExternalUserAgentWithPresentingViewController: (UIViewController *)presentingViewController From b819c541d0513eec9a8037e1484245987fed03ea Mon Sep 17 00:00:00 2001 From: bryceknz <25199713+bryceknz@users.noreply.github.com> Date: Wed, 20 May 2026 10:47:26 +0100 Subject: [PATCH 7/9] chore: update native error changeset Document the separate nativeError debug field without suggesting native details belong in user-facing messages. --- .changeset/fuzzy-ravens-explain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fuzzy-ravens-explain.md b/.changeset/fuzzy-ravens-explain.md index e715cf4a..5cc03f11 100644 --- a/.changeset/fuzzy-ravens-explain.md +++ b/.changeset/fuzzy-ravens-explain.md @@ -2,4 +2,4 @@ "react-native-app-auth": patch --- -Include underlying native error causes in Android and iOS authorization error messages. +Expose underlying native authorization errors on `error.nativeError` for debugging while keeping `error.message` user-safe. From 51c1241474176995e62759fb41cff692a08bbbc5 Mon Sep 17 00:00:00 2001 From: bryceknz <25199713+bryceknz@users.noreply.github.com> Date: Wed, 20 May 2026 11:09:36 +0100 Subject: [PATCH 8/9] fix: preserve native authorization exception context Pass Android nativeError metadata without dropping the original AuthorizationException, and align the changeset frontmatter with repo style. --- .changeset/fuzzy-ravens-explain.md | 2 +- .../android/src/main/java/com/rnappauth/RNAppAuthModule.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/fuzzy-ravens-explain.md b/.changeset/fuzzy-ravens-explain.md index 5cc03f11..ca90246d 100644 --- a/.changeset/fuzzy-ravens-explain.md +++ b/.changeset/fuzzy-ravens-explain.md @@ -1,5 +1,5 @@ --- -"react-native-app-auth": patch +'react-native-app-auth': patch --- Expose underlying native authorization errors on `error.nativeError` for debugging while keeping `error.message` user-safe. diff --git a/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java b/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java index 6463c583..33ca6812 100644 --- a/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java +++ b/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java @@ -1051,7 +1051,7 @@ private void handleAuthorizationException(final String fallbackErrorCode, final if (cause != null && cause.getLocalizedMessage() != null) { WritableMap userInfo = Arguments.createMap(); userInfo.putString("nativeError", cause.getLocalizedMessage()); - promise.reject(code, message, userInfo); + promise.reject(code, message, ex, userInfo); } else { promise.reject(code, message, ex); } From 9038a649e978c0024402c23612c258104f4514a2 Mon Sep 17 00:00:00 2001 From: bryceknz <25199713+bryceknz@users.noreply.github.com> Date: Wed, 20 May 2026 16:05:29 +0100 Subject: [PATCH 9/9] chore: mark native error changeset minor --- .changeset/fuzzy-ravens-explain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fuzzy-ravens-explain.md b/.changeset/fuzzy-ravens-explain.md index ca90246d..b37887df 100644 --- a/.changeset/fuzzy-ravens-explain.md +++ b/.changeset/fuzzy-ravens-explain.md @@ -1,5 +1,5 @@ --- -'react-native-app-auth': patch +'react-native-app-auth': minor --- Expose underlying native authorization errors on `error.nativeError` for debugging while keeping `error.message` user-safe.