Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/fuzzy-ravens-explain.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/react-native-app-auth/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,5 @@ type ErrorCode =

export interface AppAuthError extends Error {
code: ErrorCode;
nativeError?: string;
}
24 changes: 20 additions & 4 deletions packages/react-native-app-auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -191,7 +207,7 @@ export const register = ({
nativeMethodArguments.push(additionalHeaders);
}

return RNAppAuth.register(...nativeMethodArguments);
return wrapNativeAuthPromise(RNAppAuth.register(...nativeMethodArguments));
};

export const authorize = ({
Expand Down Expand Up @@ -253,7 +269,7 @@ export const authorize = ({
nativeMethodArguments.push(iosPrefersEphemeralSession);
}

return RNAppAuth.authorize(...nativeMethodArguments);
return wrapNativeAuthPromise(RNAppAuth.authorize(...nativeMethodArguments));
};

export const refresh = (
Expand Down Expand Up @@ -308,7 +324,7 @@ export const refresh = (
nativeMethodArguments.push(iosCustomBrowser);
}

return RNAppAuth.refresh(...nativeMethodArguments);
return wrapNativeAuthPromise(RNAppAuth.refresh(...nativeMethodArguments));
};

export const revoke = async (
Expand Down Expand Up @@ -389,5 +405,5 @@ export const logout = (
nativeMethodArguments.push(iosPrefersEphemeralSession);
}

return RNAppAuth.logout(...nativeMethodArguments);
return wrapNativeAuthPromise(RNAppAuth.logout(...nativeMethodArguments));
};
13 changes: 13 additions & 0 deletions packages/react-native-app-auth/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
63 changes: 52 additions & 11 deletions packages/react-native-app-auth/ios/RNAppAuth.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}];
}
Expand Down Expand Up @@ -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];
}
};

Expand Down Expand Up @@ -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];
}
};

Expand Down Expand Up @@ -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];
}
}];
}
Expand Down Expand Up @@ -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];
}
}];
}
Expand Down Expand Up @@ -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);
}
}

Expand Down