Skip to content

Commit bc1fd3b

Browse files
committed
Add methods to not pass a presenting vc or window for auth flow
1 parent b46216a commit bc1fd3b

39 files changed

+259
-21
lines changed

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,18 @@ let package = Package(
6767
.testTarget(
6868
name: "AppAuthCoreTests",
6969
dependencies: ["AppAuthCore"],
70-
path: "UnitTests",
70+
path: "UnitTests/AppAuthCore",
7171
exclude: ["OIDSwiftTests.swift", "AppAuthTV"]
7272
),
73+
.testTarget(
74+
name: "AppAuthTests",
75+
dependencies: ["AppAuth", "AppAuthCore"],
76+
path: "UnitTests/AppAuth"
77+
),
7378
.testTarget(
7479
name: "AppAuthCoreSwiftTests",
7580
dependencies: ["AppAuthCore"],
76-
path: "UnitTests",
81+
path: "UnitTests/AppAuthCore",
7782
sources: ["OIDSwiftTests.swift"]
7883
),
7984
.testTarget(

Source/AppAuth/iOS/OIDAuthorizationService+IOS.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,32 @@ NS_ASSUME_NONNULL_BEGIN
6060
prefersEphemeralSession:(BOOL)prefersEphemeralSession
6161
callback:(OIDAuthorizationCallback)callback API_AVAILABLE(ios(13));
6262

63+
/*! @brief Perform an authorization flow using the @c ASWebAuthenticationSession optionally using an
64+
emphemeral browser session that shares no cookies or data with the normal browser session.
65+
@param request The authorization request.
66+
@param callback The method called when the request has completed or failed.
67+
@return A @c OIDExternalUserAgentSession instance which will terminate when it
68+
receives a @c OIDExternalUserAgentSession.cancel message, or after processing a
69+
@c OIDExternalUserAgentSession.resumeExternalUserAgentFlowWithURL: message.
70+
*/
71+
+ (id<OIDExternalUserAgentSession>)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
72+
callback:(OIDAuthorizationCallback)callback;
73+
74+
/*! @brief Perform an authorization flow using the @c ASWebAuthenticationSession optionally using an
75+
emphemeral browser session that shares no cookies or data with the normal browser session.
76+
@param request The authorization request.
77+
@param prefersEphemeralSession Whether the caller prefers to use a private authentication
78+
session. See @c ASWebAuthenticationSession.prefersEphemeralWebBrowserSession for more.
79+
@param callback The method called when the request has completed or failed.
80+
@return A @c OIDExternalUserAgentSession instance which will terminate when it
81+
receives a @c OIDExternalUserAgentSession.cancel message, or after processing a
82+
@c OIDExternalUserAgentSession.resumeExternalUserAgentFlowWithURL: message.
83+
*/
84+
+ (id<OIDExternalUserAgentSession>)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
85+
prefersEphemeralSession:(BOOL)prefersEphemeralSession
86+
callback:(OIDAuthorizationCallback)callback
87+
API_AVAILABLE(ios(13));
88+
6389
@end
6490

6591
NS_ASSUME_NONNULL_END

Source/AppAuth/iOS/OIDAuthorizationService+IOS.m

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@ @implementation OIDAuthorizationService (IOS)
5757
return [self presentAuthorizationRequest:request externalUserAgent:externalUserAgent callback:callback];
5858
}
5959

60+
+ (id<OIDExternalUserAgentSession>)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
61+
callback:(OIDAuthorizationCallback)callback {
62+
return [self presentAuthorizationRequest:request
63+
prefersEphemeralSession:NO
64+
callback:callback];
65+
}
66+
67+
+ (id<OIDExternalUserAgentSession>)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
68+
prefersEphemeralSession:(BOOL)prefersEphemeralSession
69+
callback:(OIDAuthorizationCallback)callback {
70+
id<OIDExternalUserAgent> externalUserAgent;
71+
#if TARGET_OS_MACCATALYST
72+
externalUserAgent = [[OIDExternalUserAgentCatalyst alloc]
73+
initWithPrefersEphemeralSession:prefersEphemeralSession];
74+
#else // TARGET_OS_MACCATALYST
75+
externalUserAgent = [[OIDExternalUserAgentIOS alloc]
76+
initWithPrefersEphemeralSession:prefersEphemeralSession];
77+
#endif // TARGET_OS_MACCATALYST
78+
return [self presentAuthorizationRequest:request
79+
externalUserAgent:externalUserAgent
80+
callback:callback];
81+
}
82+
6083
@end
6184

6285
NS_ASSUME_NONNULL_END

Source/AppAuth/iOS/OIDExternalUserAgentIOS.h

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,25 @@ NS_ASSUME_NONNULL_BEGIN
3434
API_UNAVAILABLE(macCatalyst)
3535
@interface OIDExternalUserAgentIOS : NSObject<OIDExternalUserAgent>
3636

37-
- (nullable instancetype)init API_AVAILABLE(ios(11))
38-
__deprecated_msg("This method will not work on iOS 13, use "
39-
"initWithPresentingViewController:presentingViewController");
37+
/*! @brief Create an external user-agent.
38+
@discussion The specific authentication UI used depends on the iOS version and accessibility
39+
options. iOS 8 uses the system browser, iOS 9-10 use @c SFSafariViewController, iOS 11 uses
40+
@c SFAuthenticationSession (unless Guided Access is on which does not work) or uses
41+
@c SFSafariViewController, and iOS 12+ uses @c ASWebAuthenticationSession (unless Guided
42+
Access is on).
43+
*/
44+
- (nullable instancetype)init API_AVAILABLE(ios(11));
4045

41-
/*! @brief The designated initializer.
46+
/*! @brief Create an external user-agent with the presenting view controller.
4247
@param presentingViewController The view controller from which to present the authentication UI.
4348
@discussion The specific authentication UI used depends on the iOS version and accessibility
4449
options. iOS 8 uses the system browser, iOS 9-10 use @c SFSafariViewController, iOS 11 uses
45-
@c SFAuthenticationSession
46-
(unless Guided Access is on which does not work) or uses @c SFSafariViewController, and iOS
47-
12+ uses @c ASWebAuthenticationSession (unless Guided Access is on).
50+
@c SFAuthenticationSession (unless Guided Access is on which does not work) or uses
51+
@c SFSafariViewController, and iOS 12+ uses @c ASWebAuthenticationSession (unless Guided
52+
Access is on).
4853
*/
4954
- (nullable instancetype)initWithPresentingViewController:
50-
(UIViewController *)presentingViewController
51-
NS_DESIGNATED_INITIALIZER;
55+
(UIViewController *)presentingViewController;
5256

5357
/*! @brief Create an external user-agent which optionally uses a private authentication session.
5458
@param presentingViewController The view controller from which to present the browser.
@@ -62,6 +66,15 @@ API_UNAVAILABLE(macCatalyst)
6266
prefersEphemeralSession:(BOOL)prefersEphemeralSession
6367
API_AVAILABLE(ios(13));
6468

69+
/*! @brief Create an external user-agent which optionally uses a private authentication session.
70+
@param prefersEphemeralSession Whether the caller prefers to use a private authentication
71+
session. See @c ASWebAuthenticationSession.prefersEphemeralWebBrowserSession for more.
72+
@discussion Authentication is performed with @c ASWebAuthenticationSession (unless Guided Access
73+
is on), setting the ephemerality based on the argument.
74+
*/
75+
- (nullable instancetype)initWithPrefersEphemeralSession:(BOOL)prefersEphemeralSession
76+
API_AVAILABLE(ios(13));
77+
6578
@end
6679

6780
NS_ASSUME_NONNULL_END

Source/AppAuth/iOS/OIDExternalUserAgentIOS.m

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,22 @@ @implementation OIDExternalUserAgentIOS {
5656
}
5757

5858
- (nullable instancetype)init {
59-
#pragma clang diagnostic push
60-
#pragma clang diagnostic ignored "-Wnonnull"
61-
return [self initWithPresentingViewController:nil];
62-
#pragma clang diagnostic pop
59+
self = [super init];
60+
return self;
61+
}
62+
63+
- (nullable instancetype)initWithPrefersEphemeralSession:(BOOL)prefersEphemeralSession {
64+
self = [self init];
65+
if (self) {
66+
_prefersEphemeralSession = prefersEphemeralSession;
67+
}
68+
return self;
6369
}
6470

6571
- (nullable instancetype)initWithPresentingViewController:
6672
(UIViewController *)presentingViewController {
67-
self = [super init];
73+
self = [self init];
6874
if (self) {
69-
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
70-
NSAssert(presentingViewController != nil,
71-
@"presentingViewController cannot be nil on iOS 13");
72-
#endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
73-
7475
_presentingViewController = presentingViewController;
7576
}
7677
return self;
@@ -93,6 +94,38 @@ - (BOOL)presentExternalUserAgentRequest:(id<OIDExternalUserAgentRequest>)request
9394
return NO;
9495
}
9596

97+
if (!_presentingViewController) {
98+
// Find presentingViewController
99+
if (@available(iOS 13.0, *)) {
100+
NSSet<UIWindowScene *> *scenes =
101+
(NSSet<UIWindowScene *> *)[UIApplication sharedApplication].connectedScenes;
102+
103+
NSMutableArray<UIWindow *> *windows = [NSMutableArray array];
104+
105+
for (UIWindowScene *scene in scenes) {
106+
[windows addObjectsFromArray:scene.windows];
107+
}
108+
109+
for (UIWindow *window in windows) {
110+
if (window.isKeyWindow) { // False if calling before window appears
111+
UIWindow *keyWindow = window;
112+
_presentingViewController = keyWindow.rootViewController;
113+
break;
114+
}
115+
}
116+
} else {
117+
// ≤ iOS 12.X
118+
NSArray<UIWindow *> *windows = UIApplication.sharedApplication.windows;
119+
NSPredicate *keyWindowPredicate = [NSPredicate predicateWithFormat:@"keyWindow == YES"];
120+
UIWindow *keyWindow = [windows filteredArrayUsingPredicate:keyWindowPredicate].firstObject;
121+
_presentingViewController = keyWindow.rootViewController;
122+
}
123+
if (!_presentingViewController) {
124+
// Unable to find a presentingViewController; perhaps because no window is key and visible
125+
return NO;
126+
}
127+
}
128+
96129
_externalUserAgentFlowInProgress = YES;
97130
_session = session;
98131
BOOL openedUserAgent = NO;
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//
2+
// OIDExternalUserAgentIOSTests.m
3+
//
4+
//
5+
// Created by Matt Mathias on 1/11/23.
6+
//
7+
8+
#import <XCTest/XCTest.h>
9+
10+
#if SWIFT_PACKAGE
11+
@import AppAuth;
12+
@import AppAuthCore;
13+
#else
14+
#import "Source/AppAuth/iOS/OIDExternalUserAgentIOS.h"
15+
#import "Source/AppAuthCore/OIDAuthorizationRequest.h"
16+
#import "Source/AppAuthCore/OIDAuthorizationService.h"
17+
#endif
18+
19+
/*! @brief Test value for the @c clientID property.
20+
*/
21+
static NSString *const kTestClientID = @"ClientID";
22+
23+
/*! @brief Test value for the @c clientID property.
24+
*/
25+
static NSString *const kTestClientSecret = @"ClientSecret";
26+
27+
/*! @brief Test key for the @c additionalParameters property.
28+
*/
29+
static NSString *const kTestAdditionalParameterKey = @"A";
30+
31+
/*! @brief Test value for the @c additionalParameters property.
32+
*/
33+
static NSString *const kTestAdditionalParameterValue = @"1";
34+
35+
/*! @brief Test value for the @c scope property.
36+
*/
37+
static NSString *const kTestScope = @"Scope";
38+
39+
/*! @brief Test value for the @c scope property.
40+
*/
41+
static NSString *const kTestScopeA = @"ScopeA";
42+
43+
/*! @brief Test value for the @c redirectURL property.
44+
*/
45+
static NSString *const kTestRedirectURL = @"http://www.google.com/";
46+
47+
/*! @brief Test value for the @c state property.
48+
*/
49+
static NSString *const kTestState = @"State";
50+
51+
/*! @brief Test value for the @c responseType property.
52+
*/
53+
static NSString *const kTestResponseType = @"code";
54+
55+
/*! @brief Test value for the @c nonce property.
56+
*/
57+
static NSString *const kTestNonce = @"Nonce";
58+
59+
/*! @brief Test value for the @c codeVerifier property.
60+
*/
61+
static NSString *const kTestCodeVerifier = @"code verifier";
62+
63+
/*! @brief Test value for the @c authorizationEndpoint property.
64+
*/
65+
static NSString *const kInitializerTestAuthEndpoint = @"https://www.example.com/auth";
66+
67+
/*! @brief Test value for the @c tokenEndpoint property.
68+
*/
69+
static NSString *const kInitializerTestTokenEndpoint = @"https://www.example.com/token";
70+
71+
/*! @brief Test value for the @c tokenEndpoint property.
72+
*/
73+
static NSString *const kInitializerTestRegistrationEndpoint =
74+
@"https://www.example.com/registration";
75+
76+
@interface OIDExternalUserAgentIOSTests : XCTestCase
77+
78+
@end
79+
80+
@implementation OIDExternalUserAgentIOSTests
81+
82+
- (void)testThatPresentExternalUserAgentRequestReturnsNoWhenMissingPresentingViewController {
83+
OIDExternalUserAgentIOS *userAgent = [[OIDExternalUserAgentIOS alloc] init];
84+
OIDAuthorizationRequest *authRequest = [[self class] authorizationRequestTestInstance];
85+
[OIDAuthorizationService presentAuthorizationRequest:authRequest externalUserAgent:userAgent callback:^(OIDAuthorizationResponse * _Nullable authorizationResponse, NSError * _Nullable error) {
86+
XCTAssertNotNil(error);
87+
XCTAssertEqual(error.code, OIDErrorCodeSafariOpenError);
88+
XCTAssertEqualObjects(error.localizedDescription, @"Unable to open Safari.");
89+
}];
90+
}
91+
92+
+ (OIDAuthorizationRequest *)authorizationRequestTestInstance {
93+
NSDictionary *additionalParameters =
94+
@{ kTestAdditionalParameterKey : kTestAdditionalParameterValue };
95+
OIDServiceConfiguration *configuration = [[self class] serviceConfigurationTestInstance];
96+
OIDAuthorizationRequest *request =
97+
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
98+
clientId:kTestClientID
99+
clientSecret:kTestClientSecret
100+
scope:[OIDScopeUtilities scopesWithArray:@[ kTestScope, kTestScopeA ]]
101+
redirectURL:[NSURL URLWithString:kTestRedirectURL]
102+
responseType:kTestResponseType
103+
state:kTestState
104+
nonce:kTestNonce
105+
codeVerifier:kTestCodeVerifier
106+
codeChallenge:[[self class] codeChallenge]
107+
codeChallengeMethod:[[self class] codeChallengeMethod]
108+
additionalParameters:additionalParameters];
109+
return request;
110+
}
111+
112+
+ (OIDServiceConfiguration *)serviceConfigurationTestInstance {
113+
NSURL *authEndpoint = [NSURL URLWithString:kInitializerTestAuthEndpoint];
114+
NSURL *tokenEndpoint = [NSURL URLWithString:kInitializerTestTokenEndpoint];
115+
NSURL *registrationEndpoint = [NSURL URLWithString:kInitializerTestRegistrationEndpoint];
116+
OIDServiceConfiguration *configuration =
117+
[[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authEndpoint
118+
tokenEndpoint:tokenEndpoint
119+
registrationEndpoint:registrationEndpoint];
120+
return configuration;
121+
}
122+
123+
+ (NSString *)codeChallenge {
124+
return [OIDAuthorizationRequest codeChallengeS256ForVerifier:kTestCodeVerifier];
125+
}
126+
127+
+ (NSString *)codeChallengeMethod {
128+
return OIDOAuthorizationRequestCodeChallengeMethodS256;
129+
}
130+
131+
@end
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)