From 681d01cda4daf021849f788d5ad0247409790074 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:34:01 -0700 Subject: [PATCH 01/26] g-orchestrated: add OIDErrorCodeURLMismatch and OIDErrorCodeInvalidAuthorizationFlow error codes --- Sources/AppAuthCore/OIDError.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/AppAuthCore/OIDError.h b/Sources/AppAuthCore/OIDError.h index 5131f0ad4..bf98e5bbe 100644 --- a/Sources/AppAuthCore/OIDError.h +++ b/Sources/AppAuthCore/OIDError.h @@ -151,6 +151,14 @@ typedef NS_ENUM(NSInteger, OIDErrorCode) { /*! @brief The ID Token did not pass validation (e.g. issuer, audience checks). */ OIDErrorCodeIDTokenFailedValidationError = -15, + + /*! @brief The URL does not match the expected redirect URI for this session. + */ + OIDErrorCodeURLMismatch = -16, + + /*! @brief There is no pending authorization callback. The authorization flow may have already completed or was not started. + */ + OIDErrorCodeInvalidAuthorizationFlow = -17, }; /*! @brief Enum of all possible OAuth error codes as defined by RFC6749 From 81f02725dc6d585c38b28ef808faf515b5b82347 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:34:14 -0700 Subject: [PATCH 02/26] g-orchestrated: add resumeExternalUserAgentFlowWithURL:error: to protocol, deprecate old method --- .../AppAuthCore/OIDExternalUserAgentSession.h | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Sources/AppAuthCore/OIDExternalUserAgentSession.h b/Sources/AppAuthCore/OIDExternalUserAgentSession.h index 3b886a6c3..87b441d0c 100644 --- a/Sources/AppAuthCore/OIDExternalUserAgentSession.h +++ b/Sources/AppAuthCore/OIDExternalUserAgentSession.h @@ -51,7 +51,24 @@ NS_ASSUME_NONNULL_BEGIN @remarks Has no effect if called more than once, or after a @c cancel message was received. @return YES if the passed URL matches the expected redirect URL and was consumed, NO otherwise. */ -- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL; +- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL __deprecated_msg("Use resumeExternalUserAgentFlowWithURL:error: instead"); + +/*! @brief Clients should call this method with the result of the external user-agent code flow if + it becomes available. This is the preferred replacement for the deprecated version. + @param URL The redirect URL invoked by the server. + @param error On failure, an NSError describing why the URL was not handled. Pass NULL if you do + not need the error. + @discussion When the URL represented a valid response, implementations should clean up any + left-over UI state from the request, for example by closing the + \SFSafariViewController or loopback HTTP listener if those were used. The completion block + of the pending request should then be invoked. + Two specific error cases: (1) OIDErrorCodeURLMismatch when the URL does not match the + expected redirect, (2) OIDErrorCodeInvalidAuthorizationFlow when no pending authorization + flow exists. + @remarks Has no effect if called more than once, or after a @c cancel message was received. + @return YES if the passed URL matches the expected redirect URL and was consumed, NO otherwise. + */ +- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullable *_Nullable)error; /*! @brief @c OIDExternalUserAgent or clients should call this method when the external user-agent flow failed with a non-OAuth error. From d66de1f9a1a8704218ca4fa4a8b4b59aa905a363 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:36:27 -0700 Subject: [PATCH 03/26] g-orchestrated: implement resumeExternalUserAgentFlowWithURL:error: in OIDAuthorizationSession --- Sources/AppAuthCore/OIDAuthorizationService.m | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index cc749a3f9..e82697b99 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -121,8 +121,17 @@ - (BOOL)shouldHandleURL:(NSURL *)URL { } - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL { + return [self resumeExternalUserAgentFlowWithURL:URL error:nil]; +} + +- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullable *_Nullable)error { // rejects URLs that don't match redirect (these may be completely unrelated to the authorization) if (![self shouldHandleURL:URL]) { + if (error) { + *error = [OIDErrorUtilities errorWithCode:OIDErrorCodeURLMismatch + underlyingError:nil + description:@"URL does not match the expected redirect URI."]; + } return NO; } @@ -130,24 +139,28 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL { // checks for an invalid state if (!_pendingauthorizationFlowCallback) { - [NSException raise:OIDOAuthExceptionInvalidAuthorizationFlow - format:@"%@", OIDOAuthExceptionInvalidAuthorizationFlow, nil]; + if (error) { + *error = [OIDErrorUtilities errorWithCode:OIDErrorCodeInvalidAuthorizationFlow + underlyingError:nil + description:OIDOAuthExceptionInvalidAuthorizationFlow]; + } + return NO; } OIDURLQueryComponent *query = [[OIDURLQueryComponent alloc] initWithURL:URL]; - NSError *error; + NSError *errorLocal; OIDAuthorizationResponse *response = nil; // checks for an OAuth error response as per RFC6749 Section 4.1.2.1 if (query.dictionaryValue[OIDOAuthErrorFieldError]) { - error = [OIDErrorUtilities OAuthErrorWithDomain:OIDOAuthAuthorizationErrorDomain + errorLocal = [OIDErrorUtilities OAuthErrorWithDomain:OIDOAuthAuthorizationErrorDomain OAuthResponse:query.dictionaryValue underlyingError:nil]; } // no error, should be a valid OAuth 2.0 response - if (!error) { + if (!errorLocal) { response = [[OIDAuthorizationResponse alloc] initWithRequest:_request parameters:query.dictionaryValue]; @@ -161,14 +174,14 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL { response.state, response]; response = nil; - error = [NSError errorWithDomain:OIDOAuthAuthorizationErrorDomain + errorLocal = [NSError errorWithDomain:OIDOAuthAuthorizationErrorDomain code:OIDErrorCodeOAuthAuthorizationClientError userInfo:userInfo]; } } [_externalUserAgent dismissExternalUserAgentAnimated:YES completion:^{ - [self didFinishWithResponse:response error:error]; + [self didFinishWithResponse:response error:errorLocal]; }]; return YES; From b86c33e2feb1047a1ef3d3f1c2ef26ba61667022 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:37:45 -0700 Subject: [PATCH 04/26] g-orchestrated: implement resumeExternalUserAgentFlowWithURL:error: in OIDEndSessionImplementation --- Sources/AppAuthCore/OIDAuthorizationService.m | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index e82697b99..ddec33a6c 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -267,18 +267,32 @@ - (BOOL)shouldHandleURL:(NSURL *)URL { } - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL { + return [self resumeExternalUserAgentFlowWithURL:URL error:nil]; +} + +- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL + error:(NSError *_Nullable *_Nullable)error { // rejects URLs that don't match redirect (these may be completely unrelated to the authorization) if (![self shouldHandleURL:URL]) { + if (error) { + *error = [OIDErrorUtilities errorWithCode:OIDErrorCodeURLMismatch + underlyingError:nil + description:@"URL does not match the expected redirect URI."]; + } return NO; } // checks for an invalid state if (!_pendingEndSessionCallback) { - [NSException raise:OIDOAuthExceptionInvalidAuthorizationFlow - format:@"%@", OIDOAuthExceptionInvalidAuthorizationFlow, nil]; + if (error) { + *error = [OIDErrorUtilities errorWithCode:OIDErrorCodeInvalidAuthorizationFlow + underlyingError:nil + description:OIDOAuthExceptionInvalidAuthorizationFlow]; + } + return NO; } - NSError *error; + NSError *responseError; OIDEndSessionResponse *response = nil; OIDURLQueryComponent *query = [[OIDURLQueryComponent alloc] initWithURL:URL]; @@ -295,13 +309,13 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL { response.state, response]; response = nil; - error = [NSError errorWithDomain:OIDOAuthAuthorizationErrorDomain + responseError = [NSError errorWithDomain:OIDOAuthAuthorizationErrorDomain code:OIDErrorCodeOAuthAuthorizationClientError userInfo:userInfo]; } [_externalUserAgent dismissExternalUserAgentAnimated:YES completion:^{ - [self didFinishWithResponse:response error:error]; + [self didFinishWithResponse:response error:responseError]; }]; return YES; From 4437b2d706e30b0b99aed1c00dab666f654e8c93 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:44:08 -0700 Subject: [PATCH 05/26] g-orchestrated: update resumeExternalUserAgentFlowWithURL call site in iOS agent --- Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m index 7a3fa2278..556cd132f 100644 --- a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m +++ b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m @@ -114,7 +114,7 @@ - (BOOL)presentExternalUserAgentRequest:(id)request } strongSelf->_webAuthenticationVC = nil; if (callbackURL) { - [strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL]; + [strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL error:nil]; } else { NSError *safariError = [OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow From 125ea26a886c7f0b6f75bbba13b84e864dc599ae Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:49:54 -0700 Subject: [PATCH 06/26] g-orchestrated: update resumeExternalUserAgentFlowWithURL call site in Catalyst agent --- Sources/AppAuth/iOS/OIDExternalUserAgentCatalyst.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AppAuth/iOS/OIDExternalUserAgentCatalyst.m b/Sources/AppAuth/iOS/OIDExternalUserAgentCatalyst.m index d6771b3e9..4c10c154d 100644 --- a/Sources/AppAuth/iOS/OIDExternalUserAgentCatalyst.m +++ b/Sources/AppAuth/iOS/OIDExternalUserAgentCatalyst.m @@ -89,7 +89,7 @@ - (BOOL)presentExternalUserAgentRequest:(id)request } strongSelf->_webAuthenticationVC = nil; if (callbackURL) { - [strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL]; + [strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL error:nil]; } else { NSError *safariError = [OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow From 667c141c43c3b9677ba9128c526f455490528034 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:53:46 -0700 Subject: [PATCH 07/26] g-orchestrated: update resumeExternalUserAgentFlowWithURL call site in macOS agent --- Sources/AppAuth/macOS/OIDExternalUserAgentMac.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AppAuth/macOS/OIDExternalUserAgentMac.m b/Sources/AppAuth/macOS/OIDExternalUserAgentMac.m index d1e08f902..184767668 100644 --- a/Sources/AppAuth/macOS/OIDExternalUserAgentMac.m +++ b/Sources/AppAuth/macOS/OIDExternalUserAgentMac.m @@ -97,7 +97,7 @@ - (BOOL)presentExternalUserAgentRequest:(id)request } strongSelf->_webAuthenticationSession = nil; if (callbackURL) { - [strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL]; + [strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL error:nil]; } else { NSError *safariError = [OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow From a1a02286a12e4df05b510452e07b579764f82915 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:54:10 -0700 Subject: [PATCH 08/26] g-orchestrated: update resumeExternalUserAgentFlowWithURL call site in redirect handler --- Sources/AppAuth/macOS/OIDRedirectHTTPHandler.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AppAuth/macOS/OIDRedirectHTTPHandler.m b/Sources/AppAuth/macOS/OIDRedirectHTTPHandler.m index 3d0d765d8..5f7c4813b 100644 --- a/Sources/AppAuth/macOS/OIDRedirectHTTPHandler.m +++ b/Sources/AppAuth/macOS/OIDRedirectHTTPHandler.m @@ -126,7 +126,7 @@ - (void)stopHTTPListener { - (void)HTTPConnection:(HTTPConnection *)conn didReceiveRequest:(HTTPServerRequest *)mess { // Sends URL to AppAuth. CFURLRef url = CFHTTPMessageCopyRequestURL(mess.request); - BOOL handled = [_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:(__bridge NSURL *)url]; + BOOL handled = [_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:(__bridge NSURL *)url error:nil]; // Stops listening to further requests after the first valid authorization response. if (handled) { From 54f70a3443d36e07d238ca4126661111835a3411 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:57:43 -0700 Subject: [PATCH 09/26] g-orchestrated: add notes about preferred error-returning API in README snippets --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 540feda72..83d0b7276 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,7 @@ authorization session (created in the previous session): options:(NSDictionary *)options { // Sends the URL to the current authorization flow (if any) which will // process it if it relates to an authorization response. + // Note: resumeExternalUserAgentFlowWithURL:error: is now preferred. if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { _currentAuthorizationFlow = nil; return YES; @@ -401,6 +402,7 @@ func application(_ app: UIApplication, // Sends the URL to the current authorization flow (if any) which will // process it if it relates to an authorization response. if let authorizationFlow = self.currentAuthorizationFlow, + // Note: the error-returning variant is now preferred. authorizationFlow.resumeExternalUserAgentFlow(with: url) { self.currentAuthorizationFlow = nil return true From 19d20e473c67a3d36b02828eff9fd8bc23bc80cf Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:58:57 -0700 Subject: [PATCH 10/26] g-orchestrated: add CHANGELOG entry for error-returning API --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d86a0f705..9fa4e33d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.1.0 +- Added `resumeExternalUserAgentFlowWithURL:error:` to `OIDExternalUserAgentSession` protocol. This method returns errors via an out-parameter instead of throwing `NSException` in invalid-state scenarios. The previous `resumeExternalUserAgentFlowWithURL:` method is deprecated but continues to work. +- Added `OIDErrorCodeURLMismatch` and `OIDErrorCodeInvalidAuthorizationFlow` error codes. + # 2.0.0 - Raise minimum supported iOS version to iOS 12. ([#918](https://github.com/openid/AppAuth-iOS/pull/918)) - Remove deprecated `[UIApplication openURL:]` method to compile with Xcode 16. ([#911](https://github.com/openid/AppAuth-iOS/pull/911)) From 4bd3e88890dc41fad3350b94620afbf0f18f0235 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:59:52 -0700 Subject: [PATCH 11/26] g-orchestrated: update ObjC example to use error-returning API --- Examples/Example-iOS_ObjC/Source/AppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example-iOS_ObjC/Source/AppDelegate.m b/Examples/Example-iOS_ObjC/Source/AppDelegate.m index cbf861d4a..d00f1a25a 100644 --- a/Examples/Example-iOS_ObjC/Source/AppDelegate.m +++ b/Examples/Example-iOS_ObjC/Source/AppDelegate.m @@ -49,7 +49,7 @@ - (BOOL)application:(UIApplication *)app options:(NSDictionary *)options { // Sends the URL to the current authorization flow (if any) which will process it if it relates to // an authorization response. - if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { + if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url error:nil]) { _currentAuthorizationFlow = nil; return YES; } From 710eec318da6eb978610db9074fce8da42ebd38e Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:00:09 -0700 Subject: [PATCH 12/26] g-orchestrated: update ObjC-Carthage example to use error-returning API --- Examples/Example-iOS_ObjC-Carthage/Source/AppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example-iOS_ObjC-Carthage/Source/AppDelegate.m b/Examples/Example-iOS_ObjC-Carthage/Source/AppDelegate.m index 716270381..f646eee17 100644 --- a/Examples/Example-iOS_ObjC-Carthage/Source/AppDelegate.m +++ b/Examples/Example-iOS_ObjC-Carthage/Source/AppDelegate.m @@ -49,7 +49,7 @@ - (BOOL)application:(UIApplication *)app options:(NSDictionary *)options { // Sends the URL to the current authorization flow (if any) which will process it if it relates to // an authorization response. - if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { + if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url error:nil]) { _currentAuthorizationFlow = nil; return YES; } From 56ef29b41647605a4f03ba432261d9a982f4ff3d Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:00:32 -0700 Subject: [PATCH 13/26] g-orchestrated: update macOS example to use error-returning API --- Examples/Example-macOS/Source/AppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example-macOS/Source/AppDelegate.m b/Examples/Example-macOS/Source/AppDelegate.m index 7e5a8068b..b5de912de 100644 --- a/Examples/Example-macOS/Source/AppDelegate.m +++ b/Examples/Example-macOS/Source/AppDelegate.m @@ -49,7 +49,7 @@ - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { NSString *URLString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; NSURL *URL = [NSURL URLWithString:URLString]; - [_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:URL]; + [_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:URL error:nil]; } @end From 2220eef75fabca987995dbe2f26ffc6ed095e06b Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:00:54 -0700 Subject: [PATCH 14/26] g-orchestrated: add deprecation note to Swift example --- Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift b/Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift index 070c3d71a..8ad0aafd5 100644 --- a/Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift +++ b/Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift @@ -32,6 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { + // Note: resumeExternalUserAgentFlow(with:) is deprecated; the error-throwing variant is preferred. if let authorizationFlow = self.currentAuthorizationFlow, authorizationFlow.resumeExternalUserAgentFlow(with: url) { self.currentAuthorizationFlow = nil return true From 62232e347ed5b0d2718cb70b0800a17135be00cf Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:54:36 -0700 Subject: [PATCH 15/26] Demonstrate error handling in README ObjC snippet --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 83d0b7276..96db4e145 100644 --- a/README.md +++ b/README.md @@ -382,10 +382,15 @@ authorization session (created in the previous session): options:(NSDictionary *)options { // Sends the URL to the current authorization flow (if any) which will // process it if it relates to an authorization response. - // Note: resumeExternalUserAgentFlowWithURL:error: is now preferred. - if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { + // Inspect the error to distinguish a benign mismatch (the URL belongs to + // another handler) from an unexpected condition such as no pending flow, + // which previously surfaced as an NSException. + NSError *error = nil; + if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url error:&error]) { _currentAuthorizationFlow = nil; return YES; + } else if (error) { + NSLog(@"Authorization flow could not handle URL: %@", error.localizedDescription); } // Your additional URL handling (if any) goes here. From 082c03277e08742ca8d9b177a8a7f18f679670e7 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:55:09 -0700 Subject: [PATCH 16/26] Demonstrate error handling in README Swift snippet --- README.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 96db4e145..d93bcc5f3 100644 --- a/README.md +++ b/README.md @@ -405,12 +405,18 @@ func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { // Sends the URL to the current authorization flow (if any) which will - // process it if it relates to an authorization response. - if let authorizationFlow = self.currentAuthorizationFlow, - // Note: the error-returning variant is now preferred. - authorizationFlow.resumeExternalUserAgentFlow(with: url) { - self.currentAuthorizationFlow = nil - return true + // process it if it relates to an authorization response. Handling the + // error lets you distinguish a benign URL mismatch (the URL belongs to + // another handler) from an unexpected condition such as no pending flow, + // which previously surfaced as an NSException. + if let authorizationFlow = self.currentAuthorizationFlow { + do { + try authorizationFlow.resumeExternalUserAgentFlow(with: url) + self.currentAuthorizationFlow = nil + return true + } catch { + print("Authorization flow could not handle URL: \(error.localizedDescription)") + } } // Your additional URL handling (if any) From f378d1d8f12f7c25bb12a26d9f9908be5ce77c89 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:55:35 -0700 Subject: [PATCH 17/26] Demonstrate error handling in Swift-Carthage example --- .../Source/AppDelegate.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift b/Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift index 8ad0aafd5..e530753fb 100644 --- a/Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift +++ b/Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift @@ -32,10 +32,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { - // Note: resumeExternalUserAgentFlow(with:) is deprecated; the error-throwing variant is preferred. - if let authorizationFlow = self.currentAuthorizationFlow, authorizationFlow.resumeExternalUserAgentFlow(with: url) { - self.currentAuthorizationFlow = nil - return true + // Inspecting the error lets you distinguish a benign URL mismatch + // (the URL belongs to another handler) from an unexpected condition + // such as no pending flow, which previously surfaced as an NSException. + if let authorizationFlow = self.currentAuthorizationFlow { + do { + try authorizationFlow.resumeExternalUserAgentFlow(with: url) + self.currentAuthorizationFlow = nil + return true + } catch { + print("Authorization flow could not handle URL: \(error.localizedDescription)") + } } return false From 3ff64a6bccf17429ea334cc1ccbb0bb43e3d8d39 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 4 May 2026 10:37:35 -0700 Subject: [PATCH 18/26] Suppress -Wdeprecated-implementations on resumeExternalUserAgentFlowWithURL: shims The shims forward to the new error-returning variant for backward compatibility; their callers see the deprecation, but the shim itself must compile cleanly under -Werror. --- Sources/AppAuthCore/OIDAuthorizationService.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index ddec33a6c..72ba0b8af 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -120,9 +120,12 @@ - (BOOL)shouldHandleURL:(NSURL *)URL { return [[self class] URL:URL matchesRedirectionURL:_request.redirectURL]; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL { return [self resumeExternalUserAgentFlowWithURL:URL error:nil]; } +#pragma clang diagnostic pop - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullable *_Nullable)error { // rejects URLs that don't match redirect (these may be completely unrelated to the authorization) @@ -266,9 +269,12 @@ - (BOOL)shouldHandleURL:(NSURL *)URL { matchesRedirectionURL:_request.postLogoutRedirectURL]; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL { return [self resumeExternalUserAgentFlowWithURL:URL error:nil]; } +#pragma clang diagnostic pop - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullable *_Nullable)error { From b3ef79c9a066a3f7c631d71dd4785fa2855442b1 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 4 May 2026 11:36:45 -0700 Subject: [PATCH 19/26] Fix an additional instance of the deprecated API --- UnitTests/OIDRPProfileCode.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnitTests/OIDRPProfileCode.m b/UnitTests/OIDRPProfileCode.m index 1c37b1f30..afc381d8c 100644 --- a/UnitTests/OIDRPProfileCode.m +++ b/UnitTests/OIDRPProfileCode.m @@ -64,7 +64,7 @@ - (BOOL)presentExternalUserAgentRequest:(id )reques NSDictionary* headers = [(NSHTTPURLResponse *)response allHeaderFields]; NSString *location = [headers objectForKey:@"Location"]; NSURL *url = [NSURL URLWithString:location]; - [session resumeExternalUserAgentFlowWithURL:url]; + [session resumeExternalUserAgentFlowWithURL:url error:nil]; }] resume]; return YES; From d69f9ab913d72dfe0f78cea1ec3625cb0292570c Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 4 May 2026 14:13:38 -0700 Subject: [PATCH 20/26] g-orchestrated: clarify CHANGELOG note for deprecated API behavior change The deprecated resumeExternalUserAgentFlowWithURL: now silently returns NO on invalid state instead of raising NSException. Spell that out so callers relying on @try/@catch around the old API aren't surprised. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aae0f355..0f9d6d579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Unreleased - Add SwiftUI + Swift Package Manager sample app under `Examples/Example-iOS_Swift-SPM`. ([#952](https://github.com/openid/AppAuth-iOS/pull/952)) - Removed external browser (Safari) fallback from `OIDExternalUserAgentIOS`. If `ASWebAuthenticationSession` fails to start (e.g., Guided Access isenabled), the authorization flow now fails with an error instead of opening an external browser. -- Added `resumeExternalUserAgentFlowWithURL:error:` to `OIDExternalUserAgentSession` protocol. This method returns errors via an out-parameter instead of throwing `NSException` in invalid-state scenarios. The previous `resumeExternalUserAgentFlowWithURL:` method is deprecated but continues to work. +- Added `resumeExternalUserAgentFlowWithURL:error:` to `OIDExternalUserAgentSession` protocol. This method returns errors via an out-parameter instead of throwing `NSException` in invalid-state scenarios. The previous `resumeExternalUserAgentFlowWithURL:` method is deprecated and forwards to the new method. Note that, as a side effect of this forwarding, the deprecated method no longer raises `NSException` (with name `OIDOAuthExceptionInvalidAuthorizationFlow`) on invalid state — it now returns `NO` silently. Migrate to the `resumeExternalUserAgentFlowWithURL:error:` variant to inspect the cause. - Added `OIDErrorCodeURLMismatch` and `OIDErrorCodeInvalidAuthorizationFlow` error codes. # 2.0.0 From aaaf256cb863e6e0990640822024ac704d663029 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 4 May 2026 14:13:41 -0700 Subject: [PATCH 21/26] g-orchestrated: migrate Example-iOS_Swift-SPM AppDelegate to throwing API The new SwiftUI/SPM sample (added in #952) was using the deprecated resumeExternalUserAgentFlow(with:); update it to use the throwing variant, mirroring the Carthage Swift sample. --- .../Example/AppDelegate.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Examples/Example-iOS_Swift-SPM/Example/AppDelegate.swift b/Examples/Example-iOS_Swift-SPM/Example/AppDelegate.swift index a1838b2cd..adb459ee9 100644 --- a/Examples/Example-iOS_Swift-SPM/Example/AppDelegate.swift +++ b/Examples/Example-iOS_Swift-SPM/Example/AppDelegate.swift @@ -24,9 +24,17 @@ class AppDelegate: NSObject, UIApplicationDelegate { var currentAuthorizationFlow: OIDExternalUserAgentSession? func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - if let authorizationFlow = self.currentAuthorizationFlow, authorizationFlow.resumeExternalUserAgentFlow(with: url) { - self.currentAuthorizationFlow = nil - return true + // Inspecting the error lets you distinguish a benign URL mismatch + // (the URL belongs to another handler) from an unexpected condition + // such as no pending flow, which previously surfaced as an NSException. + if let authorizationFlow = self.currentAuthorizationFlow { + do { + try authorizationFlow.resumeExternalUserAgentFlow(with: url) + self.currentAuthorizationFlow = nil + return true + } catch { + print("Authorization flow could not handle URL: \(error.localizedDescription)") + } } return false From e5f45f48ff3dec9a65b155a8a6d318559d0e7425 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 4 May 2026 14:13:42 -0700 Subject: [PATCH 22/26] g-orchestrated: tighten OIDExternalUserAgentSession.h @return doc Spell out both NO cases (URLMismatch + InvalidAuthorizationFlow) on the new :error: method so callers know what NO can mean. --- Sources/AppAuthCore/OIDExternalUserAgentSession.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/AppAuthCore/OIDExternalUserAgentSession.h b/Sources/AppAuthCore/OIDExternalUserAgentSession.h index 87b441d0c..7bb080c58 100644 --- a/Sources/AppAuthCore/OIDExternalUserAgentSession.h +++ b/Sources/AppAuthCore/OIDExternalUserAgentSession.h @@ -66,7 +66,9 @@ NS_ASSUME_NONNULL_BEGIN expected redirect, (2) OIDErrorCodeInvalidAuthorizationFlow when no pending authorization flow exists. @remarks Has no effect if called more than once, or after a @c cancel message was received. - @return YES if the passed URL matches the expected redirect URL and was consumed, NO otherwise. + @return YES if the passed URL matches the expected redirect URL and was consumed. + NO if the URL did not match (\@c OIDErrorCodeURLMismatch) or no authorization flow + was pending (\@c OIDErrorCodeInvalidAuthorizationFlow). */ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullable *_Nullable)error; From 78c51d85954747dd68644867395331998636f0cc Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 4 May 2026 14:13:43 -0700 Subject: [PATCH 23/26] g-orchestrated: replace exception-prose error description with user-facing string OIDOAuthExceptionInvalidAuthorizationFlow is exception-prose, not error prose. Use a clean inline localizedDescription at both call sites instead. The constant remains in OIDError.{h,m} for backward compatibility. --- Sources/AppAuthCore/OIDAuthorizationService.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index 72ba0b8af..069a97eed 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -145,7 +145,7 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullabl if (error) { *error = [OIDErrorUtilities errorWithCode:OIDErrorCodeInvalidAuthorizationFlow underlyingError:nil - description:OIDOAuthExceptionInvalidAuthorizationFlow]; + description:@"There is no pending authorization flow to resume."]; } return NO; } @@ -292,7 +292,7 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL if (error) { *error = [OIDErrorUtilities errorWithCode:OIDErrorCodeInvalidAuthorizationFlow underlyingError:nil - description:OIDOAuthExceptionInvalidAuthorizationFlow]; + description:@"There is no pending authorization flow to resume."]; } return NO; } From ea5fa5eb958bb2cf6b585e4bced79b32a81e0d26 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 4 May 2026 14:13:44 -0700 Subject: [PATCH 24/26] g-orchestrated: tighten OIDError.h doc for OIDErrorCodeInvalidAuthorizationFlow The previous wording ("may have already completed or was not started") was speculative. Describe the actual trigger: the redirect URL was received but no pending callback exists. --- Sources/AppAuthCore/OIDError.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/AppAuthCore/OIDError.h b/Sources/AppAuthCore/OIDError.h index bf98e5bbe..256244b4b 100644 --- a/Sources/AppAuthCore/OIDError.h +++ b/Sources/AppAuthCore/OIDError.h @@ -156,7 +156,8 @@ typedef NS_ENUM(NSInteger, OIDErrorCode) { */ OIDErrorCodeURLMismatch = -16, - /*! @brief There is no pending authorization callback. The authorization flow may have already completed or was not started. + /*! @brief The redirect URL was received, but the session has no pending callback to deliver it + to (the callback was already invoked or the session was cancelled). */ OIDErrorCodeInvalidAuthorizationFlow = -17, }; From 4dbc8da920d389f669c5663566f86ef6662a4bc6 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 4 May 2026 14:13:44 -0700 Subject: [PATCH 25/26] g-orchestrated: filter benign URLMismatch errors from README log examples The previous snippets logged every error from resumeExternalUserAgentFlowWithURL:error:, which is noisy because OIDErrorCodeURLMismatch fires on every unrelated deeplink. Only log on OIDErrorCodeInvalidAuthorizationFlow. --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7734e2ff9..edbd54632 100644 --- a/README.md +++ b/README.md @@ -380,14 +380,15 @@ authorization session (created in the previous session): options:(NSDictionary *)options { // Sends the URL to the current authorization flow (if any) which will // process it if it relates to an authorization response. - // Inspect the error to distinguish a benign mismatch (the URL belongs to - // another handler) from an unexpected condition such as no pending flow, - // which previously surfaced as an NSException. + // Handling the error lets you filter for conditions that require + // logging, such as an unexpected state + // (OIDErrorCodeInvalidAuthorizationFlow). Benign mismatches + // (OIDErrorCodeURLMismatch) are kept silent. NSError *error = nil; if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url error:&error]) { _currentAuthorizationFlow = nil; return YES; - } else if (error) { + } else if (error.code == OIDErrorCodeInvalidAuthorizationFlow) { NSLog(@"Authorization flow could not handle URL: %@", error.localizedDescription); } @@ -404,16 +405,18 @@ func application(_ app: UIApplication, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { // Sends the URL to the current authorization flow (if any) which will // process it if it relates to an authorization response. Handling the - // error lets you distinguish a benign URL mismatch (the URL belongs to - // another handler) from an unexpected condition such as no pending flow, - // which previously surfaced as an NSException. + // error lets you filter for conditions that require logging, such as + // an unexpected state (OIDErrorCodeInvalidAuthorizationFlow). Benign + // mismatches (OIDErrorCodeURLMismatch) are kept silent. if let authorizationFlow = self.currentAuthorizationFlow { do { try authorizationFlow.resumeExternalUserAgentFlow(with: url) self.currentAuthorizationFlow = nil return true - } catch { + } catch let error as NSError where error.code == OIDErrorCodeInvalidAuthorizationFlow.rawValue { print("Authorization flow could not handle URL: \(error.localizedDescription)") + } catch { + // Benign mismatch (e.g. OIDErrorCodeURLMismatch): fall through. } } From 8a154b48066998c24d6e3e546909da78325a6471 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 4 May 2026 14:33:59 -0700 Subject: [PATCH 26/26] Add relevant PR to CHANGELOG.md entry. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f9d6d579..9a397dec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Unreleased - Add SwiftUI + Swift Package Manager sample app under `Examples/Example-iOS_Swift-SPM`. ([#952](https://github.com/openid/AppAuth-iOS/pull/952)) - Removed external browser (Safari) fallback from `OIDExternalUserAgentIOS`. If `ASWebAuthenticationSession` fails to start (e.g., Guided Access isenabled), the authorization flow now fails with an error instead of opening an external browser. -- Added `resumeExternalUserAgentFlowWithURL:error:` to `OIDExternalUserAgentSession` protocol. This method returns errors via an out-parameter instead of throwing `NSException` in invalid-state scenarios. The previous `resumeExternalUserAgentFlowWithURL:` method is deprecated and forwards to the new method. Note that, as a side effect of this forwarding, the deprecated method no longer raises `NSException` (with name `OIDOAuthExceptionInvalidAuthorizationFlow`) on invalid state — it now returns `NO` silently. Migrate to the `resumeExternalUserAgentFlowWithURL:error:` variant to inspect the cause. -- Added `OIDErrorCodeURLMismatch` and `OIDErrorCodeInvalidAuthorizationFlow` error codes. +- Added `resumeExternalUserAgentFlowWithURL:error:` to `OIDExternalUserAgentSession` protocol. This method returns errors via an out-parameter instead of throwing `NSException` in invalid-state scenarios. The previous `resumeExternalUserAgentFlowWithURL:` method is deprecated and forwards to the new method. Note that, as a side effect of this forwarding, the deprecated method no longer raises `NSException` (with name `OIDOAuthExceptionInvalidAuthorizationFlow`) on invalid state — it now returns `NO` silently. Migrate to the `resumeExternalUserAgentFlowWithURL:error:` variant to inspect the cause. ([#955](https://github.com/openid/AppAuth-iOS/pull/955)) +- Added `OIDErrorCodeURLMismatch` and `OIDErrorCodeInvalidAuthorizationFlow` error codes. ([#955](https://github.com/openid/AppAuth-iOS/pull/955)) # 2.0.0 - Raise minimum supported iOS version to iOS 12. ([#918](https://github.com/openid/AppAuth-iOS/pull/918))