Skip to content

Commit fc5d6dc

Browse files
Expose optional native error details for auth failures (#886)
Co-authored-by: bryceknz <25199713+bryceknz@users.noreply.github.com>
1 parent 7679b95 commit fc5d6dc

6 files changed

Lines changed: 103 additions & 17 deletions

File tree

.changeset/fuzzy-ravens-explain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'react-native-app-auth': minor
3+
---
4+
5+
Expose underlying native authorization errors on `error.nativeError` for debugging while keeping `error.message` user-safe.

packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import androidx.browser.customtabs.TrustedWebUtils;
1818

1919
import com.facebook.react.bridge.ActivityEventListener;
20+
import com.facebook.react.bridge.Arguments;
2021
import com.facebook.react.bridge.ReactApplicationContext;
2122
import com.facebook.react.bridge.ReactContextBaseJavaModule;
2223
import com.facebook.react.bridge.ReactMethod;
@@ -1041,10 +1042,19 @@ private AuthorizationServiceConfiguration getServiceConfiguration(@Nullable Stri
10411042

10421043
private void handleAuthorizationException(final String fallbackErrorCode, final AuthorizationException ex,
10431044
final Promise promise) {
1045+
final String code = ex.error != null ? ex.error : fallbackErrorCode;
10441046
if (ex.getLocalizedMessage() == null) {
1045-
promise.reject(fallbackErrorCode, ex.error, ex);
1047+
promise.reject(code, ex.error, ex);
10461048
} else {
1047-
promise.reject(ex.error != null ? ex.error : fallbackErrorCode, ex.getLocalizedMessage(), ex);
1049+
final String message = ex.getLocalizedMessage();
1050+
final Throwable cause = ex.getCause();
1051+
if (cause != null && cause.getLocalizedMessage() != null) {
1052+
WritableMap userInfo = Arguments.createMap();
1053+
userInfo.putString("nativeError", cause.getLocalizedMessage());
1054+
promise.reject(code, message, ex, userInfo);
1055+
} else {
1056+
promise.reject(code, message, ex);
1057+
}
10481058
}
10491059
}
10501060

packages/react-native-app-auth/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,5 @@ type ErrorCode =
199199

200200
export interface AppAuthError extends Error {
201201
code: ErrorCode;
202+
nativeError?: string;
202203
}

packages/react-native-app-auth/index.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ import base64 from 'react-native-base64';
44

55
const { RNAppAuth } = NativeModules;
66

7+
const normalizeNativeAuthError = error => {
8+
if (!error || error.nativeError) {
9+
return error;
10+
}
11+
12+
const nativeError = error.userInfo && error.userInfo.nativeError;
13+
if (nativeError) {
14+
error.nativeError = nativeError;
15+
}
16+
17+
return error;
18+
};
19+
20+
const wrapNativeAuthPromise = promise =>
21+
Promise.resolve(promise).catch(error => Promise.reject(normalizeNativeAuthError(error)));
22+
723
const validateIssuer = issuer => typeof issuer === 'string' && issuer.length;
824
const validateIssuerOrServiceConfigurationEndpoints = (issuer, serviceConfiguration) => {
925
invariant(
@@ -191,7 +207,7 @@ export const register = ({
191207
nativeMethodArguments.push(additionalHeaders);
192208
}
193209

194-
return RNAppAuth.register(...nativeMethodArguments);
210+
return wrapNativeAuthPromise(RNAppAuth.register(...nativeMethodArguments));
195211
};
196212

197213
export const authorize = ({
@@ -253,7 +269,7 @@ export const authorize = ({
253269
nativeMethodArguments.push(iosPrefersEphemeralSession);
254270
}
255271

256-
return RNAppAuth.authorize(...nativeMethodArguments);
272+
return wrapNativeAuthPromise(RNAppAuth.authorize(...nativeMethodArguments));
257273
};
258274

259275
export const refresh = (
@@ -308,7 +324,7 @@ export const refresh = (
308324
nativeMethodArguments.push(iosCustomBrowser);
309325
}
310326

311-
return RNAppAuth.refresh(...nativeMethodArguments);
327+
return wrapNativeAuthPromise(RNAppAuth.refresh(...nativeMethodArguments));
312328
};
313329

314330
export const revoke = async (
@@ -389,5 +405,5 @@ export const logout = (
389405
nativeMethodArguments.push(iosPrefersEphemeralSession);
390406
}
391407

392-
return RNAppAuth.logout(...nativeMethodArguments);
408+
return wrapNativeAuthPromise(RNAppAuth.logout(...nativeMethodArguments));
393409
};

packages/react-native-app-auth/index.spec.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,19 @@ describe('AppAuth', () => {
575575
);
576576
});
577577

578+
it('exposes native auth errors without changing the message', async () => {
579+
const error = new Error('Network error');
580+
error.userInfo = {
581+
nativeError: 'Unacceptable certificate',
582+
};
583+
mockAuthorize.mockRejectedValue(error);
584+
585+
await expect(authorize(config)).rejects.toMatchObject({
586+
message: 'Network error',
587+
nativeError: 'Unacceptable certificate',
588+
});
589+
});
590+
578591
describe('iOS-specific', () => {
579592
beforeEach(() => {
580593
require('react-native').Platform.OS = 'ios';

packages/react-native-app-auth/ios/RNAppAuth.m

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,9 @@ - (void)registerWithConfiguration: (OIDServiceConfiguration *) configuration
311311
if (response) {
312312
resolve([self formatRegistrationResponse:response]);
313313
} else {
314-
reject([self getErrorCode: error defaultCode:@"registration_failed"],
315-
[self getErrorMessage: error], error);
314+
[self rejectPromise:reject
315+
defaultCode:@"registration_failed"
316+
error:error];
316317
}
317318
}];
318319
}
@@ -389,8 +390,9 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration
389390
if (authorizationResponse) {
390391
resolve([self formatAuthorizationResponse:authorizationResponse withCodeVerifier:codeVerifier]);
391392
} else {
392-
reject([self getErrorCode: error defaultCode:@"authentication_failed"],
393-
[self getErrorMessage: error], error);
393+
[self rejectPromise:reject
394+
defaultCode:@"authentication_failed"
395+
error:error];
394396
}
395397
};
396398

@@ -425,8 +427,9 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration
425427
resolve([self formatResponse:authState.lastTokenResponse
426428
withAuthResponse:authState.lastAuthorizationResponse]);
427429
} else {
428-
reject([self getErrorCode: error defaultCode:@"authentication_failed"],
429-
[self getErrorMessage: error], error);
430+
[self rejectPromise:reject
431+
defaultCode:@"authentication_failed"
432+
error:error];
430433
}
431434
};
432435

@@ -482,8 +485,9 @@ - (void)refreshWithConfiguration: (OIDServiceConfiguration *)configuration
482485
if (response) {
483486
resolve([self formatResponse:response]);
484487
} else {
485-
reject([self getErrorCode: error defaultCode:@"token_refresh_failed"],
486-
[self getErrorMessage: error], error);
488+
[self rejectPromise:reject
489+
defaultCode:@"token_refresh_failed"
490+
error:error];
487491
}
488492
}];
489493
}
@@ -535,8 +539,9 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration
535539
if (response) {
536540
resolve([self formatEndSessionResponse:response]);
537541
} else {
538-
reject([self getErrorCode: error defaultCode:@"end_session_failed"],
539-
[self getErrorMessage: error], error);
542+
[self rejectPromise:reject
543+
defaultCode:@"end_session_failed"
544+
error:error];
540545
}
541546
}];
542547
}
@@ -735,8 +740,44 @@ - (NSString*)getErrorMessage: (NSError*) error {
735740
userInfo[OIDOAuthErrorResponseErrorKey] &&
736741
userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldErrorDescription]) {
737742
return userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldErrorDescription];
743+
}
744+
745+
return [error localizedDescription];
746+
}
747+
748+
- (NSString *)getNativeErrorFromError:(NSError *)error {
749+
NSDictionary *userInfo = [error userInfo];
750+
if (!userInfo) {
751+
return nil;
752+
}
753+
754+
NSError *underlyingError = userInfo[NSUnderlyingErrorKey];
755+
if ([underlyingError isKindOfClass:[NSError class]] && [underlyingError localizedDescription]) {
756+
return [underlyingError localizedDescription];
757+
}
758+
759+
return nil;
760+
}
761+
762+
- (void)rejectPromise:(RCTPromiseRejectBlock)reject
763+
defaultCode:(NSString *)defaultCode
764+
error:(NSError *)error {
765+
NSString *code = [self getErrorCode:error defaultCode:defaultCode];
766+
NSString *message = [self getErrorMessage:error];
767+
NSString *nativeError = [self getNativeErrorFromError:error];
768+
769+
if (nativeError) {
770+
NSMutableDictionary *userInfo = [[error userInfo] mutableCopy];
771+
if (!userInfo) {
772+
userInfo = [NSMutableDictionary dictionary];
773+
}
774+
userInfo[@"nativeError"] = nativeError;
775+
NSError *rejectionError = [NSError errorWithDomain:error.domain
776+
code:error.code
777+
userInfo:userInfo];
778+
reject(code, message, rejectionError);
738779
} else {
739-
return [error localizedDescription];
780+
reject(code, message, error);
740781
}
741782
}
742783

0 commit comments

Comments
 (0)