diff --git a/.changeset/fuzzy-ravens-explain.md b/.changeset/fuzzy-ravens-explain.md new file mode 100644 index 00000000..b37887df --- /dev/null +++ b/.changeset/fuzzy-ravens-explain.md @@ -0,0 +1,5 @@ +--- +'react-native-app-auth': minor +--- + +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 d9de8130..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 @@ -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,10 +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 { - promise.reject(ex.error != null ? ex.error : fallbackErrorCode, ex.getLocalizedMessage(), ex); + 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, ex, userInfo); + } else { + promise.reject(code, 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 ed51141d..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]; } }]; } @@ -735,8 +740,44 @@ - (NSString*)getErrorMessage: (NSError*) error { userInfo[OIDOAuthErrorResponseErrorKey] && userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldErrorDescription]) { 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 isKindOfClass:[NSError class]] && [underlyingError localizedDescription]) { + return [underlyingError localizedDescription]; + } + + 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 { - return [error localizedDescription]; + reject(code, message, error); } }