Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
681d01c
g-orchestrated: add OIDErrorCodeURLMismatch and OIDErrorCodeInvalidAu…
w-goog Apr 28, 2026
81f0272
g-orchestrated: add resumeExternalUserAgentFlowWithURL:error: to prot…
w-goog Apr 28, 2026
d66de1f
g-orchestrated: implement resumeExternalUserAgentFlowWithURL:error: i…
w-goog Apr 28, 2026
b86c33e
g-orchestrated: implement resumeExternalUserAgentFlowWithURL:error: i…
w-goog Apr 28, 2026
4437b2d
g-orchestrated: update resumeExternalUserAgentFlowWithURL call site i…
w-goog Apr 28, 2026
125ea26
g-orchestrated: update resumeExternalUserAgentFlowWithURL call site i…
w-goog Apr 28, 2026
667c141
g-orchestrated: update resumeExternalUserAgentFlowWithURL call site i…
w-goog Apr 28, 2026
a1a0228
g-orchestrated: update resumeExternalUserAgentFlowWithURL call site i…
w-goog Apr 28, 2026
54f70a3
g-orchestrated: add notes about preferred error-returning API in READ…
w-goog Apr 28, 2026
19d20e4
g-orchestrated: add CHANGELOG entry for error-returning API
w-goog Apr 28, 2026
4bd3e88
g-orchestrated: update ObjC example to use error-returning API
w-goog Apr 28, 2026
710eec3
g-orchestrated: update ObjC-Carthage example to use error-returning API
w-goog Apr 28, 2026
56ef29b
g-orchestrated: update macOS example to use error-returning API
w-goog Apr 28, 2026
2220eef
g-orchestrated: add deprecation note to Swift example
w-goog Apr 28, 2026
dce930c
Merge codegen/error-returns-api
w-goog Apr 28, 2026
6c3de6a
Merge codegen/error-returns-impl
w-goog Apr 28, 2026
06018a2
Merge codegen/error-returns-callsites
w-goog Apr 28, 2026
bd0e75d
Merge codegen/error-returns-docs
w-goog Apr 28, 2026
62232e3
Demonstrate error handling in README ObjC snippet
w-goog Apr 28, 2026
082c032
Demonstrate error handling in README Swift snippet
w-goog Apr 28, 2026
f378d1d
Demonstrate error handling in Swift-Carthage example
w-goog Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
2 changes: 1 addition & 1 deletion Examples/Example-iOS_ObjC-Carthage/Source/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ - (BOOL)application:(UIApplication *)app
options:(NSDictionary<NSString *, id> *)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;
}
Expand Down
2 changes: 1 addition & 1 deletion Examples/Example-iOS_ObjC/Source/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ - (BOOL)application:(UIApplication *)app
options:(NSDictionary<NSString *, id> *)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;
}
Expand Down
14 changes: 11 additions & 3 deletions Examples/Example-iOS_Swift-Carthage/Source/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : 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
Expand Down
2 changes: 1 addition & 1 deletion Examples/Example-macOS/Source/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,15 @@ authorization session (created in the previous session):
options:(NSDictionary<NSString *, id> *)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]) {
// 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.
Expand All @@ -399,11 +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,
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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppAuth/iOS/OIDExternalUserAgentCatalyst.m
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ - (BOOL)presentExternalUserAgentRequest:(id<OIDExternalUserAgentRequest>)request
}
strongSelf->_webAuthenticationVC = nil;
if (callbackURL) {
[strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL];
[strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL error:nil];
} else {
NSError *safariError =
[OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ - (BOOL)presentExternalUserAgentRequest:(id<OIDExternalUserAgentRequest>)request
}
strongSelf->_webAuthenticationVC = nil;
if (callbackURL) {
[strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL];
[strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL error:nil];
} else {
NSError *safariError =
[OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppAuth/macOS/OIDExternalUserAgentMac.m
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ - (BOOL)presentExternalUserAgentRequest:(id<OIDExternalUserAgentRequest>)request
}
strongSelf->_webAuthenticationSession = nil;
if (callbackURL) {
[strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL];
[strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL error:nil];
} else {
NSError *safariError =
[OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppAuth/macOS/OIDRedirectHTTPHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
51 changes: 39 additions & 12 deletions Sources/AppAuthCore/OIDAuthorizationService.m
Original file line number Diff line number Diff line change
Expand Up @@ -121,33 +121,46 @@ - (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;
}

AppAuthRequestTrace(@"Authorization Response: %@", 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];

Expand All @@ -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;
Expand Down Expand Up @@ -254,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];
Expand All @@ -282,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;
Expand Down
8 changes: 8 additions & 0 deletions Sources/AppAuthCore/OIDError.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion Sources/AppAuthCore/OIDExternalUserAgentSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading