From 9d54480a0907eee119e45566f46f7c41199ae77d Mon Sep 17 00:00:00 2001 From: Donald Roshi Date: Fri, 26 Sep 2025 12:23:29 +0200 Subject: [PATCH 1/5] support universal link as redirect url For iOS, when version 17.4+ is available, check whether the redirect url is a universal link. If it is, use the new init method with callback accepting the universal link. --- Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m index eb8c3f08d..328a02044 100644 --- a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m +++ b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m @@ -25,6 +25,7 @@ #import #import +#import #import "OIDErrorUtilities.h" #import "OIDExternalUserAgentSession.h" #import "OIDExternalUserAgentRequest.h" @@ -97,10 +98,42 @@ - (BOOL)presentExternalUserAgentRequest:(id)request BOOL openedUserAgent = NO; NSURL *requestURL = [request externalUserAgentRequestURL]; + // iOS 17.4 and later, use ASWebAuthenticationSession with universal link + if (@available(iOS 17.4, *) && [[request.redirectScheme lowercaseString] isEqualToString:@"https"]) { + // ASWebAuthenticationSession doesn't work with guided access (rdar://40809553) + if (!UIAccessibilityIsGuidedAccessEnabled()) { + __weak OIDExternalUserAgentIOS *weakSelf = self; + NSURL *redirectURL = ((OIDAuthorizationRequest *)request).redirectURL; + ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithHTTPSHost:redirectURL.host path:redirectURL.path]; + ASWebAuthenticationSession *authenticationVC = + [[ASWebAuthenticationSession alloc] initWithURL:requestURL + callback:callback + completionHandler:^(NSURL * _Nullable callbackURL, + NSError * _Nullable error) { + __strong OIDExternalUserAgentIOS *strongSelf = weakSelf; + if (!strongSelf) { return; } + strongSelf->_webAuthenticationVC = nil; + if (callbackURL) { + [strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL]; + } else { + NSError *safariError = + [OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow + underlyingError:error + description:nil]; + [strongSelf->_session failExternalUserAgentFlowWithError:safariError]; + } + }]; + authenticationVC.presentationContextProvider = self; + authenticationVC.prefersEphemeralWebBrowserSession = NO; + _webAuthenticationVC = authenticationVC; + openedUserAgent = [authenticationVC start]; + } + } + // iOS 12 and later, use ASWebAuthenticationSession if (@available(iOS 12.0, *)) { // ASWebAuthenticationSession doesn't work with guided access (rdar://40809553) - if (!UIAccessibilityIsGuidedAccessEnabled()) { + if (!openedUserAgent && !UIAccessibilityIsGuidedAccessEnabled()) { __weak OIDExternalUserAgentIOS *weakSelf = self; NSString *redirectScheme = request.redirectScheme; ASWebAuthenticationSession *authenticationVC = From 09cbc594f00cdd61547e904754416004069b403b Mon Sep 17 00:00:00 2001 From: Khaleel Shaheen Date: Tue, 15 Sep 2026 02:22:28 +0300 Subject: [PATCH 2/5] Address review feedback for universal link support --- AppAuth.xcodeproj/project.pbxproj | 6 + Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m | 71 ++++++- UnitTests/OIDExternalUserAgentIOSTests.m | 199 ++++++++++++++++++ 3 files changed, 265 insertions(+), 11 deletions(-) create mode 100644 UnitTests/OIDExternalUserAgentIOSTests.m diff --git a/AppAuth.xcodeproj/project.pbxproj b/AppAuth.xcodeproj/project.pbxproj index 7cfd05bc5..7c5d042c6 100644 --- a/AppAuth.xcodeproj/project.pbxproj +++ b/AppAuth.xcodeproj/project.pbxproj @@ -558,6 +558,8 @@ A5EEF29A20D821960044F470 /* OIDTokenUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A5EEF1FD20CF07760044F470 /* OIDTokenUtilitiesTests.m */; }; A5EEF29B20D821970044F470 /* OIDTokenUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A5EEF1FD20CF07760044F470 /* OIDTokenUtilitiesTests.m */; }; A5EEF29C20D821970044F470 /* OIDTokenUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A5EEF1FD20CF07760044F470 /* OIDTokenUtilitiesTests.m */; }; + AA00AF0100000000000AF001 /* OIDExternalUserAgentIOSTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AA00AF0000000000000AF000 /* OIDExternalUserAgentIOSTests.m */; }; + AA00AF0200000000000AF002 /* OIDExternalUserAgentIOSTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AA00AF0000000000000AF000 /* OIDExternalUserAgentIOSTests.m */; }; A6CEB11A2007E49C009D492A /* OIDEndSessionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */; }; A6CEB11B2007E49D009D492A /* OIDEndSessionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */; }; A6CEB11C2007E49E009D492A /* OIDEndSessionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */; }; @@ -836,6 +838,7 @@ 73F574332B7C42690023FFF0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; A5EEF1FD20CF07760044F470 /* OIDTokenUtilitiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OIDTokenUtilitiesTests.m; sourceTree = ""; }; A6CEB1172007E384009D492A /* OIDEndSessionRequestTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OIDEndSessionRequestTests.h; sourceTree = ""; }; + AA00AF0000000000000AF000 /* OIDExternalUserAgentIOSTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OIDExternalUserAgentIOSTests.m; sourceTree = ""; }; A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OIDEndSessionRequestTests.m; sourceTree = ""; }; A6DEAB982018E4A20022AC32 /* OIDExternalUserAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OIDExternalUserAgent.h; sourceTree = ""; }; A6DEAB992018E4A20022AC32 /* OIDExternalUserAgentSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OIDExternalUserAgentSession.h; sourceTree = ""; }; @@ -1122,6 +1125,7 @@ 341742071C5D82D3000EF209 /* OIDResponseTypesTests.m */, A6CEB1172007E384009D492A /* OIDEndSessionRequestTests.h */, A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */, + AA00AF0000000000000AF000 /* OIDExternalUserAgentIOSTests.m */, 341742081C5D82D3000EF209 /* OIDScopesTests.m */, 341742091C5D82D3000EF209 /* OIDServiceConfigurationTests.h */, 3417420A1C5D82D3000EF209 /* OIDServiceConfigurationTests.m */, @@ -2202,6 +2206,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AA00AF0100000000000AF001 /* OIDExternalUserAgentIOSTests.m in Sources */, 34D5EC451E6D1AD900814354 /* OIDSwiftTests.swift in Sources */, 341742211C5D82D3000EF209 /* OIDURLQueryComponentTests.m in Sources */, 341742201C5D82D3000EF209 /* OIDTokenResponseTests.m in Sources */, @@ -2376,6 +2381,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AA00AF0200000000000AF002 /* OIDExternalUserAgentIOSTests.m in Sources */, 343AAA781E8346B400F9D36E /* OIDScopesTests.m in Sources */, 343AAA7D1E8346B400F9D36E /* OIDURLQueryComponentTests.m in Sources */, 343AAA791E8346B400F9D36E /* OIDServiceConfigurationTests.m in Sources */, diff --git a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m index 328a02044..aef03dc37 100644 --- a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m +++ b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m @@ -25,7 +25,8 @@ #import #import -#import +#import "OIDAuthorizationRequest.h" +#import "OIDEndSessionRequest.h" #import "OIDErrorUtilities.h" #import "OIDExternalUserAgentSession.h" #import "OIDExternalUserAgentRequest.h" @@ -42,6 +43,27 @@ @interface OIDExternalUserAgentIOS () @end #endif +API_AVAILABLE(ios(17.4)) +ASWebAuthenticationSessionCallback *_Nullable + OIDHTTPSCallbackForRequest(id request) { + NSURL *redirectURL = nil; + // OIDExternalUserAgentRequest does not conform to NSObject, so message the request as id. + id requestObject = request; + if ([requestObject isKindOfClass:[OIDAuthorizationRequest class]]) { + redirectURL = ((OIDAuthorizationRequest *)requestObject).redirectURL; + } else if ([requestObject isKindOfClass:[OIDEndSessionRequest class]]) { + redirectURL = ((OIDEndSessionRequest *)requestObject).postLogoutRedirectURL; + } + if (![[redirectURL.scheme lowercaseString] isEqualToString:@"https"] || + redirectURL.host.length == 0) { + return nil; + } + // A redirect URL with no path is normalized to the root path, so that it matches the callback + // URL the authorization server redirects to. + NSString *path = redirectURL.path.length > 0 ? redirectURL.path : @"/"; + return [ASWebAuthenticationSessionCallback callbackWithHTTPSHost:redirectURL.host path:path]; +} + @implementation OIDExternalUserAgentIOS { UIViewController *_presentingViewController; BOOL _prefersEphemeralSession; @@ -97,24 +119,51 @@ - (BOOL)presentExternalUserAgentRequest:(id)request _session = session; BOOL openedUserAgent = NO; NSURL *requestURL = [request externalUserAgentRequestURL]; + // An HTTPS redirect is a universal link, which only the iOS 17.4 callback below can handle. + // ASWebAuthenticationSession does not support @c https as a callbackURLScheme, so such a request + // must never reach the scheme based session: it would open a browser whose callback never fires. + // Before iOS 17.4 such a request therefore fails to open, ending the flow with an error rather + // than leaving it hanging. + BOOL hasHTTPSRedirect = [[request.redirectScheme lowercaseString] isEqualToString:@"https"]; - // iOS 17.4 and later, use ASWebAuthenticationSession with universal link - if (@available(iOS 17.4, *) && [[request.redirectScheme lowercaseString] isEqualToString:@"https"]) { + // iOS 17.4 and later: if the redirect URL is an HTTPS universal link, use + // ASWebAuthenticationSession's HTTPS callback. + if (@available(iOS 17.4, *)) { // ASWebAuthenticationSession doesn't work with guided access (rdar://40809553) - if (!UIAccessibilityIsGuidedAccessEnabled()) { + if (hasHTTPSRedirect && !UIAccessibilityIsGuidedAccessEnabled()) { + ASWebAuthenticationSessionCallback *callback = OIDHTTPSCallbackForRequest(request); + if (!callback) { + // The redirect URL is HTTPS but a callback couldn't be created for it (e.g. it has no + // host). A session started with such a redirect could never complete, so fail instead. + [self cleanUp]; + NSError *error = + [OIDErrorUtilities errorWithCode:OIDErrorCodeSafariOpenError + underlyingError:nil + description:@"The request's HTTPS redirect URL is not a valid " + "universal link."]; + [session failExternalUserAgentFlowWithError:error]; + return NO; + } __weak OIDExternalUserAgentIOS *weakSelf = self; - NSURL *redirectURL = ((OIDAuthorizationRequest *)request).redirectURL; - ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithHTTPSHost:redirectURL.host path:redirectURL.path]; ASWebAuthenticationSession *authenticationVC = [[ASWebAuthenticationSession alloc] initWithURL:requestURL - callback:callback + callback:callback completionHandler:^(NSURL * _Nullable callbackURL, NSError * _Nullable error) { __strong OIDExternalUserAgentIOS *strongSelf = weakSelf; - if (!strongSelf) { return; } + if (!strongSelf) { + return; + } strongSelf->_webAuthenticationVC = nil; if (callbackURL) { - [strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL]; + // The callback matches the redirect URL case insensitively and ignores its port, so it + // can fire for a URL the session itself rejects. Report that instead of leaving the + // flow with neither a response nor an error. + NSError *resumeError; + if (![strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL + error:&resumeError]) { + [strongSelf->_session failExternalUserAgentFlowWithError:resumeError]; + } } else { NSError *safariError = [OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow @@ -124,7 +173,7 @@ - (BOOL)presentExternalUserAgentRequest:(id)request } }]; authenticationVC.presentationContextProvider = self; - authenticationVC.prefersEphemeralWebBrowserSession = NO; + authenticationVC.prefersEphemeralWebBrowserSession = _prefersEphemeralSession; _webAuthenticationVC = authenticationVC; openedUserAgent = [authenticationVC start]; } @@ -133,7 +182,7 @@ - (BOOL)presentExternalUserAgentRequest:(id)request // iOS 12 and later, use ASWebAuthenticationSession if (@available(iOS 12.0, *)) { // ASWebAuthenticationSession doesn't work with guided access (rdar://40809553) - if (!openedUserAgent && !UIAccessibilityIsGuidedAccessEnabled()) { + if (!hasHTTPSRedirect && !UIAccessibilityIsGuidedAccessEnabled()) { __weak OIDExternalUserAgentIOS *weakSelf = self; NSString *redirectScheme = request.redirectScheme; ASWebAuthenticationSession *authenticationVC = diff --git a/UnitTests/OIDExternalUserAgentIOSTests.m b/UnitTests/OIDExternalUserAgentIOSTests.m new file mode 100644 index 000000000..05a255e1b --- /dev/null +++ b/UnitTests/OIDExternalUserAgentIOSTests.m @@ -0,0 +1,199 @@ +/*! @file OIDExternalUserAgentIOSTests.m + @brief AppAuth iOS SDK + @copyright + Copyright 2025 Google Inc. All Rights Reserved. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +// These tests exercise iOS-only code. They are excluded from the Swift package's test targets, +// which only depend on AppAuthCore. +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST && !SWIFT_PACKAGE + +#import + +#import + +#import "Sources/AppAuthCore/OIDAuthorizationRequest.h" +#import "Sources/AppAuthCore/OIDEndSessionRequest.h" +#import "Sources/AppAuthCore/OIDExternalUserAgentRequest.h" +#import "Sources/AppAuthCore/OIDResponseTypes.h" +#import "Sources/AppAuthCore/OIDScopes.h" +#import "Sources/AppAuthCore/OIDServiceConfiguration.h" + +/*! @brief Creates the @c ASWebAuthenticationSessionCallback for a request whose redirect URL is an + HTTPS universal link, or nil if one couldn't be created; for example, the request is of an + unsupported type, or its redirect URL is not a valid HTTPS URL with a host. + @discussion Implemented in @c OIDExternalUserAgentIOS.m and declared here rather than in a + header, so that it is testable without becoming part of AppAuth's public API. + */ +extern ASWebAuthenticationSessionCallback *_Nullable + OIDHTTPSCallbackForRequest(id _Nonnull request) + API_AVAILABLE(ios(17.4)); + +// Ignore warnings about "Use of GNU statement expression extension" which is raised by our use of +// the XCTAssert___ macros. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wgnu" + +/*! @brief Test value for the @c clientID property. + */ +static NSString *const kTestClientID = @"ClientID"; + +/*! @brief Test value for the @c authorizationEndpoint property. + */ +static NSString *const kTestAuthorizationEndpoint = @"https://accounts.example.com/authorize"; + +/*! @brief Test value for the @c tokenEndpoint property. + */ +static NSString *const kTestTokenEndpoint = @"https://accounts.example.com/token"; + +/*! @brief Test value for the @c idTokenHint parameter. + */ +static NSString *const kTestIDTokenHint = @"id-token-hint"; + +/*! @brief A request of a type unknown to @c OIDHTTPSCallbackForRequest. + */ +@interface OIDUnsupportedExternalUserAgentRequest : NSObject +@end + +@implementation OIDUnsupportedExternalUserAgentRequest + +- (NSURL *)externalUserAgentRequestURL { + return [NSURL URLWithString:kTestAuthorizationEndpoint]; +} + +- (NSString *)redirectScheme { + return @"https"; +} + +@end + +/*! @brief Unit tests for the iOS external user agent's HTTPS (universal link) callback support. + */ +@interface OIDExternalUserAgentIOSTests : XCTestCase +@end + +@implementation OIDExternalUserAgentIOSTests + +- (OIDAuthorizationRequest *)authorizationRequestWithRedirectURL:(NSURL *)redirectURL { + OIDServiceConfiguration *configuration = [[OIDServiceConfiguration alloc] + initWithAuthorizationEndpoint:[NSURL URLWithString:kTestAuthorizationEndpoint] + tokenEndpoint:[NSURL URLWithString:kTestTokenEndpoint]]; + return [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration + clientId:kTestClientID + scopes:@[ OIDScopeOpenID ] + redirectURL:redirectURL + responseType:OIDResponseTypeCode + additionalParameters:nil]; +} + +/*! @brief An authorization request with an HTTPS redirect gets a callback matching that redirect. + */ +- (void)testHTTPSCallbackForAuthorizationRequest { + if (@available(iOS 17.4, *)) { + OIDAuthorizationRequest *request = [self authorizationRequestWithRedirectURL: + [NSURL URLWithString:@"https://client.example.com/oauth2redirect"]]; + ASWebAuthenticationSessionCallback *callback = OIDHTTPSCallbackForRequest(request); + XCTAssertNotNil(callback); + XCTAssertTrue([callback matchesURL: + [NSURL URLWithString:@"https://client.example.com/oauth2redirect?code=1234"]]); + XCTAssertFalse([callback matchesURL: + [NSURL URLWithString:@"https://other.example.com/oauth2redirect?code=1234"]]); + } else { + XCTSkip(@"ASWebAuthenticationSessionCallback requires iOS 17.4."); + } +} + +/*! @brief An end session request with an HTTPS post-logout redirect gets a callback rather than + crashing on an unchecked cast. + */ +- (void)testHTTPSCallbackForEndSessionRequest { + if (@available(iOS 17.4, *)) { + OIDServiceConfiguration *configuration = [[OIDServiceConfiguration alloc] + initWithAuthorizationEndpoint:[NSURL URLWithString:kTestAuthorizationEndpoint] + tokenEndpoint:[NSURL URLWithString:kTestTokenEndpoint]]; + OIDEndSessionRequest *request = [[OIDEndSessionRequest alloc] + initWithConfiguration:configuration + idTokenHint:kTestIDTokenHint + postLogoutRedirectURL:[NSURL URLWithString:@"https://client.example.com/signout"] + additionalParameters:nil]; + ASWebAuthenticationSessionCallback *callback = OIDHTTPSCallbackForRequest(request); + XCTAssertNotNil(callback); + XCTAssertTrue([callback matchesURL: + [NSURL URLWithString:@"https://client.example.com/signout?state=1234"]]); + } else { + XCTSkip(@"ASWebAuthenticationSessionCallback requires iOS 17.4."); + } +} + +/*! @brief A custom scheme redirect is not a universal link, so no callback is created. + */ +- (void)testNoHTTPSCallbackForCustomSchemeRedirect { + if (@available(iOS 17.4, *)) { + OIDAuthorizationRequest *request = [self authorizationRequestWithRedirectURL: + [NSURL URLWithString:@"com.example.app:/oauth2redirect"]]; + XCTAssertNil(OIDHTTPSCallbackForRequest(request)); + } else { + XCTSkip(@"ASWebAuthenticationSessionCallback requires iOS 17.4."); + } +} + +/*! @brief An HTTPS redirect without a host can never be matched, so no callback is created. + */ +- (void)testNoHTTPSCallbackForRedirectWithoutHost { + if (@available(iOS 17.4, *)) { + OIDAuthorizationRequest *request = [self authorizationRequestWithRedirectURL: + [NSURL URLWithString:@"https:///oauth2redirect"]]; + XCTAssertNil(OIDHTTPSCallbackForRequest(request)); + } else { + XCTSkip(@"ASWebAuthenticationSessionCallback requires iOS 17.4."); + } +} + +/*! @brief An HTTPS redirect without a path matches on the root path. + */ +- (void)testHTTPSCallbackForRedirectWithoutPath { + if (@available(iOS 17.4, *)) { + OIDAuthorizationRequest *request = [self authorizationRequestWithRedirectURL: + [NSURL URLWithString:@"https://client.example.com"]]; + ASWebAuthenticationSessionCallback *callback = OIDHTTPSCallbackForRequest(request); + XCTAssertNotNil(callback); + XCTAssertTrue([callback matchesURL: + [NSURL URLWithString:@"https://client.example.com/?code=1234"]]); + XCTAssertFalse([callback matchesURL: + [NSURL URLWithString:@"https://client.example.com/oauth2redirect?code=1234"]]); + } else { + XCTSkip(@"ASWebAuthenticationSessionCallback requires iOS 17.4."); + } +} + +/*! @brief Request types without a known redirect URL get no callback rather than crashing. + */ +- (void)testNoHTTPSCallbackForUnsupportedRequestType { + if (@available(iOS 17.4, *)) { + id request = + [[OIDUnsupportedExternalUserAgentRequest alloc] init]; + XCTAssertNil(OIDHTTPSCallbackForRequest(request)); + } else { + XCTSkip(@"ASWebAuthenticationSessionCallback requires iOS 17.4."); + } +} + +@end + +#pragma GCC diagnostic pop + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST && !SWIFT_PACKAGE From a02b069a0a038e54009be18361784c03d865e4c4 Mon Sep 17 00:00:00 2001 From: Khaleel Shaheen Date: Thu, 24 Sep 2026 01:00:14 +0300 Subject: [PATCH 3/5] Address review feedback for universal link support --- AppAuth.xcodeproj/project.pbxproj | 12 +- README.md | 3 +- Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m | 180 +++++++----------- UnitTests/OIDExternalUserAgentIOSTests.m | 31 ++- 4 files changed, 93 insertions(+), 133 deletions(-) diff --git a/AppAuth.xcodeproj/project.pbxproj b/AppAuth.xcodeproj/project.pbxproj index 7c5d042c6..64496e053 100644 --- a/AppAuth.xcodeproj/project.pbxproj +++ b/AppAuth.xcodeproj/project.pbxproj @@ -558,8 +558,6 @@ A5EEF29A20D821960044F470 /* OIDTokenUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A5EEF1FD20CF07760044F470 /* OIDTokenUtilitiesTests.m */; }; A5EEF29B20D821970044F470 /* OIDTokenUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A5EEF1FD20CF07760044F470 /* OIDTokenUtilitiesTests.m */; }; A5EEF29C20D821970044F470 /* OIDTokenUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A5EEF1FD20CF07760044F470 /* OIDTokenUtilitiesTests.m */; }; - AA00AF0100000000000AF001 /* OIDExternalUserAgentIOSTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AA00AF0000000000000AF000 /* OIDExternalUserAgentIOSTests.m */; }; - AA00AF0200000000000AF002 /* OIDExternalUserAgentIOSTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AA00AF0000000000000AF000 /* OIDExternalUserAgentIOSTests.m */; }; A6CEB11A2007E49C009D492A /* OIDEndSessionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */; }; A6CEB11B2007E49D009D492A /* OIDEndSessionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */; }; A6CEB11C2007E49E009D492A /* OIDEndSessionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */; }; @@ -608,6 +606,8 @@ CF37C0701F1FC21A00662E41 /* OIDEndSessionRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CF37C06C1F1FC21A00662E41 /* OIDEndSessionRequest.m */; }; CF37C0711F1FC21A00662E41 /* OIDEndSessionRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CF37C06C1F1FC21A00662E41 /* OIDEndSessionRequest.m */; }; CF6431F41F228A980075B6B5 /* OIDEndSessionResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = CF6431F31F228A980075B6B5 /* OIDEndSessionResponse.m */; }; + E56963F03064815800AC4DFE /* OIDExternalUserAgentIOSTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E56963EF3064815800AC4DFE /* OIDExternalUserAgentIOSTests.m */; }; + E56963F13064815800AC4DFE /* OIDExternalUserAgentIOSTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E56963EF3064815800AC4DFE /* OIDExternalUserAgentIOSTests.m */; }; F9A7082E2355ED74004B3E6D /* OIDExternalUserAgentCatalyst.h in Headers */ = {isa = PBXBuildFile; fileRef = F9A7082C2355ED74004B3E6D /* OIDExternalUserAgentCatalyst.h */; settings = {ATTRIBUTES = (Public, ); }; }; F9A7082F2355ED74004B3E6D /* OIDExternalUserAgentCatalyst.m in Sources */ = {isa = PBXBuildFile; fileRef = F9A7082D2355ED74004B3E6D /* OIDExternalUserAgentCatalyst.m */; }; /* End PBXBuildFile section */ @@ -838,7 +838,6 @@ 73F574332B7C42690023FFF0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; A5EEF1FD20CF07760044F470 /* OIDTokenUtilitiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OIDTokenUtilitiesTests.m; sourceTree = ""; }; A6CEB1172007E384009D492A /* OIDEndSessionRequestTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OIDEndSessionRequestTests.h; sourceTree = ""; }; - AA00AF0000000000000AF000 /* OIDExternalUserAgentIOSTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OIDExternalUserAgentIOSTests.m; sourceTree = ""; }; A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OIDEndSessionRequestTests.m; sourceTree = ""; }; A6DEAB982018E4A20022AC32 /* OIDExternalUserAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OIDExternalUserAgent.h; sourceTree = ""; }; A6DEAB992018E4A20022AC32 /* OIDExternalUserAgentSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OIDExternalUserAgentSession.h; sourceTree = ""; }; @@ -851,6 +850,7 @@ CF37C06C1F1FC21A00662E41 /* OIDEndSessionRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OIDEndSessionRequest.m; sourceTree = ""; }; CF6431F21F228A980075B6B5 /* OIDEndSessionResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OIDEndSessionResponse.h; sourceTree = ""; }; CF6431F31F228A980075B6B5 /* OIDEndSessionResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OIDEndSessionResponse.m; sourceTree = ""; }; + E56963EF3064815800AC4DFE /* OIDExternalUserAgentIOSTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OIDExternalUserAgentIOSTests.m; sourceTree = ""; }; F6F60FB01D2BFEFE00325CB3 /* OIDAuthState+IOS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OIDAuthState+IOS.m"; sourceTree = ""; }; F6F60FB11D2BFEFE00325CB3 /* OIDAuthorizationService+IOS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OIDAuthorizationService+IOS.m"; sourceTree = ""; }; F6F60FB31D2BFEFE00325CB3 /* OIDAuthorizationService+IOS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OIDAuthorizationService+IOS.h"; sourceTree = ""; }; @@ -1112,6 +1112,7 @@ 341741FB1C5D82D3000EF209 /* UnitTests */ = { isa = PBXGroup; children = ( + E56963EF3064815800AC4DFE /* OIDExternalUserAgentIOSTests.m */, 2D52A08D24C24C260022E402 /* AppAuthTV */, 341742231C5D8317000EF209 /* UnitTestsInfo.plist */, 341742001C5D82D3000EF209 /* OIDAuthorizationRequestTests.h */, @@ -1125,7 +1126,6 @@ 341742071C5D82D3000EF209 /* OIDResponseTypesTests.m */, A6CEB1172007E384009D492A /* OIDEndSessionRequestTests.h */, A6CEB1182007E384009D492A /* OIDEndSessionRequestTests.m */, - AA00AF0000000000000AF000 /* OIDExternalUserAgentIOSTests.m */, 341742081C5D82D3000EF209 /* OIDScopesTests.m */, 341742091C5D82D3000EF209 /* OIDServiceConfigurationTests.h */, 3417420A1C5D82D3000EF209 /* OIDServiceConfigurationTests.m */, @@ -2206,11 +2206,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AA00AF0100000000000AF001 /* OIDExternalUserAgentIOSTests.m in Sources */, 34D5EC451E6D1AD900814354 /* OIDSwiftTests.swift in Sources */, 341742211C5D82D3000EF209 /* OIDURLQueryComponentTests.m in Sources */, 341742201C5D82D3000EF209 /* OIDTokenResponseTests.m in Sources */, 341742221C5D82D3000EF209 /* OIDURLQueryComponentTestsIOS7.m in Sources */, + E56963F03064815800AC4DFE /* OIDExternalUserAgentIOSTests.m in Sources */, 3417421E1C5D82D3000EF209 /* OIDServiceDiscoveryTests.m in Sources */, 3417421F1C5D82D3000EF209 /* OIDTokenRequestTests.m in Sources */, 341742181C5D82D3000EF209 /* OIDAuthorizationResponseTests.m in Sources */, @@ -2381,7 +2381,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AA00AF0200000000000AF002 /* OIDExternalUserAgentIOSTests.m in Sources */, 343AAA781E8346B400F9D36E /* OIDScopesTests.m in Sources */, 343AAA7D1E8346B400F9D36E /* OIDURLQueryComponentTests.m in Sources */, 343AAA791E8346B400F9D36E /* OIDServiceConfigurationTests.m in Sources */, @@ -2392,6 +2391,7 @@ 343AAA7C1E8346B400F9D36E /* OIDTokenResponseTests.m in Sources */, 343AAA7B1E8346B400F9D36E /* OIDTokenRequestTests.m in Sources */, 343AAA771E8346B400F9D36E /* OIDResponseTypesTests.m in Sources */, + E56963F13064815800AC4DFE /* OIDExternalUserAgentIOSTests.m in Sources */, 343AAA7F1E8346B400F9D36E /* OIDRegistrationRequestTests.m in Sources */, 343AAA731E8346B400F9D36E /* OIDAuthorizationRequestTests.m in Sources */, 343AAA761E8346B400F9D36E /* OIDGrantTypesTests.m in Sources */, diff --git a/README.md b/README.md index 6a30e3973..8dcab0c88 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ Authentication is performed using `ASWebAuthenticationSession`. #### Authorization Server Requirements -Both Custom URI Schemes and Universal Links can be used with the library. +Both Custom URI Schemes and Universal Links (iOS 17.4+, requires the Associated Domains +entitlement and an AASA file for the host) can be used with the library. In general, AppAuth can work with any authorization server that supports native apps, as documented in [RFC 8252](https://tools.ietf.org/html/rfc8252), diff --git a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m index aef03dc37..6a4d538dc 100644 --- a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m +++ b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m @@ -43,27 +43,6 @@ @interface OIDExternalUserAgentIOS () @end #endif -API_AVAILABLE(ios(17.4)) -ASWebAuthenticationSessionCallback *_Nullable - OIDHTTPSCallbackForRequest(id request) { - NSURL *redirectURL = nil; - // OIDExternalUserAgentRequest does not conform to NSObject, so message the request as id. - id requestObject = request; - if ([requestObject isKindOfClass:[OIDAuthorizationRequest class]]) { - redirectURL = ((OIDAuthorizationRequest *)requestObject).redirectURL; - } else if ([requestObject isKindOfClass:[OIDEndSessionRequest class]]) { - redirectURL = ((OIDEndSessionRequest *)requestObject).postLogoutRedirectURL; - } - if (![[redirectURL.scheme lowercaseString] isEqualToString:@"https"] || - redirectURL.host.length == 0) { - return nil; - } - // A redirect URL with no path is normalized to the root path, so that it matches the callback - // URL the authorization server redirects to. - NSString *path = redirectURL.path.length > 0 ? redirectURL.path : @"/"; - return [ASWebAuthenticationSessionCallback callbackWithHTTPSHost:redirectURL.host path:path]; -} - @implementation OIDExternalUserAgentIOS { UIViewController *_presentingViewController; BOOL _prefersEphemeralSession; @@ -108,6 +87,30 @@ - (nullable instancetype)initWithPresentingViewController: return self; } +/*! @brief Creates the callback for a request whose redirect URL is an HTTPS universal link. + @return The callback, or nil if the request is of an unsupported type, or its redirect URL is + not an HTTPS URL with a host. + */ ++ (nullable ASWebAuthenticationSessionCallback *)HTTPSCallbackForRequest: + (id)request API_AVAILABLE(ios(17.4)) { + NSURL *redirectURL = nil; + // OIDExternalUserAgentRequest does not conform to NSObject, so message the request as id. + id requestObject = request; + if ([requestObject isKindOfClass:[OIDAuthorizationRequest class]]) { + redirectURL = ((OIDAuthorizationRequest *)requestObject).redirectURL; + } else if ([requestObject isKindOfClass:[OIDEndSessionRequest class]]) { + redirectURL = ((OIDEndSessionRequest *)requestObject).postLogoutRedirectURL; + } + if (![[redirectURL.scheme lowercaseString] isEqualToString:@"https"] || + redirectURL.host.length == 0) { + return nil; + } + // A redirect URL with no path is normalized to the root path, so that it matches the callback + // URL the authorization server redirects to. + NSString *path = redirectURL.path.length > 0 ? redirectURL.path : @"/"; + return [ASWebAuthenticationSessionCallback callbackWithHTTPSHost:redirectURL.host path:path]; +} + - (BOOL)presentExternalUserAgentRequest:(id)request session:(id)session { if (_externalUserAgentFlowInProgress) { @@ -119,101 +122,60 @@ - (BOOL)presentExternalUserAgentRequest:(id)request _session = session; BOOL openedUserAgent = NO; NSURL *requestURL = [request externalUserAgentRequestURL]; - // An HTTPS redirect is a universal link, which only the iOS 17.4 callback below can handle. - // ASWebAuthenticationSession does not support @c https as a callbackURLScheme, so such a request - // must never reach the scheme based session: it would open a browser whose callback never fires. - // Before iOS 17.4 such a request therefore fails to open, ending the flow with an error rather - // than leaving it hanging. - BOOL hasHTTPSRedirect = [[request.redirectScheme lowercaseString] isEqualToString:@"https"]; - // iOS 17.4 and later: if the redirect URL is an HTTPS universal link, use - // ASWebAuthenticationSession's HTTPS callback. - if (@available(iOS 17.4, *)) { - // ASWebAuthenticationSession doesn't work with guided access (rdar://40809553) - if (hasHTTPSRedirect && !UIAccessibilityIsGuidedAccessEnabled()) { - ASWebAuthenticationSessionCallback *callback = OIDHTTPSCallbackForRequest(request); - if (!callback) { - // The redirect URL is HTTPS but a callback couldn't be created for it (e.g. it has no - // host). A session started with such a redirect could never complete, so fail instead. - [self cleanUp]; - NSError *error = - [OIDErrorUtilities errorWithCode:OIDErrorCodeSafariOpenError - underlyingError:nil - description:@"The request's HTTPS redirect URL is not a valid " - "universal link."]; - [session failExternalUserAgentFlowWithError:error]; - return NO; + // ASWebAuthenticationSession doesn't work with guided access (rdar://40809553) + if (!UIAccessibilityIsGuidedAccessEnabled()) { + __weak OIDExternalUserAgentIOS *weakSelf = self; + ASWebAuthenticationSessionCompletionHandler completionHandler = + ^(NSURL * _Nullable callbackURL, NSError * _Nullable error) { + __strong OIDExternalUserAgentIOS *strongSelf = weakSelf; + if (!strongSelf) { + return; } - __weak OIDExternalUserAgentIOS *weakSelf = self; - ASWebAuthenticationSession *authenticationVC = - [[ASWebAuthenticationSession alloc] initWithURL:requestURL - callback:callback - completionHandler:^(NSURL * _Nullable callbackURL, - NSError * _Nullable error) { - __strong OIDExternalUserAgentIOS *strongSelf = weakSelf; - if (!strongSelf) { - return; - } - strongSelf->_webAuthenticationVC = nil; - if (callbackURL) { - // The callback matches the redirect URL case insensitively and ignores its port, so it - // can fire for a URL the session itself rejects. Report that instead of leaving the - // flow with neither a response nor an error. - NSError *resumeError; - if (![strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL - error:&resumeError]) { - [strongSelf->_session failExternalUserAgentFlowWithError:resumeError]; - } - } else { - NSError *safariError = - [OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow - underlyingError:error - description:nil]; - [strongSelf->_session failExternalUserAgentFlowWithError:safariError]; + strongSelf->_webAuthenticationVC = nil; + if (callbackURL) { + // The session matches callback URLs more loosely than the flow checks the redirect URL (a + // custom scheme callback matches on the scheme alone, and an HTTPS callback ignores case + // and the port), so the flow can reject the URL. Fail the flow if it does, rather than + // leaving it with neither a response nor an error. + NSError *resumeError; + if (![strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL + error:&resumeError]) { + [strongSelf->_session failExternalUserAgentFlowWithError:resumeError]; } - }]; - authenticationVC.presentationContextProvider = self; - authenticationVC.prefersEphemeralWebBrowserSession = _prefersEphemeralSession; - _webAuthenticationVC = authenticationVC; - openedUserAgent = [authenticationVC start]; - } - } + } else { + NSError *safariError = + [OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow + underlyingError:error + description:nil]; + [strongSelf->_session failExternalUserAgentFlowWithError:safariError]; + } + }; - // iOS 12 and later, use ASWebAuthenticationSession - if (@available(iOS 12.0, *)) { - // ASWebAuthenticationSession doesn't work with guided access (rdar://40809553) - if (!hasHTTPSRedirect && !UIAccessibilityIsGuidedAccessEnabled()) { - __weak OIDExternalUserAgentIOS *weakSelf = self; - NSString *redirectScheme = request.redirectScheme; - ASWebAuthenticationSession *authenticationVC = - [[ASWebAuthenticationSession alloc] initWithURL:requestURL - callbackURLScheme:redirectScheme - completionHandler:^(NSURL * _Nullable callbackURL, - NSError * _Nullable error) { - __strong OIDExternalUserAgentIOS *strongSelf = weakSelf; - if (!strongSelf) { - return; - } - strongSelf->_webAuthenticationVC = nil; - if (callbackURL) { - [strongSelf->_session resumeExternalUserAgentFlowWithURL:callbackURL error:nil]; - } else { - NSError *safariError = - [OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow - underlyingError:error - description:nil]; - [strongSelf->_session failExternalUserAgentFlowWithError:safariError]; + ASWebAuthenticationSession *authenticationVC = nil; + NSString *redirectScheme = request.redirectScheme; + if ([[redirectScheme lowercaseString] isEqualToString:@"https"]) { + // An HTTPS redirect URL is a universal link, which needs the HTTPS callback added in iOS 17.4. + // Without one no session is started, as https is not supported as a callbackURLScheme: the + // session's callback would never fire. + if (@available(iOS 17.4, *)) { + ASWebAuthenticationSessionCallback *callback = + [[self class] HTTPSCallbackForRequest:request]; + if (callback) { + authenticationVC = [[ASWebAuthenticationSession alloc] initWithURL:requestURL + callback:callback + completionHandler:completionHandler]; } - }]; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 - if (@available(iOS 13.0, *)) { - authenticationVC.presentationContextProvider = self; - authenticationVC.prefersEphemeralWebBrowserSession = _prefersEphemeralSession; } -#endif - _webAuthenticationVC = authenticationVC; - openedUserAgent = [authenticationVC start]; + } else { + authenticationVC = [[ASWebAuthenticationSession alloc] initWithURL:requestURL + callbackURLScheme:redirectScheme + completionHandler:completionHandler]; } + authenticationVC.presentationContextProvider = self; + authenticationVC.prefersEphemeralWebBrowserSession = _prefersEphemeralSession; + _webAuthenticationVC = authenticationVC; + openedUserAgent = [authenticationVC start]; } if (!openedUserAgent) { [self cleanUp]; diff --git a/UnitTests/OIDExternalUserAgentIOSTests.m b/UnitTests/OIDExternalUserAgentIOSTests.m index 05a255e1b..c4bcb155d 100644 --- a/UnitTests/OIDExternalUserAgentIOSTests.m +++ b/UnitTests/OIDExternalUserAgentIOSTests.m @@ -1,7 +1,7 @@ /*! @file OIDExternalUserAgentIOSTests.m @brief AppAuth iOS SDK @copyright - Copyright 2025 Google Inc. All Rights Reserved. + Copyright 2026 Google Inc. All Rights Reserved. @copydetails Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ #import +#import "Sources/AppAuth/iOS/OIDExternalUserAgentIOS.h" #import "Sources/AppAuthCore/OIDAuthorizationRequest.h" #import "Sources/AppAuthCore/OIDEndSessionRequest.h" #import "Sources/AppAuthCore/OIDExternalUserAgentRequest.h" @@ -33,15 +34,11 @@ #import "Sources/AppAuthCore/OIDScopes.h" #import "Sources/AppAuthCore/OIDServiceConfiguration.h" -/*! @brief Creates the @c ASWebAuthenticationSessionCallback for a request whose redirect URL is an - HTTPS universal link, or nil if one couldn't be created; for example, the request is of an - unsupported type, or its redirect URL is not a valid HTTPS URL with a host. - @discussion Implemented in @c OIDExternalUserAgentIOS.m and declared here rather than in a - header, so that it is testable without becoming part of AppAuth's public API. - */ -extern ASWebAuthenticationSessionCallback *_Nullable - OIDHTTPSCallbackForRequest(id _Nonnull request) - API_AVAILABLE(ios(17.4)); +@interface OIDExternalUserAgentIOS (Testing) + // expose private method for simple testing ++ (nullable ASWebAuthenticationSessionCallback *)HTTPSCallbackForRequest: + (nonnull id)request API_AVAILABLE(ios(17.4)); +@end // Ignore warnings about "Use of GNU statement expression extension" which is raised by our use of // the XCTAssert___ macros. @@ -64,7 +61,7 @@ */ static NSString *const kTestIDTokenHint = @"id-token-hint"; -/*! @brief A request of a type unknown to @c OIDHTTPSCallbackForRequest. +/*! @brief A request of a type unknown to @c OIDExternalUserAgentIOS.HTTPSCallbackForRequest:. */ @interface OIDUnsupportedExternalUserAgentRequest : NSObject @end @@ -106,7 +103,7 @@ - (void)testHTTPSCallbackForAuthorizationRequest { if (@available(iOS 17.4, *)) { OIDAuthorizationRequest *request = [self authorizationRequestWithRedirectURL: [NSURL URLWithString:@"https://client.example.com/oauth2redirect"]]; - ASWebAuthenticationSessionCallback *callback = OIDHTTPSCallbackForRequest(request); + ASWebAuthenticationSessionCallback *callback = [OIDExternalUserAgentIOS HTTPSCallbackForRequest:request]; XCTAssertNotNil(callback); XCTAssertTrue([callback matchesURL: [NSURL URLWithString:@"https://client.example.com/oauth2redirect?code=1234"]]); @@ -130,7 +127,7 @@ - (void)testHTTPSCallbackForEndSessionRequest { idTokenHint:kTestIDTokenHint postLogoutRedirectURL:[NSURL URLWithString:@"https://client.example.com/signout"] additionalParameters:nil]; - ASWebAuthenticationSessionCallback *callback = OIDHTTPSCallbackForRequest(request); + ASWebAuthenticationSessionCallback *callback = [OIDExternalUserAgentIOS HTTPSCallbackForRequest:request]; XCTAssertNotNil(callback); XCTAssertTrue([callback matchesURL: [NSURL URLWithString:@"https://client.example.com/signout?state=1234"]]); @@ -145,7 +142,7 @@ - (void)testNoHTTPSCallbackForCustomSchemeRedirect { if (@available(iOS 17.4, *)) { OIDAuthorizationRequest *request = [self authorizationRequestWithRedirectURL: [NSURL URLWithString:@"com.example.app:/oauth2redirect"]]; - XCTAssertNil(OIDHTTPSCallbackForRequest(request)); + XCTAssertNil([OIDExternalUserAgentIOS HTTPSCallbackForRequest:request]); } else { XCTSkip(@"ASWebAuthenticationSessionCallback requires iOS 17.4."); } @@ -157,7 +154,7 @@ - (void)testNoHTTPSCallbackForRedirectWithoutHost { if (@available(iOS 17.4, *)) { OIDAuthorizationRequest *request = [self authorizationRequestWithRedirectURL: [NSURL URLWithString:@"https:///oauth2redirect"]]; - XCTAssertNil(OIDHTTPSCallbackForRequest(request)); + XCTAssertNil([OIDExternalUserAgentIOS HTTPSCallbackForRequest:request]); } else { XCTSkip(@"ASWebAuthenticationSessionCallback requires iOS 17.4."); } @@ -169,7 +166,7 @@ - (void)testHTTPSCallbackForRedirectWithoutPath { if (@available(iOS 17.4, *)) { OIDAuthorizationRequest *request = [self authorizationRequestWithRedirectURL: [NSURL URLWithString:@"https://client.example.com"]]; - ASWebAuthenticationSessionCallback *callback = OIDHTTPSCallbackForRequest(request); + ASWebAuthenticationSessionCallback *callback = [OIDExternalUserAgentIOS HTTPSCallbackForRequest:request]; XCTAssertNotNil(callback); XCTAssertTrue([callback matchesURL: [NSURL URLWithString:@"https://client.example.com/?code=1234"]]); @@ -186,7 +183,7 @@ - (void)testNoHTTPSCallbackForUnsupportedRequestType { if (@available(iOS 17.4, *)) { id request = [[OIDUnsupportedExternalUserAgentRequest alloc] init]; - XCTAssertNil(OIDHTTPSCallbackForRequest(request)); + XCTAssertNil([OIDExternalUserAgentIOS HTTPSCallbackForRequest:request]); } else { XCTSkip(@"ASWebAuthenticationSessionCallback requires iOS 17.4."); } From 4acb029479a88287d1f0e7451596ff0e55063394 Mon Sep 17 00:00:00 2001 From: Khaleel Shaheen Date: Thu, 24 Sep 2026 01:08:30 +0300 Subject: [PATCH 4/5] Mention webcredentials in the README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8dcab0c88..8d1e7d156 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ Authentication is performed using `ASWebAuthenticationSession`. #### Authorization Server Requirements -Both Custom URI Schemes and Universal Links (iOS 17.4+, requires the Associated Domains -entitlement and an AASA file for the host) can be used with the library. +Both Custom URI Schemes and Universal Links (iOS 17.4+, requires a `webcredentials` Associated +Domains entitlement and an AASA file for the host) can be used with the library. In general, AppAuth can work with any authorization server that supports native apps, as documented in [RFC 8252](https://tools.ietf.org/html/rfc8252), From 767b5859b9a5a677425d7eeb0e91b2fa9401e0c5 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:34:27 -0700 Subject: [PATCH 5/5] g-orchestrated: describe HTTPS redirects without universal link wording --- README.md | 8 +++++--- Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m | 4 ++-- UnitTests/OIDExternalUserAgentIOSTests.m | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8d1e7d156..ddbc86521 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,14 @@ Authentication is performed using `ASWebAuthenticationSession`. #### Authorization Server Requirements -Both Custom URI Schemes and Universal Links (iOS 17.4+, requires a `webcredentials` Associated -Domains entitlement and an AASA file for the host) can be used with the library. +Both custom URI scheme redirects and HTTPS redirects (RFC 8252 "claimed https" redirect URIs) can +be used with the library. HTTPS redirects require iOS 17.4+, an Associated Domains entitlement +for the redirect host using the `webcredentials` service, and an AASA file on that host listing +the app under `webcredentials`. An `applinks` (Universal Links) association alone is not enough. In general, AppAuth can work with any authorization server that supports native apps, as documented in [RFC 8252](https://tools.ietf.org/html/rfc8252), -either through custom URI scheme redirects, or universal links. +either through custom URI scheme redirects, or HTTPS redirects. Authorization servers that assume all clients are web-based, or require clients to maintain confidentiality of the client secrets may not work well. diff --git a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m index 6a4d538dc..b6e29bd2a 100644 --- a/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m +++ b/Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m @@ -87,7 +87,7 @@ - (nullable instancetype)initWithPresentingViewController: return self; } -/*! @brief Creates the callback for a request whose redirect URL is an HTTPS universal link. +/*! @brief Creates the callback for a request whose redirect URL is an HTTPS URL. @return The callback, or nil if the request is of an unsupported type, or its redirect URL is not an HTTPS URL with a host. */ @@ -155,7 +155,7 @@ - (BOOL)presentExternalUserAgentRequest:(id)request ASWebAuthenticationSession *authenticationVC = nil; NSString *redirectScheme = request.redirectScheme; if ([[redirectScheme lowercaseString] isEqualToString:@"https"]) { - // An HTTPS redirect URL is a universal link, which needs the HTTPS callback added in iOS 17.4. + // An HTTPS redirect URL needs the HTTPS callback added in iOS 17.4. // Without one no session is started, as https is not supported as a callbackURLScheme: the // session's callback would never fire. if (@available(iOS 17.4, *)) { diff --git a/UnitTests/OIDExternalUserAgentIOSTests.m b/UnitTests/OIDExternalUserAgentIOSTests.m index c4bcb155d..7cc7fd8c6 100644 --- a/UnitTests/OIDExternalUserAgentIOSTests.m +++ b/UnitTests/OIDExternalUserAgentIOSTests.m @@ -78,7 +78,7 @@ - (NSString *)redirectScheme { @end -/*! @brief Unit tests for the iOS external user agent's HTTPS (universal link) callback support. +/*! @brief Unit tests for the iOS external user agent's HTTPS redirect callback support. */ @interface OIDExternalUserAgentIOSTests : XCTestCase @end @@ -136,7 +136,7 @@ - (void)testHTTPSCallbackForEndSessionRequest { } } -/*! @brief A custom scheme redirect is not a universal link, so no callback is created. +/*! @brief A custom scheme redirect is not an HTTPS redirect, so no callback is created. */ - (void)testNoHTTPSCallbackForCustomSchemeRedirect { if (@available(iOS 17.4, *)) {