From d22101188c0384128443a210ca2dc6408acba964 Mon Sep 17 00:00:00 2001 From: Federico Bartoli Date: Fri, 9 Oct 2026 14:55:59 +0200 Subject: [PATCH 1/2] fix: do not forward redirects to a completed session on iOS --- .changeset/fix-ios-completed-session.md | 5 +++ .../react-native-app-auth/ios/RNAppAuth.m | 32 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-ios-completed-session.md diff --git a/.changeset/fix-ios-completed-session.md b/.changeset/fix-ios-completed-session.md new file mode 100644 index 000000000..ebec200bd --- /dev/null +++ b/.changeset/fix-ios-completed-session.md @@ -0,0 +1,5 @@ +--- +"react-native-app-auth": patch +--- + +Fix an iOS crash ("An OAuth redirect was sent to a OIDExternalUserAgentSession after it already completed") by not keeping an authorization or logout session that completed synchronously, and by forwarding at most one redirect to each session. diff --git a/packages/react-native-app-auth/ios/RNAppAuth.m b/packages/react-native-app-auth/ios/RNAppAuth.m index c1181d6d5..e266a0dfc 100644 --- a/packages/react-native-app-auth/ios/RNAppAuth.m +++ b/packages/react-native-app-auth/ios/RNAppAuth.m @@ -17,7 +17,18 @@ @interface RNAppAuth() { @implementation RNAppAuth -(BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)url { - return [_currentSession resumeExternalUserAgentFlowWithURL:url]; + id session = _currentSession; + if (session == nil) { + return NO; + } + BOOL handled = [session resumeExternalUserAgentFlowWithURL:url]; + // A session consumes at most one redirect. Once it has accepted one, stop forwarding URLs to + // it: the code exchange that may still be running does not need the session, and AppAuth + // < 2.1.0 raises an NSException when a completed session is resumed again. + if (handled && _currentSession == session) { + _currentSession = nil; + } + return handled; } - (dispatch_queue_t)methodQueue @@ -374,6 +385,9 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration } appDelegate.authorizationFlowManagerDelegate = self; __weak typeof(self) weakSelf = self; + // Set when the flow's callback has run. The callback can run synchronously, before the + // presenting call below returns (e.g. when the user-agent fails to open). + __block BOOL flowCompleted = NO; rnAppAuthTaskId = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^{ [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; @@ -388,6 +402,7 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration OIDAuthorizationCallback callback = ^(OIDAuthorizationResponse *_Nullable authorizationResponse, NSError *_Nullable error) { typeof(self) strongSelf = weakSelf; + flowCompleted = YES; strongSelf->_currentSession = nil; [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; rnAppAuthTaskId = UIBackgroundTaskInvalid; @@ -424,6 +439,7 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration NSError *_Nullable error ) { typeof(self) strongSelf = weakSelf; + flowCompleted = YES; strongSelf->_currentSession = nil; [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; rnAppAuthTaskId = UIBackgroundTaskInvalid; @@ -456,6 +472,12 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration } } } + + // If the callback already ran, the session returned above has completed: do not keep it, + // otherwise a later redirect would be forwarded to a finished session. + if (flowCompleted) { + _currentSession = nil; + } } /* @@ -523,6 +545,9 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration } appDelegate.authorizationFlowManagerDelegate = self; __weak typeof(self) weakSelf = self; + // Set when the flow's callback has run. The callback can run synchronously, before the + // presenting call below returns (e.g. when the user-agent fails to open). + __block BOOL flowCompleted = NO; rnAppAuthTaskId = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^{ [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; @@ -540,6 +565,7 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration externalUserAgent: externalUserAgent callback: ^(OIDEndSessionResponse *_Nullable response, NSError *_Nullable error) { typeof(self) strongSelf = weakSelf; + flowCompleted = YES; strongSelf->_currentSession = nil; [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; rnAppAuthTaskId = UIBackgroundTaskInvalid; @@ -551,6 +577,10 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration error:error]; } }]; + + if (flowCompleted) { + _currentSession = nil; + } } - (void)configureUrlSession: (NSDictionary*) headers sessionTimeout: (double) sessionTimeout{ From aa18f255fcc9166d4c8adde80f1915b8a78b9776 Mon Sep 17 00:00:00 2001 From: Federico Bartoli Date: Fri, 9 Oct 2026 17:15:32 +0200 Subject: [PATCH 2/2] chore: comment the completed-flow check in logout --- packages/react-native-app-auth/ios/RNAppAuth.m | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-app-auth/ios/RNAppAuth.m b/packages/react-native-app-auth/ios/RNAppAuth.m index e266a0dfc..79cda3c59 100644 --- a/packages/react-native-app-auth/ios/RNAppAuth.m +++ b/packages/react-native-app-auth/ios/RNAppAuth.m @@ -578,6 +578,7 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration } }]; + // As in authorizeWithConfiguration:, do not keep a session whose callback already ran. if (flowCompleted) { _currentSession = nil; }