Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add foundation for presenting authorization flow without a window or view controller #743

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 16 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,33 @@ let package = Package(
path: "Source/AppAuthTV",
publicHeadersPath: ""
),
.target(
name: "TestHelpers",
dependencies: ["AppAuthCore"],
path: "UnitTests/Helpers",
publicHeadersPath: ""
),
.testTarget(
name: "AppAuthCoreTests",
dependencies: ["AppAuthCore"],
path: "UnitTests",
exclude: ["OIDSwiftTests.swift", "AppAuthTV"]
dependencies: ["AppAuthCore", "TestHelpers"],
path: "UnitTests/AppAuthCore",
exclude: ["OIDSwiftTests.swift"]
),
.testTarget(
name: "AppAuthTests",
dependencies: ["AppAuth", "TestHelpers"],
path: "UnitTests/AppAuth"
),
.testTarget(
name: "AppAuthCoreSwiftTests",
dependencies: ["AppAuthCore"],
path: "UnitTests",
path: "UnitTests/AppAuthCore",
sources: ["OIDSwiftTests.swift"]
),
.testTarget(
name: "AppAuthTVTests",
dependencies: ["AppAuthTV"],
path: "UnitTests/AppAuthTV"
),
)
]
)
26 changes: 26 additions & 0 deletions Source/AppAuth/iOS/OIDAuthorizationService+IOS.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,32 @@ NS_ASSUME_NONNULL_BEGIN
prefersEphemeralSession:(BOOL)prefersEphemeralSession
callback:(OIDAuthorizationCallback)callback API_AVAILABLE(ios(13));

/*! @brief Perform an authorization flow using the @c ASWebAuthenticationSession optionally using an
emphemeral browser session that shares no cookies or data with the normal browser session.
@param request The authorization request.
@param callback The method called when the request has completed or failed.
@return A @c OIDExternalUserAgentSession instance which will terminate when it
receives a @c OIDExternalUserAgentSession.cancel message, or after processing a
@c OIDExternalUserAgentSession.resumeExternalUserAgentFlowWithURL: message.
*/
+ (id<OIDExternalUserAgentSession>)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
callback:(OIDAuthorizationCallback)callback;

/*! @brief Perform an authorization flow using the @c ASWebAuthenticationSession optionally using an
emphemeral browser session that shares no cookies or data with the normal browser session.
@param request The authorization request.
@param prefersEphemeralSession Whether the caller prefers to use a private authentication
session. See @c ASWebAuthenticationSession.prefersEphemeralWebBrowserSession for more.
@param callback The method called when the request has completed or failed.
@return A @c OIDExternalUserAgentSession instance which will terminate when it
receives a @c OIDExternalUserAgentSession.cancel message, or after processing a
@c OIDExternalUserAgentSession.resumeExternalUserAgentFlowWithURL: message.
*/
+ (id<OIDExternalUserAgentSession>)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
prefersEphemeralSession:(BOOL)prefersEphemeralSession
callback:(OIDAuthorizationCallback)callback
API_AVAILABLE(ios(13));

@end

NS_ASSUME_NONNULL_END
Expand Down
40 changes: 40 additions & 0 deletions Source/AppAuth/iOS/OIDAuthorizationService+IOS.m
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,46 @@ @implementation OIDAuthorizationService (IOS)
return [self presentAuthorizationRequest:request externalUserAgent:externalUserAgent callback:callback];
}

+ (id<OIDExternalUserAgentSession>)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
callback:(OIDAuthorizationCallback)callback {
return [self presentAuthorizationRequest:request
prefersEphemeralSessionIfAvailable:NO
callback:callback];
}

+ (id<OIDExternalUserAgentSession>)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
prefersEphemeralSession:(BOOL)prefersEphemeralSession
callback:(OIDAuthorizationCallback)callback {
return [self presentAuthorizationRequest:request
prefersEphemeralSessionIfAvailable:prefersEphemeralSession
callback:callback];
}

+ (id<OIDExternalUserAgentSession>)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
prefersEphemeralSessionIfAvailable:(BOOL)prefersEphemeralSession
callback:(OIDAuthorizationCallback)callback {
id<OIDExternalUserAgent> externalUserAgent;

if (@available(iOS 13, *)) {
#if TARGET_OS_MACCATALYST
externalUserAgent = [[OIDExternalUserAgentCatalyst alloc]
initWithPrefersEphemeralSession:prefersEphemeralSession];
#else // TARGET_OS_MACCATALYST
externalUserAgent = [[OIDExternalUserAgentIOS alloc]
initWithPrefersEphemeralSession:prefersEphemeralSession];
#endif // TARGET_OS_MACCATALYST
} else {
#if TARGET_OS_MACCATALYST
externalUserAgent = [[OIDExternalUserAgentCatalyst alloc] init];
#else // TARGET_OS_MACCATALYST
externalUserAgent = [[OIDExternalUserAgentIOS alloc] init];
#endif // TARGET_OS_MACCATALYST
}
return [self presentAuthorizationRequest:request
externalUserAgent:externalUserAgent
callback:callback];
}

@end

NS_ASSUME_NONNULL_END
Expand Down
21 changes: 15 additions & 6 deletions Source/AppAuth/iOS/OIDExternalUserAgentCatalyst.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,27 @@ NS_ASSUME_NONNULL_BEGIN
API_AVAILABLE(macCatalyst(13)) API_UNAVAILABLE(ios)
@interface OIDExternalUserAgentCatalyst : NSObject<OIDExternalUserAgent>

/*! @internal
@brief Unavailable. Please use @c initWithPresentingViewController:
/*! @brief Create an external user-agent.
@discussion The specific authentication UI used depends on the iOS version and accessibility
options. iOS 8 uses the system browser, iOS 9-10 use @c SFSafariViewController, iOS 11 uses
@c SFAuthenticationSession (unless Guided Access is on which does not work) or uses
@c SFSafariViewController, and iOS 12+ uses @c ASWebAuthenticationSession (unless Guided
Access is on).
*/
- (nonnull instancetype)init NS_UNAVAILABLE;
- (nonnull instancetype)init;

/*! @brief The designated initializer.
/*! @brief Create an external user-agent which optionally uses a private authentication session.
@param prefersEphemeralSession Whether the caller prefers to use a private authentication
session. See @c ASWebAuthenticationSession.prefersEphemeralWebBrowserSession for more.
*/
- (nullable instancetype)initWithPrefersEphemeralSession:(BOOL)prefersEphemeralSession;

/*! @brief Create an external user-agent with a presenting view controller.
@param presentingViewController The view controller from which to present the
\SFSafariViewController.
*/
- (nullable instancetype)initWithPresentingViewController:
(UIViewController *)presentingViewController
NS_DESIGNATED_INITIALIZER;
(UIViewController *)presentingViewController;

/*! @brief Create an external user-agent which optionally uses a private authentication session.
@param presentingViewController The view controller from which to present the browser.
Expand Down
47 changes: 46 additions & 1 deletion Source/AppAuth/iOS/OIDExternalUserAgentCatalyst.m
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,22 @@ @implementation OIDExternalUserAgentCatalyst {
ASWebAuthenticationSession *_webAuthenticationVC;
}

- (nonnull instancetype)init {
self = [super init];
return self;
}

- (nullable instancetype)initWithPrefersEphemeralSession:(BOOL)prefersEphemeralSession {
self = [self init];
if (self) {
_prefersEphemeralSession = prefersEphemeralSession;
}
return self;
}

- (nullable instancetype)initWithPresentingViewController:
(UIViewController *)presentingViewController {
self = [super init];
self = [self init];
if (self) {
_presentingViewController = presentingViewController;
}
Expand All @@ -71,6 +84,38 @@ - (BOOL)presentExternalUserAgentRequest:(id<OIDExternalUserAgentRequest>)request
return NO;
}

if (!_presentingViewController) {
// Find presentingViewController
if (@available(iOS 13.0, *)) {
NSSet<UIWindowScene *> *scenes =
(NSSet<UIWindowScene *> *)[UIApplication sharedApplication].connectedScenes;

NSMutableArray<UIWindow *> *windows = [NSMutableArray array];

for (UIWindowScene *scene in scenes) {
[windows addObjectsFromArray:scene.windows];
}

for (UIWindow *window in windows) {
if (window.isKeyWindow) { // False if calling before window appears
UIWindow *keyWindow = window;
_presentingViewController = keyWindow.rootViewController;
break;
}
}
} else {
// ≤ iOS 12.X
NSArray<UIWindow *> *windows = UIApplication.sharedApplication.windows;
NSPredicate *keyWindowPredicate = [NSPredicate predicateWithFormat:@"keyWindow == YES"];
UIWindow *keyWindow = [windows filteredArrayUsingPredicate:keyWindowPredicate].firstObject;
_presentingViewController = keyWindow.rootViewController;
}
if (!_presentingViewController) {
// Unable to find a presentingViewController; perhaps because no window is key and visible
return NO;
}
}

_externalUserAgentFlowInProgress = YES;
_session = session;
BOOL openedUserAgent = NO;
Expand Down
31 changes: 22 additions & 9 deletions Source/AppAuth/iOS/OIDExternalUserAgentIOS.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,25 @@ NS_ASSUME_NONNULL_BEGIN
API_UNAVAILABLE(macCatalyst)
@interface OIDExternalUserAgentIOS : NSObject<OIDExternalUserAgent>

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

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

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

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

@end

NS_ASSUME_NONNULL_END
Expand Down
53 changes: 43 additions & 10 deletions Source/AppAuth/iOS/OIDExternalUserAgentIOS.m
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,22 @@ @implementation OIDExternalUserAgentIOS {
}

- (nullable instancetype)init {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnonnull"
return [self initWithPresentingViewController:nil];
#pragma clang diagnostic pop
self = [super init];
return self;
}

- (nullable instancetype)initWithPrefersEphemeralSession:(BOOL)prefersEphemeralSession {
self = [self init];
if (self) {
_prefersEphemeralSession = prefersEphemeralSession;
}
return self;
}

- (nullable instancetype)initWithPresentingViewController:
(UIViewController *)presentingViewController {
self = [super init];
self = [self init];
if (self) {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
NSAssert(presentingViewController != nil,
@"presentingViewController cannot be nil on iOS 13");
#endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000

_presentingViewController = presentingViewController;
}
return self;
Expand All @@ -93,6 +94,38 @@ - (BOOL)presentExternalUserAgentRequest:(id<OIDExternalUserAgentRequest>)request
return NO;
}

if (!_presentingViewController) {
// Find presentingViewController
if (@available(iOS 13.0, *)) {
NSSet<UIWindowScene *> *scenes =
(NSSet<UIWindowScene *> *)[UIApplication sharedApplication].connectedScenes;

NSMutableArray<UIWindow *> *windows = [NSMutableArray array];

for (UIWindowScene *scene in scenes) {
[windows addObjectsFromArray:scene.windows];
}

for (UIWindow *window in windows) {
if (window.isKeyWindow) { // False if calling before window appears
UIWindow *keyWindow = window;
_presentingViewController = keyWindow.rootViewController;
break;
}
}
} else {
// ≤ iOS 12.X
NSArray<UIWindow *> *windows = UIApplication.sharedApplication.windows;
NSPredicate *keyWindowPredicate = [NSPredicate predicateWithFormat:@"keyWindow == YES"];
UIWindow *keyWindow = [windows filteredArrayUsingPredicate:keyWindowPredicate].firstObject;
_presentingViewController = keyWindow.rootViewController;
}
if (!_presentingViewController) {
// Unable to find a presentingViewController; perhaps because no window is key and visible
return NO;
}
}

_externalUserAgentFlowInProgress = YES;
_session = session;
BOOL openedUserAgent = NO;
Expand Down
60 changes: 60 additions & 0 deletions UnitTests/AppAuth/OIDExternUserAgentTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*! @file OIDExternalUserAgentTests.m
@brief AppAuth iOS SDK
@copyright
Copyright 2023 The AppAuth Authors. 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 <TargetConditionals.h>

#import <XCTest/XCTest.h>

#if SWIFT_PACKAGE
@import AppAuth;
@import TestHelpers;
#else
#import "Source/AppAuth/iOS/OIDExternalUserAgentCatalyst.h"
#import "Source/AppAuth/iOS/OIDExternalUserAgentIOS.h"
#import "Source/AppAuthCore/OIDError.h"
#import "UnitTests/TestHelpers/OIDAuthorizationRequest+TestHelper.h"
#endif

@interface OIDExternalUserAgentTests : XCTestCase

@end

@implementation OIDExternalUserAgentTests

- (void)testThatPresentExternalUserAgentRequestReturnsNoWhenMissingPresentingViewController {
id<OIDExternalUserAgent> userAgent;

#if TARGET_OS_MACCATALYST
userAgent = [[OIDExternalUserAgentCatalyst alloc] init];
#elif TARGET_OS_IOS
userAgent = [[OIDExternalUserAgentIOS alloc] init];
#endif

OIDAuthorizationRequest *authRequest = [OIDAuthorizationRequest testInstance];
[OIDAuthorizationService
presentAuthorizationRequest:authRequest
externalUserAgent:userAgent
callback:^(OIDAuthorizationResponse * _Nullable authorizationResponse,
NSError * _Nullable error) {
XCTAssertNotNil(error);
XCTAssertEqual(error.code, OIDErrorCodeSafariOpenError);
XCTAssertEqualObjects(error.localizedDescription, @"Unable to open Safari.");
}];
}

@end
Loading