diff --git a/GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h b/GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h new file mode 100644 index 00000000..0fa96065 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Google LLC + * + * 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 + +@class GIDSignInInternalOptions; +@class OIDAuthorizationResponse; + +NS_ASSUME_NONNULL_BEGIN + +/// The protocol to control the authorization flow. +@protocol GIDAuthorizationFlowProcessor + +/// The state of the authorization flow. +@property(nonatomic, readonly, getter=isStarted) BOOL start; + +/// Starts the authorization flow. +/// +/// This method sends authorization request to AppAuth `OIDAuthorizationService` and gets back the +/// response or an error. +/// +/// @param options The `GIDSignInInternalOptions` object to provide serverClientID, hostedDomain, +/// clientID, scopes, loginHint and extraParams. +/// @param emmSupport The EMM support info string. +/// @param completion The block that is called on completion asynchronously. +/// authorizationResponse The response from `OIDAuthorizationService`. +/// error The error from `OIDAuthorizationService`. +- (void)startWithOptions:(GIDSignInInternalOptions *)options + emmSupport:(nullable NSString *)emmSupport + completion:(void (^)(OIDAuthorizationResponse *_Nullable authorizationResponse, + NSError *_Nullable error))completion; + +/// Handles the custom URL scheme opened by SFSafariViewController and returns control to the +/// client on iOS 10. +/// +/// @param url The redirect URL invoked by the server. +/// @return YES if the passed URL matches the expected redirect URL and was consumed, NO otherwise. +- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)url; + +/// Cancels the authorization flow. +- (void)cancelAuthenticationFlow; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h b/GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h new file mode 100644 index 00000000..e8703314 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Google LLC + * + * 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 + +#import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h" + +@class OIDServiceConfiguration; + +NS_ASSUME_NONNULL_BEGIN + +/// Concrete implementation of the protocol `GIDAuthorizationFlowProcessor`. +@interface GIDAuthorizationFlowProcessor : NSObject + +@end + +NS_ASSUME_NONNULL_END + diff --git a/GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.m b/GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.m new file mode 100644 index 00000000..b8b62d2d --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.m @@ -0,0 +1,134 @@ +/* + * Copyright 2023 Google LLC + * + * 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 "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h" + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" + +#import "GoogleSignIn/Sources/GIDEMMSupport.h" +#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" +#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" +#import "GoogleSignIn/Sources/GIDSignInPreferences.h" + +#ifdef SWIFT_PACKAGE +@import AppAuth; +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +// Parameters for the auth and token exchange endpoints. +static NSString *const kAudienceParameter = @"audience"; + +static NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes"; +static NSString *const kLoginHintParameter = @"login_hint"; +static NSString *const kHostedDomainParameter = @"hd"; + +@interface GIDAuthorizationFlowProcessor () + +/// AppAuth external user-agent session state. +@property(nonatomic, nullable)id currentAuthorizationFlow; + +/// AppAuth configuration object. +@property(nonatomic)OIDServiceConfiguration *appAuthConfiguration; + +@end + +@implementation GIDAuthorizationFlowProcessor + +# pragma mark - Public API + +- (BOOL)isStarted { + return self.currentAuthorizationFlow != nil; +} + +- (void)startWithOptions:(GIDSignInInternalOptions *)options + emmSupport:(nullable NSString *)emmSupport + completion:(void (^)(OIDAuthorizationResponse *_Nullable authorizationResponse, + NSError *_Nullable error))completion { + GIDSignInCallbackSchemes *schemes = + [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID]; + NSString *urlString = [NSString stringWithFormat:@"%@:%@", + [schemes clientIdentifierScheme], kBrowserCallbackPath]; + NSURL *redirectURL = [NSURL URLWithString:urlString]; + + NSMutableDictionary *additionalParameters = [@{} mutableCopy]; + additionalParameters[kIncludeGrantedScopesParameter] = @"true"; + if (options.configuration.serverClientID) { + additionalParameters[kAudienceParameter] = options.configuration.serverClientID; + } + if (options.loginHint) { + additionalParameters[kLoginHintParameter] = options.loginHint; + } + if (options.configuration.hostedDomain) { + additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain; + } + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + [additionalParameters addEntriesFromDictionary: + [GIDEMMSupport parametersWithParameters:options.extraParams + emmSupport:emmSupport + isPasscodeInfoRequired:NO]]; +#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST + [additionalParameters addEntriesFromDictionary:options.extraParams]; +#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST + additionalParameters[kSDKVersionLoggingParameter] = GIDVersion(); + additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment(); + + NSURL *authorizationEndpointURL = [GIDSignInPreferences authorizationEndpointURL]; + NSURL *tokenEndpointURL = [GIDSignInPreferences tokenEndpointURL]; + OIDServiceConfiguration *appAuthConfiguration = + [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpointURL + tokenEndpoint:tokenEndpointURL]; + OIDAuthorizationRequest *request = + [[OIDAuthorizationRequest alloc] initWithConfiguration:appAuthConfiguration + clientId:options.configuration.clientID + scopes:options.scopes + redirectURL:redirectURL + responseType:OIDResponseTypeCode + additionalParameters:additionalParameters]; + + _currentAuthorizationFlow = [OIDAuthorizationService + presentAuthorizationRequest:request +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + presentingViewController:options.presentingViewController +#elif TARGET_OS_OSX + presentingWindow:options.presentingWindow +#endif // TARGET_OS_OSX + callback:^(OIDAuthorizationResponse *authorizationResponse, + NSError *error) { + completion(authorizationResponse, error); + }]; +} + +- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)url { + if ([self.currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { + self.currentAuthorizationFlow = nil; + return YES; + } else { + return NO; + } +} + +- (void)cancelAuthenticationFlow { + [self.currentAuthorizationFlow cancel]; + self.currentAuthorizationFlow = nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index d1c0e9fc..0a84fce8 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -21,6 +21,8 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h" +#import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h" +#import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h" #import "GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHTTPFetcher.h" #import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h" #import "GoogleSignIn/Sources/GIDEMMSupport.h" @@ -79,9 +81,6 @@ // The URL template for the URL to revoke the token. static NSString *const kRevokeTokenURLTemplate = @"https://%@/o/oauth2/revoke"; -// Expected path in the URL scheme to be handled. -static NSString *const kBrowserCallbackPath = @"/oauth2callback"; - // Expected path for EMM callback. static NSString *const kEMMCallbackPath = @"/emmcallback"; @@ -116,9 +115,6 @@ static NSString *const kAudienceParameter = @"audience"; // See b/11669751 . static NSString *const kOpenIDRealmParameter = @"openid.realm"; -static NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes"; -static NSString *const kLoginHintParameter = @"login_hint"; -static NSString *const kHostedDomainParameter = @"hd"; // Minimum time to expiration for a restored access token. static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0; @@ -148,8 +144,6 @@ @implementation GIDSignIn { // represent a sign in continuation. GIDSignInInternalOptions *_currentOptions; - // AppAuth external user-agent session state. - id _currentAuthorizationFlow; // Flag to indicate that the auth flow is restarting. BOOL _restarting; @@ -161,6 +155,9 @@ @implementation GIDSignIn { // The class to fetches data from a url end point. id _httpFetcher; + + // The class to control the authorization flow. + id _authorizationFlowProcessor; } #pragma mark - Public methods @@ -172,8 +169,7 @@ @implementation GIDSignIn { - (BOOL)handleURL:(NSURL *)url { // Check if the callback path matches the expected one for a URL from Safari/Chrome/SafariVC. if ([url.path isEqual:kBrowserCallbackPath]) { - if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { - _currentAuthorizationFlow = nil; + if ([_authorizationFlowProcessor resumeExternalUserAgentFlowWithURL:url]) { return YES; } return NO; @@ -245,12 +241,12 @@ - (void)signInWithPresentingViewController:(UIViewController *)presentingViewCon additionalScopes:(nullable NSArray *)additionalScopes completion:(nullable GIDSignInCompletion)completion { GIDSignInInternalOptions *options = - [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration - presentingViewController:presentingViewController - loginHint:hint - addScopesFlow:NO - scopes:additionalScopes - completion:completion]; + [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration + presentingViewController:presentingViewController + loginHint:hint + addScopesFlow:NO + scopes:additionalScopes + completion:completion]; [self signInWithOptions:options]; } @@ -323,12 +319,12 @@ - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow additionalScopes:(nullable NSArray *)additionalScopes completion:(nullable GIDSignInCompletion)completion { GIDSignInInternalOptions *options = - [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration - presentingWindow:presentingWindow - loginHint:hint - addScopesFlow:NO - scopes:additionalScopes - completion:completion]; + [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration + presentingWindow:presentingWindow + loginHint:hint + addScopesFlow:NO + scopes:additionalScopes + completion:completion]; [self signInWithOptions:options]; } @@ -451,15 +447,20 @@ + (GIDSignIn *)sharedInstance { - (id)initPrivate { id keychainHandler = [[GIDKeychainHandler alloc] init]; id httpFetcher = [[GIDHTTPFetcher alloc] init]; + id authorizationFlowProcessor = + [[GIDAuthorizationFlowProcessor alloc] init]; id profileDataFetcher = [[GIDProfileDataFetcher alloc] init]; return [self initWithKeychainHandler:keychainHandler httpFetcher:httpFetcher - profileDataFetcher:profileDataFetcher]; + profileDataFetcher:profileDataFetcher + authorizationFlowProcessor:authorizationFlowProcessor]; } - (instancetype)initWithKeychainHandler:(id)keychainHandler httpFetcher:(id)httpFetcher - profileDataFetcher:(id)profileDataFetcher { + profileDataFetcher:(id)profileDataFetcher + authorizationFlowProcessor: + (id)authorizationFlowProcessor { self = [super init]; if (self) { // Get the bundle of the current executable. @@ -489,6 +490,7 @@ - (instancetype)initWithKeychainHandler:(id)keychainHandler _keychainHandler = keychainHandler; _httpFetcher = httpFetcher; + _authorizationFlowProcessor = authorizationFlowProcessor; _profileDataFetcher = profileDataFetcher; } return self; @@ -515,7 +517,6 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options { // Explicitly throw exception for missing client ID here. This must come before // scheme check because schemes rely on reverse client IDs. [self assertValidParameters]; - [self assertValidPresentingViewController]; // If the application does not support the required URL schemes tell the developer so. @@ -554,64 +555,17 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options { #pragma mark - Authentication flow - (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options { - GIDSignInCallbackSchemes *schemes = - [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID]; - NSURL *redirectURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@", - [schemes clientIdentifierScheme], - kBrowserCallbackPath]]; NSString *emmSupport; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST emmSupport = [[self class] isOperatingSystemAtLeast9] ? kEMMVersion : nil; #elif TARGET_OS_MACCATALYST || TARGET_OS_OSX emmSupport = nil; #endif // TARGET_OS_MACCATALYST || TARGET_OS_OSX - - NSMutableDictionary *additionalParameters = [@{} mutableCopy]; - additionalParameters[kIncludeGrantedScopesParameter] = @"true"; - if (options.configuration.serverClientID) { - additionalParameters[kAudienceParameter] = options.configuration.serverClientID; - } - if (options.loginHint) { - additionalParameters[kLoginHintParameter] = options.loginHint; - } - if (options.configuration.hostedDomain) { - additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain; - } - -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - [additionalParameters addEntriesFromDictionary: - [GIDEMMSupport parametersWithParameters:options.extraParams - emmSupport:emmSupport - isPasscodeInfoRequired:NO]]; -#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST - [additionalParameters addEntriesFromDictionary:options.extraParams]; -#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST - additionalParameters[kSDKVersionLoggingParameter] = GIDVersion(); - additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment(); - NSURL *authorizationEndpointURL = [GIDSignInPreferences authorizationEndpointURL]; - NSURL *tokenEndpointURL = [GIDSignInPreferences tokenEndpointURL]; - OIDServiceConfiguration *appAuthConfiguration = - [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpointURL - tokenEndpoint:tokenEndpointURL]; - - OIDAuthorizationRequest *request = - [[OIDAuthorizationRequest alloc] initWithConfiguration:appAuthConfiguration - clientId:options.configuration.clientID - scopes:options.scopes - redirectURL:redirectURL - responseType:OIDResponseTypeCode - additionalParameters:additionalParameters]; - - _currentAuthorizationFlow = [OIDAuthorizationService - presentAuthorizationRequest:request -#if TARGET_OS_IOS || TARGET_OS_MACCATALYST - presentingViewController:options.presentingViewController -#elif TARGET_OS_OSX - presentingWindow:options.presentingWindow -#endif // TARGET_OS_OSX - callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse, - NSError *_Nullable error) { + [_authorizationFlowProcessor startWithOptions:options + emmSupport:(NSString *)emmSupport + completion:^(OIDAuthorizationResponse *authorizationResponse, + NSError *error) { [self processAuthorizationResponse:authorizationResponse error:error emmSupport:emmSupport]; @@ -680,7 +634,6 @@ - (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationRe // Perform authentication with the provided options. - (void)authenticateWithOptions:(GIDSignInInternalOptions *)options { - // If this is an interactive flow, we're not going to try to restore any saved auth state. if (options.interactive) { [self authenticateInteractivelyWithOptions:options]; @@ -873,12 +826,11 @@ - (BOOL)handleDevicePolicyAppURL:(NSURL *)url { return NO; } #endif // TARGET_OS_OSX - if (!_currentAuthorizationFlow) { + if (!_authorizationFlowProcessor.isStarted) { return NO; } _restarting = YES; - [_currentAuthorizationFlow cancel]; - _currentAuthorizationFlow = nil; + [_authorizationFlowProcessor cancelAuthenticationFlow]; _restarting = NO; NSDictionary *extraParameters = @{ kEMMRestartAuthParameter : @"1" }; // In iOS 13 the presentation of ASWebAuthenticationSession needs an anchor window, diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.h b/GoogleSignIn/Sources/GIDSignInPreferences.h index 3d630f77..f7f0a637 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.h +++ b/GoogleSignIn/Sources/GIDSignInPreferences.h @@ -18,9 +18,15 @@ NS_ASSUME_NONNULL_BEGIN +/// The name of the query parameter used for logging the SDK version. extern NSString *const kSDKVersionLoggingParameter; + +/// The name of the query parameter used for logging the Apple execution environment. extern NSString *const kEnvironmentLoggingParameter; +/// Expected path in the URL scheme to be handled. +extern NSString *const kBrowserCallbackPath; + NSString* GIDVersion(void); NSString* GIDEnvironment(void); diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.m b/GoogleSignIn/Sources/GIDSignInPreferences.m index 7c4aa159..366f3bbb 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.m +++ b/GoogleSignIn/Sources/GIDSignInPreferences.m @@ -16,16 +16,14 @@ NS_ASSUME_NONNULL_BEGIN +NSString *const kSDKVersionLoggingParameter = @"gpsdk"; +NSString *const kEnvironmentLoggingParameter = @"gidenv"; +NSString *const kBrowserCallbackPath = @"/oauth2callback"; + static NSString *const kLSOServer = @"accounts.google.com"; static NSString *const kTokenServer = @"oauth2.googleapis.com"; static NSString *const kUserInfoServer = @"www.googleapis.com"; -// The name of the query parameter used for logging the SDK version. -NSString *const kSDKVersionLoggingParameter = @"gpsdk"; - -// The name of the query parameter used for logging the Apple execution environment. -NSString *const kEnvironmentLoggingParameter = @"gidenv"; - // Supported Apple execution environments static NSString *const kAppleEnvironmentUnknown = @"unknown"; static NSString *const kAppleEnvironmentIOS = @"ios"; diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index 94368cfc..35a45c93 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -29,6 +29,7 @@ NS_ASSUME_NONNULL_BEGIN @class GIDGoogleUser; @class GIDSignInInternalOptions; +@protocol GIDAuthorizationFlowProcessor; @protocol GIDHTTPFetcher; @protocol GIDKeychainHandler; @protocol GIDProfileDataFetcher; @@ -54,6 +55,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); - (instancetype)initWithKeychainHandler:(id)keychainHandler httpFetcher:(id)HTTPFetcher profileDataFetcher:(id)profileDataFetcher + authorizationFlowProcessor: + (id)authorizationFlowProcessor NS_DESIGNATED_INITIALIZER; /// Authenticates with extra options. diff --git a/GoogleSignIn/Tests/Unit/GIDAuthorizationFlowProcessorTest.m b/GoogleSignIn/Tests/Unit/GIDAuthorizationFlowProcessorTest.m new file mode 100644 index 00000000..b1c5c756 --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDAuthorizationFlowProcessorTest.m @@ -0,0 +1,139 @@ +// Copyright 2023 Google LLC +// +// 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 "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h" + +#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" +#import "GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h" +#import "GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h" +#import "GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.h" + +#import + +#ifdef SWIFT_PACKAGE +@import AppAuth; +@import OCMock; +#else +#import +#import +#endif + +static NSString *const kFakeURL = @"www.fakeURL.com"; +static NSString *const kErrorDomain = @"ERROR_DOMAIN"; +static NSInteger const kErrorCode = 400; +static NSInteger const kTimeout = 1; + +@interface GIDAuthorizationFlowProcessorTest : XCTestCase { + GIDAuthorizationFlowProcessor *_authorizationFlowProcessor; + OIDFakeExternalUserAgentSession *_fakeExternalUserAgentSession; + id _authorizationServiceMock; + OIDAuthorizationResponse *_fakeResponse; +} + +@end + +@implementation GIDAuthorizationFlowProcessorTest + +- (void)setUp { + [super setUp]; + + _authorizationFlowProcessor = [[GIDAuthorizationFlowProcessor alloc] init]; + _fakeExternalUserAgentSession= [[OIDFakeExternalUserAgentSession alloc] init]; + + _authorizationServiceMock = OCMClassMock([OIDAuthorizationService class]); + _fakeResponse = [OIDAuthorizationResponse testInstance]; + NSError *error = [self error]; + OCMStub([_authorizationServiceMock + presentAuthorizationRequest:[OCMArg any] +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST + presentingViewController:[OCMArg any] +#elif TARGET_OS_OSX + presentingWindow:[OCMArg any] +#endif // TARGET_OS_OSX + callback:([OCMArg invokeBlockWithArgs:_fakeResponse, error, nil]) + ]).andReturn(_fakeExternalUserAgentSession); +} + +- (void)testStartAndCancelAuthorizationFlow_success { + XCTestExpectation *expectation = [self expectationWithDescription:@"completion is invoked."]; + GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init]; + [_authorizationFlowProcessor startWithOptions:options + emmSupport:nil + completion:^(OIDAuthorizationResponse *authorizationResponse, + NSError *error) { + XCTAssertEqualObjects(authorizationResponse.accessToken, + self->_fakeResponse.accessToken); + XCTAssertEqualObjects(authorizationResponse.authorizationCode, + self->_fakeResponse.authorizationCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + XCTAssertTrue(_authorizationFlowProcessor.isStarted); + + [_authorizationFlowProcessor cancelAuthenticationFlow]; + XCTAssertFalse(_authorizationFlowProcessor.isStarted); +} + +- (void)testStartAndResumeAuthorizationFlow_success { + XCTestExpectation *expectation = [self expectationWithDescription:@"completion is invoked."]; + GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init]; + [_authorizationFlowProcessor startWithOptions:options + emmSupport:nil + completion:^(OIDAuthorizationResponse *authorizationResponse, + NSError *error) { + XCTAssertEqualObjects(authorizationResponse.accessToken, + self->_fakeResponse.accessToken); + XCTAssertEqualObjects(authorizationResponse.authorizationCode, + self->_fakeResponse.authorizationCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(_authorizationFlowProcessor.isStarted); + + _fakeExternalUserAgentSession.resumeExternalUserAgentFlow = YES; + + NSURL *url = [[NSURL alloc] initWithString:kFakeURL]; + [_authorizationFlowProcessor resumeExternalUserAgentFlowWithURL:url]; + XCTAssertFalse(_authorizationFlowProcessor.isStarted); +} + +- (void)testStartAndFailToResumeAuthorizationFlow { + XCTestExpectation *expectation = [self expectationWithDescription:@"completion is invoked."]; + GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init]; + [_authorizationFlowProcessor startWithOptions:options + emmSupport:nil + completion:^(OIDAuthorizationResponse *authorizationResponse, + NSError *error) { + XCTAssertEqualObjects(authorizationResponse.accessToken, + self->_fakeResponse.accessToken); + XCTAssertEqualObjects(authorizationResponse.authorizationCode, + self->_fakeResponse.authorizationCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + XCTAssertTrue(_authorizationFlowProcessor.isStarted); + + _fakeExternalUserAgentSession.resumeExternalUserAgentFlow = NO; + NSURL *url = [[NSURL alloc] initWithString:kFakeURL]; + [_authorizationFlowProcessor resumeExternalUserAgentFlowWithURL:url]; + XCTAssertTrue(_authorizationFlowProcessor.isStarted); +} + +#pragma mark - Helpers + +- (NSError *)error { + return [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:nil]; +} + +@end diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index fe1de7fd..d0fc72fa 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -26,6 +26,7 @@ // Test module imports @import GoogleSignIn; +#import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/GIDAuthorizationFlowProcessor.h" #import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" #import "GoogleSignIn/Sources/GIDSignIn_Private.h" @@ -313,10 +314,16 @@ - (void)setUp { _httpFetcher = [[GIDFakeHTTPFetcher alloc] init]; + GIDAuthorizationFlowProcessor * authorizationFlowProcessor = + [[GIDAuthorizationFlowProcessor alloc] init]; + id profileDataFetcher = [[GIDProfileDataFetcher alloc] init]; + _signIn = [[GIDSignIn alloc] initWithKeychainHandler:_keychainHandler httpFetcher:_httpFetcher - profileDataFetcher:profileDataFetcher]; + profileDataFetcher:profileDataFetcher + authorizationFlowProcessor:authorizationFlowProcessor]; + _hint = nil; __weak GIDSignInTest *weakSelf = self; diff --git a/GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.h b/GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.h new file mode 100644 index 00000000..868bd25b --- /dev/null +++ b/GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Google LLC + * + * 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 + +#ifdef SWIFT_PACKAGE +@import AppAuth; +#else +#import +#endif + +/// The fake OIDExternalUserAgentSession. +@interface OIDFakeExternalUserAgentSession : NSObject + +/// Set the return value for the method `resumeExternalUserAgentFlowWithURL:`. +/// The defualt value is YES. +@property(nonatomic) BOOL resumeExternalUserAgentFlow; + +@end diff --git a/GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.m b/GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.m new file mode 100644 index 00000000..d3268e61 --- /dev/null +++ b/GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.m @@ -0,0 +1,44 @@ +// Copyright 2023 Google LLC +// +// 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 "GoogleSignIn/Tests/Unit/OIDFakeExternalUserAgentSession.h" + +@implementation OIDFakeExternalUserAgentSession + +- (instancetype)init { + self = [super init]; + if (self) { + self.resumeExternalUserAgentFlow = YES; + } + return self; +} + +- (void)cancel { + // no op. +} + +- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL { + return self.resumeExternalUserAgentFlow; +} + +- (void)cancelWithCompletion:(nullable void (^)(void))completion { + NSAssert(NO, @"Not implemented."); +} + + +- (void)failExternalUserAgentFlowWithError:(nonnull NSError *)error { + NSAssert(NO, @"Not implemented."); +} + +@end