From 32096fe8c2258beb4345a43546430c94731e0626 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Sat, 5 Sep 2026 01:53:47 +0700 Subject: [PATCH] fix(ios): isolate OAuth URL sessions per request --- .changeset/calm-geckos-isolate.md | 5 + docs/docs/usage/config.md | 3 + .../react-native-app-auth/ios/RNAppAuth.m | 103 ++++++++++++------ 3 files changed, 75 insertions(+), 36 deletions(-) create mode 100644 .changeset/calm-geckos-isolate.md diff --git a/.changeset/calm-geckos-isolate.md b/.changeset/calm-geckos-isolate.md new file mode 100644 index 000000000..fbbfe0ed0 --- /dev/null +++ b/.changeset/calm-geckos-isolate.md @@ -0,0 +1,5 @@ +--- +"react-native-app-auth": patch +--- + +Keep each iOS request on its originating URL session so concurrent OAuth calls cannot mix additional headers or timeout settings. diff --git a/docs/docs/usage/config.md b/docs/docs/usage/config.md index f1b9d5a0c..4a6ca712b 100644 --- a/docs/docs/usage/config.md +++ b/docs/docs/usage/config.md @@ -52,3 +52,6 @@ See specific example [configurations for your provider](/docs/category/providers - **androidAllowCustomBrowsers** - (`string[]`) (default: undefined) _ANDROID_ override the used browser for authorization. If no value is provided, all browsers are allowed. - **androidTrustedWebActivity** - (`boolean`) (default: `false`) _ANDROID_ Use [`EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY`](https://developer.chrome.com/docs/android/trusted-web-activity/) when opening web view. - **connectionTimeoutSeconds** - (`number`) configure the request timeout interval in seconds. This must be a positive number. The default values are 60 seconds on iOS and 15 seconds on Android. + +On iOS, additional headers and timeout settings are scoped to the call that supplied them. Concurrent +authorize, refresh, and register requests do not inherit another call's URL session configuration. diff --git a/packages/react-native-app-auth/ios/RNAppAuth.m b/packages/react-native-app-auth/ios/RNAppAuth.m index c48163427..9708eaf01 100644 --- a/packages/react-native-app-auth/ios/RNAppAuth.m +++ b/packages/react-native-app-auth/ios/RNAppAuth.m @@ -52,7 +52,8 @@ - (dispatch_queue_t)methodQueue resolve: (RCTPromiseResolveBlock) resolve reject: (RCTPromiseRejectBlock) reject) { - [self configureUrlSession:additionalHeaders sessionTimeout:connectionTimeoutSeconds]; + NSURLSession *urlSession = [self createUrlSession:additionalHeaders sessionTimeout:connectionTimeoutSeconds]; + [OIDURLSessionProvider setSession:urlSession]; // if we have manually provided configuration, we can use it and skip the OIDC well-known discovery endpoint call if (serviceConfiguration) { @@ -64,6 +65,7 @@ - (dispatch_queue_t)methodQueue subjectType: subjectType tokenEndpointAuthMethod: tokenEndpointAuthMethod additionalParameters: additionalParameters + urlSession: urlSession resolve: resolve reject: reject]; } else { @@ -80,6 +82,7 @@ - (dispatch_queue_t)methodQueue subjectType: subjectType tokenEndpointAuthMethod: tokenEndpointAuthMethod additionalParameters: additionalParameters + urlSession: urlSession resolve: resolve reject: reject]; }]; @@ -104,7 +107,8 @@ - (dispatch_queue_t)methodQueue resolve: (RCTPromiseResolveBlock) resolve reject: (RCTPromiseRejectBlock) reject) { - [self configureUrlSession:additionalHeaders sessionTimeout:connectionTimeoutSeconds]; + NSURLSession *urlSession = [self createUrlSession:additionalHeaders sessionTimeout:connectionTimeoutSeconds]; + [OIDURLSessionProvider setSession:urlSession]; // if we have manually provided configuration, we can use it and skip the OIDC well-known discovery endpoint call if (serviceConfiguration) { @@ -120,6 +124,7 @@ - (dispatch_queue_t)methodQueue skipCodeExchange: skipCodeExchange iosCustomBrowser: iosCustomBrowser prefersEphemeralSession: prefersEphemeralSession + urlSession: urlSession resolve: resolve reject: reject]; } else { @@ -141,6 +146,7 @@ - (dispatch_queue_t)methodQueue skipCodeExchange: skipCodeExchange iosCustomBrowser: iosCustomBrowser prefersEphemeralSession: prefersEphemeralSession + urlSession: urlSession resolve: resolve reject: reject]; }]; @@ -162,7 +168,8 @@ - (dispatch_queue_t)methodQueue resolve:(RCTPromiseResolveBlock) resolve reject: (RCTPromiseRejectBlock) reject) { - [self configureUrlSession:additionalHeaders sessionTimeout:connectionTimeoutSeconds]; + NSURLSession *urlSession = [self createUrlSession:additionalHeaders sessionTimeout:connectionTimeoutSeconds]; + [OIDURLSessionProvider setSession:urlSession]; // if we have manually provided configuration, we can use it and skip the OIDC well-known discovery endpoint call if (serviceConfiguration) { @@ -174,6 +181,7 @@ - (dispatch_queue_t)methodQueue refreshToken: refreshToken scopes: scopes additionalParameters: additionalParameters + urlSession: urlSession resolve: resolve reject: reject]; } else { @@ -191,6 +199,7 @@ - (dispatch_queue_t)methodQueue refreshToken: refreshToken scopes: scopes additionalParameters: additionalParameters + urlSession: urlSession resolve: resolve reject: reject]; }]; @@ -208,6 +217,8 @@ - (dispatch_queue_t)methodQueue resolve:(RCTPromiseResolveBlock) resolve reject: (RCTPromiseRejectBlock) reject) { + [OIDURLSessionProvider setSession:[self createUrlSession:nil sessionTimeout:60]]; + if (serviceConfiguration) { OIDServiceConfiguration *configuration = [self createServiceConfiguration:serviceConfiguration]; [self endSessionWithConfiguration: configuration @@ -288,6 +299,7 @@ - (void)registerWithConfiguration: (OIDServiceConfiguration *) configuration subjectType: (NSString *) subjectType tokenEndpointAuthMethod: (NSString *) tokenEndpointAuthMethod additionalParameters: (NSDictionary *_Nullable) additionalParameters + urlSession: (NSURLSession *) urlSession resolve: (RCTPromiseResolveBlock) resolve reject: (RCTPromiseRejectBlock) reject { @@ -305,6 +317,7 @@ - (void)registerWithConfiguration: (OIDServiceConfiguration *) configuration tokenEndpointAuthMethod:tokenEndpointAuthMethod additionalParameters:additionalParameters]; + [OIDURLSessionProvider setSession:urlSession]; [OIDAuthorizationService performRegistrationRequest:request completion:^(OIDRegistrationResponse *_Nullable response, NSError *_Nullable error) { @@ -332,6 +345,7 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration skipCodeExchange: (BOOL) skipCodeExchange iosCustomBrowser: (NSString *) iosCustomBrowser prefersEphemeralSession: (BOOL) prefersEphemeralSession + urlSession: (NSURLSession *) urlSession resolve: (RCTPromiseResolveBlock) resolve reject: (RCTPromiseRejectBlock) reject { @@ -415,40 +429,56 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration } } } else { - OIDAuthStateAuthorizationCallback callback = ^( - OIDAuthState *_Nullable authState, - NSError *_Nullable error - ) { - typeof(self) strongSelf = weakSelf; - strongSelf->_currentSession = nil; - [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; - rnAppAuthTaskId = UIBackgroundTaskInvalid; - if (authState) { - resolve([self formatResponse:authState.lastTokenResponse - withAuthResponse:authState.lastAuthorizationResponse]); - } else { - [self rejectPromise:reject - defaultCode:@"authentication_failed" - error:error]; - } - }; - + OIDAuthorizationCallback tokenExchangeCallback = ^( + OIDAuthorizationResponse *_Nullable authorizationResponse, + NSError *_Nullable error + ) { + if (!authorizationResponse) { + typeof(self) strongSelf = weakSelf; + strongSelf->_currentSession = nil; + [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; + rnAppAuthTaskId = UIBackgroundTaskInvalid; + [self rejectPromise:reject + defaultCode:@"authentication_failed" + error:error]; + return; + } + + OIDTokenRequest *tokenRequest = [authorizationResponse tokenExchangeRequest]; + [OIDURLSessionProvider setSession:urlSession]; + [OIDAuthorizationService performTokenRequest:tokenRequest + originalAuthorizationResponse:authorizationResponse + callback:^(OIDTokenResponse *_Nullable tokenResponse, + NSError *_Nullable tokenError) { + typeof(self) strongSelf = weakSelf; + strongSelf->_currentSession = nil; + [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; + rnAppAuthTaskId = UIBackgroundTaskInvalid; + if (tokenResponse) { + resolve([self formatResponse:tokenResponse + withAuthResponse:authorizationResponse]); + } else { + [self rejectPromise:reject + defaultCode:@"authentication_failed" + error:tokenError]; + } + }]; + }; + if(externalUserAgent != nil) { - _currentSession = [OIDAuthState authStateByPresentingAuthorizationRequest:request - externalUserAgent:externalUserAgent - callback:callback]; + _currentSession = [OIDAuthorizationService presentAuthorizationRequest:request + externalUserAgent:externalUserAgent + callback:tokenExchangeCallback]; } else { - - if (@available(iOS 13, *)) { - _currentSession = [OIDAuthState authStateByPresentingAuthorizationRequest:request - presentingViewController:presentingViewController - prefersEphemeralSession:prefersEphemeralSession - callback:callback]; + _currentSession = [OIDAuthorizationService presentAuthorizationRequest:request + presentingViewController:presentingViewController + prefersEphemeralSession:prefersEphemeralSession + callback:tokenExchangeCallback]; } else { - _currentSession = [OIDAuthState authStateByPresentingAuthorizationRequest:request - presentingViewController:presentingViewController - callback:callback]; + _currentSession = [OIDAuthorizationService presentAuthorizationRequest:request + presentingViewController:presentingViewController + callback:tokenExchangeCallback]; } } } @@ -464,6 +494,7 @@ - (void)refreshWithConfiguration: (OIDServiceConfiguration *)configuration refreshToken: (NSString *) refreshToken scopes: (NSArray *) scopes additionalParameters: (NSDictionary *_Nullable) additionalParameters + urlSession: (NSURLSession *) urlSession resolve:(RCTPromiseResolveBlock) resolve reject: (RCTPromiseRejectBlock) reject { @@ -479,6 +510,7 @@ - (void)refreshWithConfiguration: (OIDServiceConfiguration *)configuration codeVerifier:nil additionalParameters:additionalParameters]; + [OIDURLSessionProvider setSession:urlSession]; [OIDAuthorizationService performTokenRequest:tokenRefreshRequest callback:^(OIDTokenResponse *_Nullable response, NSError *_Nullable error) { @@ -546,7 +578,7 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration }]; } -- (void)configureUrlSession: (NSDictionary*) headers sessionTimeout: (double) sessionTimeout{ +- (NSURLSession *)createUrlSession: (NSDictionary*) headers sessionTimeout: (double) sessionTimeout{ NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; if (headers != nil) { configuration.HTTPAdditionalHeaders = headers; @@ -554,8 +586,7 @@ - (void)configureUrlSession: (NSDictionary*) headers sessionTimeout: (double) se configuration.timeoutIntervalForRequest = sessionTimeout; - NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration]; - [OIDURLSessionProvider setSession:session]; + return [NSURLSession sessionWithConfiguration:configuration]; } /*