diff --git a/GoogleSignIn/Sources/GIDAuthFlow.h b/GoogleSignIn/Sources/GIDAuthFlow.h new file mode 100644 index 00000000..26716c7c --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthFlow.h @@ -0,0 +1,61 @@ +/* + * Copyright 2024 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/GIDCallbackQueue.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OIDAuthState; +@class GIDProfileData; + +/// The callback queue used for authentication flow. +@interface GIDAuthFlow : GIDCallbackQueue + +/// A representation of the state of the OAuth session for this instance. +@property(nonatomic, strong, nullable) OIDAuthState *authState; + +/// The error thrown from the OAuth session encountered for this instance. +@property(nonatomic, strong, nullable) NSError *error; + +/// The EMM support version. +@property(nonatomic, copy, nullable) NSString *emmSupport; + +/// The profile data extracted from the ID token. +@property(nonatomic, nullable) GIDProfileData *profileData; + +/// Initialize a `GIDAuthFlow` object by specifying all available properties. +/// +/// @param authState The configuration to be used. +/// @param error The configuration to be used. +/// @param emmSupport The configuration to be used. +/// @param profileData The configuration to be used. +/// @return An initialized `GIDAuthFlow` instance. +- (instancetype)initWithAuthState:(nullable OIDAuthState *)authState + error:(nullable NSError *)error + emmSupport:(nullable NSString *)emmSupport + profileData:(nullable GIDProfileData *)profileData + NS_DESIGNATED_INITIALIZER; + +/// Initialize a `GIDAuthFlow` object by calling the designated initializer with `nil` values. +/// +/// @return An initialized `GIDAuthFlow` instance. +- (instancetype)init; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDAuthFlow.m b/GoogleSignIn/Sources/GIDAuthFlow.m new file mode 100644 index 00000000..d82ceb64 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthFlow.m @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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/GIDAuthFlow.h" + +@implementation GIDAuthFlow + +- (instancetype)initWithAuthState:(nullable OIDAuthState *)authState + error:(nullable NSError *)error + emmSupport:(nullable NSString *)emmSupport + profileData:(nullable GIDProfileData *)profileData { + self = [super init]; + if (self) { + _authState = authState; + _error = error; + _emmSupport = emmSupport; + _profileData = profileData; + } + return self; +} + +- (instancetype)init { + return [self initWithAuthState:nil + error:nil + emmSupport:nil + profileData:nil]; +} + +@end diff --git a/GoogleSignIn/Sources/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h b/GoogleSignIn/Sources/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h new file mode 100644 index 00000000..ddd362af --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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 GIDAuthFlow; +@class GIDConfiguration; +@class OIDAuthorizationResponse; + +NS_ASSUME_NONNULL_BEGIN + +/// A list of potential current flow names. +typedef NS_ENUM(NSInteger, GIDFlowName) { + /// The Sign In flow. + GIDFlowNameSignIn = 0, + /// The Verify flow. + GIDFlowNameVerifyAccountDetail = 1, +}; + +@protocol GIDAuthorizationResponseHandling + +/// Initializes a new instance of the `GIDAuthorizationResponseHandling` class with the provided fields. +/// +/// @param authorizationResponse The authorization response to be processed. +/// @param emmSupport The EMM support version. +/// @param flowName The name of the current flow. +/// @param configuration The configuration. +/// @param error The error thrown if there's no authorization response. +/// @return A new initialized instance of the `GIDAuthorizationResponseHandling` class. +- (instancetype) + initWithAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse + emmSupport:(nullable NSString *)emmSupport + flowName:(enum GIDFlowName)flowName + configuration:(nullable GIDConfiguration *)configuration + error:(nullable NSError *)error; + +/// Fetches the access token if necessary as part of the auth flow. +/// +/// @param authFlow The auth flow to either fetch tokens or error. +- (void)maybeFetchToken:(GIDAuthFlow *)authFlow; + +/// Processes the authorization response and returns an auth flow. +/// +/// @return An instance of `GIDAuthFlow`. +- (GIDAuthFlow *)generateAuthFlowFromAuthorizationResponse; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.h b/GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.h new file mode 100644 index 00000000..3165870e --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.h @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OIDAuthState; + +/// A fake implementation of `GIDAuthorizationResponseHandling` for testing purposes. +@interface GIDAuthorizationResponseHandlingFake : NSObject + +/// The auth state to be used to fetch tokens. +@property (nonatomic, nullable) OIDAuthState *authState; + +/// The error to be passed into the completion. +@property (nonatomic, nullable) NSError *error; + +/// Creates an instance conforming to `GIDAuthorizationResponseHandling` with the provided +/// auth state and error. +/// +/// @param authState The `OIDAuthState` instance to access tokens. +/// @param error The `NSError` to pass into the completion. +- (instancetype)initWithAuthState:(nullable OIDAuthState *)authState + error:(nullable NSError *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.m b/GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.m new file mode 100644 index 00000000..f10bdd66 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.m @@ -0,0 +1,76 @@ +/* + * Copyright 2024 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/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.h" + +#import "GoogleSignIn/Sources/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h" + +#import "GoogleSignIn/Sources/GIDAuthFlow.h" +#import "GoogleSignIn/Sources/GIDSignInConstants.h" + +#ifdef SWIFT_PACKAGE +@import AppAuth; +#else +#import +#import +#endif + +@implementation GIDAuthorizationResponseHandlingFake + +- (instancetype)initWithAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse + emmSupport:(NSString *)emmSupport + flowName:(enum GIDFlowName)flowName + configuration:(GIDConfiguration *)configuration + error:(NSError *)error { + self = [super init]; + if (self) { + NSAssert(false, @"This class is only to be used in testing. Do not use."); + } + return self; +} + +- (instancetype)initWithAuthState:(nullable OIDAuthState *)authState + error:(nullable NSError *)error { + self = [super init]; + if (self) { + _authState = authState; + _error = error; + } + return self; +} + +- (GIDAuthFlow *)generateAuthFlowFromAuthorizationResponse { + GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:self.authState + error:self.error + emmSupport:nil + profileData:nil]; + return authFlow; +} + +- (void)maybeFetchToken:(GIDAuthFlow *)authFlow { + if (authFlow.error || + (_authState.lastTokenResponse.accessToken && + [_authState.lastTokenResponse.accessTokenExpirationDate timeIntervalSinceNow] > + kMinimumRestoredAccessTokenTimeToExpire)) { + return; + } + + authFlow.authState = self.authState; + authFlow.error = self.error; +} + +@end + diff --git a/GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h b/GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h new file mode 100644 index 00000000..8f80054d --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h" + +@class GIDAuthFlow; +@class GIDConfiguration; +@class OIDAuthorizationResponse; + +NS_ASSUME_NONNULL_BEGIN + +/// A helper class to process the authorization response. +@interface GIDAuthorizationResponseHelper : NSObject + +/// The response handler used to process the authorization response. +@property (nonatomic, readonly) id responseHandler; + +/// Initializes a new instance of the `GIDAuthorizationResponseHelper` class with the provided field. +/// +/// @param responseHandler The response handler with the authorization response to process. +/// @return A new initialized instance of the `GIDAuthorizationResponseHelper` class. +- (instancetype) +initWithAuthorizationResponseHandler:(id)responseHandler; + +/// Fetches the access token if necessary using the response handler as part of the auth flow. +/// +/// @param authFlow The auth flow to either fetch tokens or error. +- (void)fetchTokenWithAuthFlow:(GIDAuthFlow *)authFlow; + + +/// Processes the initialized authorization response and returns a filled `GIDAuthFlow` instance. +/// +/// @return An instance of `GIDAuthFlow` to either fetch tokens or error. +- (GIDAuthFlow *)fetchAuthFlowFromProcessedResponse; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.m b/GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.m new file mode 100644 index 00000000..2ac14de4 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.m @@ -0,0 +1,66 @@ +/* + * Copyright 2024 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/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h" + +#import "GoogleSignIn/Sources/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h" + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h" + +#import "GoogleSignIn/Sources/GIDAuthFlow.h" +#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" +#import "GoogleSignIn/Sources/GIDEMMSupport.h" +#import "GoogleSignIn/Sources/GIDSignInConstants.h" +#import "GoogleSignIn/Sources/GIDSignInPreferences.h" + +#ifdef SWIFT_PACKAGE +@import AppAuth; +#else +#import +#import +#import +#import +#import +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@implementation GIDAuthorizationResponseHelper + +- (instancetype) +initWithAuthorizationResponseHandler:(id)responseHandler { + self = [super init]; + if (self) { + _responseHandler = responseHandler; + } + return self; +} + +- (void)fetchTokenWithAuthFlow:(GIDAuthFlow *)authFlow { + [self.responseHandler maybeFetchToken:authFlow]; +} + +- (GIDAuthFlow *)fetchAuthFlowFromProcessedResponse { + GIDAuthFlow *authFlow = [self.responseHandler generateAuthFlowFromAuthorizationResponse]; + + return authFlow; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h b/GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h new file mode 100644 index 00000000..523d8852 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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/GIDAuthorizationResponse/API/GIDAuthorizationResponseHandling.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A class that handles the authorization response and fetches tokens. +@interface GIDAuthorizationResponseHandler : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.m b/GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.m new file mode 100644 index 00000000..832ea3e9 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.m @@ -0,0 +1,268 @@ +/* + * Copyright 2024 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/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h" + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h" + +#import "GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h" + +#import "GoogleSignIn/Sources/GIDAuthFlow.h" +#import "GoogleSignIn/Sources/GIDSignInConstants.h" +#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" +#import "GoogleSignIn/Sources/GIDEMMSupport.h" +#import "GoogleSignIn/Sources/GIDSignInPreferences.h" + +#ifdef SWIFT_PACKAGE +@import AppAuth; +#else +#import +#import +#import +#import +#import +#import +#endif + +@interface GIDAuthorizationResponseHandler () + +/// The authorization response to process. +@property(nonatomic, nullable) OIDAuthorizationResponse *authorizationResponse; + +/// The EMM support version. +@property(nonatomic, nullable) NSString *emmSupport; + +/// The name of the current flow. +@property(nonatomic) GIDFlowName flowName; + +/// The configuration for the current flow. +@property(nonatomic, nullable) GIDConfiguration *configuration; + +/// The configuration for the current flow. +@property(nonatomic, nullable) NSError *error; + +@end + +@implementation GIDAuthorizationResponseHandler + +- (instancetype) + initWithAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse + emmSupport:(nullable NSString *)emmSupport + flowName:(GIDFlowName)flowName + configuration:(nullable GIDConfiguration *)configuration + error:(nullable NSError *)error { + self = [super init]; + if (self) { + _authorizationResponse = authorizationResponse; + _emmSupport = emmSupport; + _flowName = flowName; + _configuration = configuration; + _error = error; + } + return self; +} + +- (GIDAuthFlow *)generateAuthFlowFromAuthorizationResponse { + GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:nil + error:nil + emmSupport:_emmSupport + profileData:nil]; + + if (_authorizationResponse) { + if (_authorizationResponse.authorizationCode.length) { + authFlow.authState = + [[OIDAuthState alloc] initWithAuthorizationResponse:_authorizationResponse]; + [self maybeFetchToken:authFlow]; + } else { + [self authorizationCodeErrorToAuthFlow:authFlow]; + } + } else { + [self authorizationResponseErrorToAuthFlow:authFlow error:_error]; + } + return authFlow; +} + +- (void)maybeFetchToken:(GIDAuthFlow *)authFlow { + OIDAuthState *authState = authFlow.authState; + // Do nothing if we have an auth flow error or a restored access token that isn't near expiration. + if (authFlow.error || + (authState.lastTokenResponse.accessToken && + [authState.lastTokenResponse.accessTokenExpirationDate timeIntervalSinceNow] > + kMinimumRestoredAccessTokenTimeToExpire)) { + return; + } + NSMutableDictionary *additionalParameters = [@{} mutableCopy]; + if (_configuration.serverClientID) { + additionalParameters[kAudienceParameter] = _configuration.serverClientID; + } + if (_configuration.openIDRealm) { + additionalParameters[kOpenIDRealmParameter] = _configuration.openIDRealm; + } + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + if (_flowName == GIDFlowNameSignIn) { + NSDictionary *params = + authState.lastAuthorizationResponse.additionalParameters; + NSString *passcodeInfoRequired = (NSString *)params[kEMMPasscodeInfoRequiredKeyName]; + [additionalParameters addEntriesFromDictionary: + [GIDEMMSupport parametersWithParameters:@{} + emmSupport:authFlow.emmSupport + isPasscodeInfoRequired:passcodeInfoRequired.length > 0]]; + } +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + additionalParameters[kSDKVersionLoggingParameter] = GIDVersion(); + additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment(); + + OIDTokenRequest *tokenRequest; + if (!authState.lastTokenResponse.accessToken && + authState.lastAuthorizationResponse.authorizationCode) { + tokenRequest = [authState.lastAuthorizationResponse + tokenExchangeRequestWithAdditionalParameters:additionalParameters]; + } else { + [additionalParameters + addEntriesFromDictionary:authState.lastTokenResponse.request.additionalParameters]; + tokenRequest = [authState tokenRefreshRequestWithAdditionalParameters:additionalParameters]; + } + + // TODO: Clean up callback flow (#427). + [authFlow wait]; + [OIDAuthorizationService + performTokenRequest:tokenRequest + callback:^(OIDTokenResponse *_Nullable tokenResponse, + NSError *_Nullable error) { + [authState updateWithTokenResponse:tokenResponse error:error]; + authFlow.error = error; + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + if (authFlow.emmSupport) { + [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *error) { + authFlow.error = error; + [authFlow next]; + }]; + } else { + [authFlow next]; + } +#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST + [authFlow next]; +#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST + }]; +} + +- (void)authorizationCodeErrorToAuthFlow:(GIDAuthFlow *)authFlow { + NSDictionary *params = _authorizationResponse.additionalParameters; + NSString *errorString = (NSString *)params[kOAuth2ErrorKeyName]; + + switch (_flowName) { + case GIDFlowNameSignIn: { + GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown; +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + if (authFlow.emmSupport) { + [authFlow wait]; + BOOL isEMMError = [[GIDEMMErrorHandler sharedInstance] + handleErrorFromResponse:params + completion:^{ + [authFlow next]; + }]; + if (isEMMError) { + errorCode = kGIDSignInErrorCodeEMM; + } + } +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + + if ([errorString isEqualToString:kOAuth2AccessDenied]) { + errorCode = kGIDSignInErrorCodeCanceled; + } + + authFlow.error = [self errorWithString:errorString code:errorCode]; + } + break; + case GIDFlowNameVerifyAccountDetail: { +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + GIDVerifyErrorCode errorCode = GIDVerifyErrorCodeUnknown; + if ([errorString isEqualToString:kOAuth2AccessDenied]) { + errorCode = GIDVerifyErrorCodeCanceled; + } + + authFlow.error = [self errorWithString:errorString code:errorCode]; +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + break; + } + } +} + +- (void)authorizationResponseErrorToAuthFlow:(GIDAuthFlow *)authFlow + error:(NSError *)error { + NSString *errorString = [error localizedDescription]; + switch (_flowName) { + case GIDFlowNameSignIn: { + GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown; + if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) { + // The user has canceled the flow at the iOS modal dialog. + errorString = kUserCanceledSignInError; + errorCode = kGIDSignInErrorCodeCanceled; + } + authFlow.error = [self errorWithString:errorString code:errorCode]; + break; + } + case GIDFlowNameVerifyAccountDetail: { +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + GIDVerifyErrorCode errorCode = GIDVerifyErrorCodeUnknown; + if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) { + errorString = kUserCanceledVerifyError; + errorCode = GIDVerifyErrorCodeCanceled; + } + authFlow.error = [self errorWithString:errorString code:errorCode]; +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + break; + } + } +} + +- (NSError *)errorWithString:(nullable NSString *)errorString code:(NSInteger)code { + if (errorString == nil) { + errorString = @"Unknown error"; + } + + if (!_flowName) { + errorString = @"No specified flow"; + } + + NSDictionary *errorDict = @{ NSLocalizedDescriptionKey : errorString }; + switch (_flowName) { + case GIDFlowNameSignIn: + return [NSError errorWithDomain:kGIDSignInErrorDomain + code:code + userInfo:errorDict]; + break; + case GIDFlowNameVerifyAccountDetail: +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + return [NSError errorWithDomain:kGIDVerifyErrorDomain + code:code + userInfo:errorDict]; + break; +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + default: + break; + } + return [NSError errorWithDomain:kGIDSignInErrorDomain + code:code + userInfo:errorDict]; +} + + +@end diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 0452b24f..bae9e36e 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -21,11 +21,14 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h" +#import "GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h" +#import "GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h" + +#import "GoogleSignIn/Sources/GIDAuthFlow.h" #import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDSignInConstants.h" #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" -#import "GoogleSignIn/Sources/GIDCallbackQueue.h" #import "GoogleSignIn/Sources/GIDScopes.h" #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST @@ -52,8 +55,6 @@ #import #import #import -#import -#import #import #import @@ -97,16 +98,10 @@ // Parameters in the callback URL coming back from browser. static NSString *const kAuthorizationCodeKeyName = @"code"; -static NSString *const kOAuth2ErrorKeyName = @"error"; -static NSString *const kOAuth2AccessDenied = @"access_denied"; -static NSString *const kEMMPasscodeInfoRequiredKeyName = @"emm_passcode_info_required"; // Error string for unavailable keychain. static NSString *const kKeychainError = @"keychain error"; -// Error string for user cancelations. -static NSString *const kUserCanceledError = @"The user canceled the sign-in flow."; - // User preference key to detect fresh install of the app. static NSString *const kAppHasRunBeforeKey = @"GID_AppHasRunBefore"; @@ -116,22 +111,6 @@ // The delay before the new sign-in flow can be presented after the existing one is cancelled. static const NSTimeInterval kPresentationDelayAfterCancel = 1.0; -// Minimum time to expiration for a restored access token. -static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0; - -// The callback queue used for authentication flow. -@interface GIDAuthFlow : GIDCallbackQueue - -@property(nonatomic, strong, nullable) OIDAuthState *authState; -@property(nonatomic, strong, nullable) NSError *error; -@property(nonatomic, copy, nullable) NSString *emmSupport; -@property(nonatomic, nullable) GIDProfileData *profileData; - -@end - -@implementation GIDAuthFlow -@end - @implementation GIDSignIn { // This value is used when sign-in flows are resumed via the handling of a URL. Its value is // set when a sign-in flow is begun via |signInWithOptions:| when the options passed don't @@ -593,8 +572,8 @@ - (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options }]; } -- (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse - error:(NSError *)error +- (void)processAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse + error:(nullable NSError *)error emmSupport:(NSString *)emmSupport{ if (_restarting) { // The auth flow is restarting, so the work here would be performed in the next round. @@ -602,55 +581,23 @@ - (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationRe return; } - GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init]; - authFlow.emmSupport = emmSupport; + GIDAuthorizationResponseHandler *responseHandler = + [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:authorizationResponse + emmSupport:emmSupport + flowName:GIDFlowNameSignIn + configuration: + _currentOptions.configuration + error:error]; + GIDAuthorizationResponseHelper *responseHelper = + [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler]; - if (authorizationResponse) { - if (authorizationResponse.authorizationCode.length) { - authFlow.authState = [[OIDAuthState alloc] - initWithAuthorizationResponse:authorizationResponse]; - // perform auth code exchange - [self maybeFetchToken:authFlow]; - } else { - // There was a failure, convert to appropriate error code. - NSString *errorString; - GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown; - NSDictionary *params = authorizationResponse.additionalParameters; - -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - if (authFlow.emmSupport) { - [authFlow wait]; - BOOL isEMMError = [[GIDEMMErrorHandler sharedInstance] - handleErrorFromResponse:params - completion:^{ - [authFlow next]; - }]; - if (isEMMError) { - errorCode = kGIDSignInErrorCodeEMM; - } - } -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - errorString = (NSString *)params[kOAuth2ErrorKeyName]; - if ([errorString isEqualToString:kOAuth2AccessDenied]) { - errorCode = kGIDSignInErrorCodeCanceled; - } + GIDAuthFlow *authFlow = [responseHelper fetchAuthFlowFromProcessedResponse]; - authFlow.error = [self errorWithString:errorString code:errorCode]; - } - } else { - NSString *errorString = [error localizedDescription]; - GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown; - if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) { - // The user has canceled the flow at the iOS modal dialog. - errorString = kUserCanceledError; - errorCode = kGIDSignInErrorCodeCanceled; - } - authFlow.error = [self errorWithString:errorString code:errorCode]; + if (authFlow) { + [self addDecodeIdTokenCallback:authFlow]; + [self addSaveAuthCallback:authFlow]; + [self addCompletionCallback:authFlow]; } - - [self addDecodeIdTokenCallback:authFlow]; - [self addSaveAuthCallback:authFlow]; - [self addCompletionCallback:authFlow]; } // Perform authentication with the provided options. @@ -680,77 +627,26 @@ - (void)authenticateWithOptions:(GIDSignInInternalOptions *)options { } // Complete the auth flow using saved auth in keychain. - GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init]; - authFlow.authState = authState; - [self maybeFetchToken:authFlow]; + GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:authState + error:nil + emmSupport:nil + profileData:nil]; + GIDAuthorizationResponseHandler *responseHandler = + [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:nil + emmSupport:nil + flowName:GIDFlowNameSignIn + configuration: + _currentOptions.configuration + error:nil]; + GIDAuthorizationResponseHelper *responseHelper = + [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler]; + + [responseHelper fetchTokenWithAuthFlow:authFlow]; [self addDecodeIdTokenCallback:authFlow]; [self addSaveAuthCallback:authFlow]; [self addCompletionCallback:authFlow]; } -// Fetches the access token if necessary as part of the auth flow. -- (void)maybeFetchToken:(GIDAuthFlow *)authFlow { - OIDAuthState *authState = authFlow.authState; - // Do nothing if we have an auth flow error or a restored access token that isn't near expiration. - if (authFlow.error || - (authState.lastTokenResponse.accessToken && - [authState.lastTokenResponse.accessTokenExpirationDate timeIntervalSinceNow] > - kMinimumRestoredAccessTokenTimeToExpire)) { - return; - } - NSMutableDictionary *additionalParameters = [@{} mutableCopy]; - if (_currentOptions.configuration.serverClientID) { - additionalParameters[kAudienceParameter] = _currentOptions.configuration.serverClientID; - } - if (_currentOptions.configuration.openIDRealm) { - additionalParameters[kOpenIDRealmParameter] = _currentOptions.configuration.openIDRealm; - } -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - NSDictionary *params = - authState.lastAuthorizationResponse.additionalParameters; - NSString *passcodeInfoRequired = (NSString *)params[kEMMPasscodeInfoRequiredKeyName]; - [additionalParameters addEntriesFromDictionary: - [GIDEMMSupport parametersWithParameters:@{} - emmSupport:authFlow.emmSupport - isPasscodeInfoRequired:passcodeInfoRequired.length > 0]]; -#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST - additionalParameters[kSDKVersionLoggingParameter] = GIDVersion(); - additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment(); - - OIDTokenRequest *tokenRequest; - if (!authState.lastTokenResponse.accessToken && - authState.lastAuthorizationResponse.authorizationCode) { - tokenRequest = [authState.lastAuthorizationResponse - tokenExchangeRequestWithAdditionalParameters:additionalParameters]; - } else { - [additionalParameters - addEntriesFromDictionary:authState.lastTokenResponse.request.additionalParameters]; - tokenRequest = [authState tokenRefreshRequestWithAdditionalParameters:additionalParameters]; - } - - [authFlow wait]; - [OIDAuthorizationService - performTokenRequest:tokenRequest - callback:^(OIDTokenResponse *_Nullable tokenResponse, - NSError *_Nullable error) { - [authState updateWithTokenResponse:tokenResponse error:error]; - authFlow.error = error; - -#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - if (authFlow.emmSupport) { - [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *error) { - authFlow.error = error; - [authFlow next]; - }]; - } else { - [authFlow next]; - } -#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST - [authFlow next]; -#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST - }]; -} - // Adds a callback to the auth flow to save the auth object to |self| and the keychain as well. - (void)addSaveAuthCallback:(GIDAuthFlow *)authFlow { __weak GIDAuthFlow *weakAuthFlow = authFlow; diff --git a/GoogleSignIn/Sources/GIDSignInConstants.h b/GoogleSignIn/Sources/GIDSignInConstants.h index e04db0fa..aa06346e 100644 --- a/GoogleSignIn/Sources/GIDSignInConstants.h +++ b/GoogleSignIn/Sources/GIDSignInConstants.h @@ -39,3 +39,21 @@ extern NSString *const kLoginHintParameter; /// The name of the hosted domain parameter for the auth and token exchange endpoints. extern NSString *const kHostedDomainParameter; + +/// The name of the error key that occurred during the authorization or token exchange process. +extern NSString *const kOAuth2ErrorKeyName; + +/// The value of the error key when the user cancels the authorization or token exchange process. +extern NSString *const kOAuth2AccessDenied; + +/// The name of the key that indicates whether a passocde is required for EMM. +extern NSString *const kEMMPasscodeInfoRequiredKeyName; + +/// The error string for user cancelation in the sign-in flow. +extern NSString *const kUserCanceledSignInError; + +/// The error string for user cancelation in the verify flow. +extern NSString *const kUserCanceledVerifyError; + +/// Minimum time to expiration for a restored access token. +extern const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire; diff --git a/GoogleSignIn/Sources/GIDSignInConstants.m b/GoogleSignIn/Sources/GIDSignInConstants.m index bb90d37a..93e30994 100644 --- a/GoogleSignIn/Sources/GIDSignInConstants.m +++ b/GoogleSignIn/Sources/GIDSignInConstants.m @@ -27,3 +27,12 @@ NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes"; NSString *const kLoginHintParameter = @"login_hint"; NSString *const kHostedDomainParameter = @"hd"; + +NSString *const kOAuth2ErrorKeyName = @"error"; +NSString *const kOAuth2AccessDenied = @"access_denied"; +NSString *const kEMMPasscodeInfoRequiredKeyName = @"emm_passcode_info_required"; + +NSString *const kUserCanceledSignInError = @"The user canceled the sign-in flow."; +NSString *const kUserCanceledVerifyError = @"The user canceled the verification flow."; + +const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0; diff --git a/GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m b/GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m index 7e192768..3dfb27e7 100644 --- a/GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m +++ b/GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m @@ -20,27 +20,41 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiableAccountDetail.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiedAccountDetailResult.h" +#import "GoogleSignIn/Sources/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h" +#import "GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h" + +#import "GoogleSignIn/Sources/GIDAuthFlow.h" #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" #import "GoogleSignIn/Sources/GIDSignInConstants.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" -@import GTMAppAuth; - #ifdef SWIFT_PACKAGE @import AppAuth; -@import GTMSessionFetcherCore; #else #import +#import +#import +#import #import #import + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +#import +#endif + #endif #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +// TODO: Unify error domain across sign-in and verify flow (#425). +NSErrorDomain const kGIDVerifyErrorDomain = @"com.google.GIDVerifyAccountDetail"; + @implementation GIDVerifyAccountDetail { /// AppAuth configuration object. OIDServiceConfiguration *_appAuthConfiguration; + /// AppAuth external user-agent session state. + id _currentAuthorizationFlow; } - (instancetype)initWithConfig:(GIDConfiguration *)configuration { @@ -48,10 +62,11 @@ - (instancetype)initWithConfig:(GIDConfiguration *)configuration { if (self) { _configuration = configuration; - NSString *authorizationEndpointURL = [NSString stringWithFormat:kAuthorizationURLTemplate, - [GIDSignInPreferences googleAuthorizationServer]]; - NSString *tokenEndpointURL = [NSString stringWithFormat:kTokenURLTemplate, - [GIDSignInPreferences googleTokenServer]]; + NSString *authorizationEndpointURL = + [NSString stringWithFormat:kAuthorizationURLTemplate, + [GIDSignInPreferences googleAuthorizationServer]]; + NSString *tokenEndpointURL = + [NSString stringWithFormat:kTokenURLTemplate, [GIDSignInPreferences googleTokenServer]]; _appAuthConfiguration = [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:[NSURL URLWithString:authorizationEndpointURL] tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]]; @@ -148,6 +163,7 @@ - (void)verifyAccountDetailsInteractivelyWithOptions:(GIDSignInInternalOptions * if (options.configuration.hostedDomain) { additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain; } + additionalParameters[kSDKVersionLoggingParameter] = GIDVersion(); additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment(); @@ -159,14 +175,37 @@ - (void)verifyAccountDetailsInteractivelyWithOptions:(GIDSignInInternalOptions * } } - // TODO(#405): Use request variable to present request and process response. - __unused OIDAuthorizationRequest *request = + OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration clientId:options.configuration.clientID scopes:scopes redirectURL:redirectURL responseType:OIDResponseTypeCode additionalParameters:additionalParameters]; + + _currentAuthorizationFlow = [OIDAuthorizationService + presentAuthorizationRequest:request + presentingViewController:options.presentingViewController + callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse, + NSError *_Nullable error) { + [self processAuthorizationResponse:authorizationResponse + error:error]; + }]; +} + +- (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse + error:(NSError *)error { + GIDAuthorizationResponseHandler *responseHandler = + [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:authorizationResponse + emmSupport:nil + flowName:GIDFlowNameVerifyAccountDetail + configuration:_configuration + error:error]; + GIDAuthorizationResponseHelper *responseHelper = + [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler]; + + // TODO: Add completion callback method (#413). + __unused GIDAuthFlow *authFlow = [responseHelper fetchAuthFlowFromProcessedResponse]; } #pragma mark - Helpers diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h index 3eb2dea8..669800d7 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h @@ -32,6 +32,17 @@ NS_ASSUME_NONNULL_BEGIN @class GIDVerifiableAccountDetail; @class GIDVerifiedAccountDetailResult; +/// The error domain for `NSError`s returned by the Google Sign-In SDK. +extern NSErrorDomain const kGIDVerifyErrorDomain; + +/// A list of potential error codes returned from the Google Sign-In SDK. +typedef NS_ERROR_ENUM(kGIDVerifyErrorDomain, GIDVerifyErrorCode) { + /// Indicates an unknown error has occurred. + GIDVerifyErrorCodeUnknown = 0, + /// Indicates the user canceled the verification request. + GIDVerifyErrorCodeCanceled = 1, +}; + /// Represents a completion block that takes a `GIDVerifiedAccountDetailResult` on success or an /// error if the operation was unsuccessful. typedef void (^GIDVerifyCompletion)(GIDVerifiedAccountDetailResult *_Nullable verifiedResult, diff --git a/GoogleSignIn/Tests/Unit/GIDAuthFlowTest.m b/GoogleSignIn/Tests/Unit/GIDAuthFlowTest.m new file mode 100644 index 00000000..6f4b8fcd --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDAuthFlowTest.m @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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/GIDAuthFlow.h" + +#import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h" +#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" + +@interface GIDAuthFlowTest : XCTestCase +@end + +@implementation GIDAuthFlowTest + +- (void)testInitWithAuthState { + OIDAuthState *authState = [OIDAuthState testInstance]; + GIDProfileData *profileData = [GIDProfileData testInstance]; + + GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:authState + error:nil + emmSupport:nil + profileData:profileData]; + + XCTAssertNotNil(authFlow); + XCTAssertEqual(authFlow.authState, authState); + XCTAssertNil(authFlow.error); + XCTAssertNil(authFlow.emmSupport); + XCTAssertEqual(authFlow.profileData, profileData); +} + +- (void)testInit { + GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init]; + + XCTAssertNotNil(authFlow); + XCTAssertNil(authFlow.authState); + XCTAssertNil(authFlow.error); + XCTAssertNil(authFlow.emmSupport); + XCTAssertNil(authFlow.profileData); +} + +@end diff --git a/GoogleSignIn/Tests/Unit/GIDAuthorizationResponseHelperTest.m b/GoogleSignIn/Tests/Unit/GIDAuthorizationResponseHelperTest.m new file mode 100644 index 00000000..038413b9 --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDAuthorizationResponseHelperTest.m @@ -0,0 +1,185 @@ +/* + * Copyright 2024 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/GIDAuthorizationResponse/GIDAuthorizationResponseHelper.h" +#import "GoogleSignIn/Sources/GIDAuthorizationResponse/Fake/GIDAuthorizationResponseHandlingFake.h" +#import "GoogleSignIn/Sources/GIDAuthorizationResponse/Implementations/GIDAuthorizationResponseHandler.h" + +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h" +#import "GoogleSignIn/Sources/GIDAuthFlow.h" + +#import "GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h" +#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" +#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h" + +/// The EMM support version +static NSString *const kEMMVersion = @"1"; + +/// Time interval to use for an expiring access token. +static NSTimeInterval kAccessTokenExpiration = 20; + +@interface GIDAuthorizationResponseHelperTest : XCTestCase +@end + +@implementation GIDAuthorizationResponseHelperTest + +- (void)testInitWithAuthorizationResponseHandler { + GIDAuthorizationResponseHandler *responseHandler = + [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:nil + emmSupport:nil + flowName:GIDFlowNameVerifyAccountDetail + configuration:nil + error:nil]; + GIDAuthorizationResponseHelper *responseHelper = + [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler]; + + XCTAssertNotNil(responseHelper); + XCTAssertNotNil(responseHelper.responseHandler); +} + +- (void)testFetchTokenWithAuthFlow { + OIDTokenResponse *tokenResponse = + [OIDTokenResponse testInstanceWithAccessTokenExpiration:@(kAccessTokenExpiration)]; + OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; + + GIDAuthorizationResponseHandlingFake *responseHandler = + [[GIDAuthorizationResponseHandlingFake alloc] initWithAuthState:authState error:nil]; + GIDAuthorizationResponseHelper *responseHelper = + [[GIDAuthorizationResponseHelper alloc] initWithAuthorizationResponseHandler:responseHandler]; + GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:authState + error:nil + emmSupport:nil + profileData:nil]; + + [responseHelper fetchTokenWithAuthFlow:authFlow]; + XCTAssertNotNil(authFlow); + XCTAssertEqual(authFlow.authState, authState); + XCTAssertNil(authFlow.error); +} + +- (void)testSuccessfulGenerateAuthFlowFromAuthorizationResponse { + OIDTokenResponse *tokenResponse = + [OIDTokenResponse testInstanceWithAccessTokenExpiration:@(kAccessTokenExpiration)]; + OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; + + GIDAuthorizationResponseHandlingFake *responseHandler = + [[GIDAuthorizationResponseHandlingFake alloc] initWithAuthState:authState error:nil]; + + GIDAuthFlow *authFlow = [responseHandler generateAuthFlowFromAuthorizationResponse]; + XCTAssertNotNil(authFlow); + XCTAssertEqual(authFlow.authState, authState); + XCTAssertNil(authFlow.error); +} + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (void)testGenerateAuthFlowFromAuthorizationResponse_noCode { + NSDictionary *errorDict = + @{ NSLocalizedDescriptionKey : @"Unknown error" }; + NSError *expectedError = [NSError errorWithDomain:kGIDVerifyErrorDomain + code:GIDVerifyErrorCodeUnknown + userInfo:errorDict]; + + OIDAuthorizationResponse *authorizationResponse = + [OIDAuthorizationResponse testInstanceNoAuthCodeWithAdditionalParameters:nil + errorString:nil]; + GIDAuthorizationResponseHandler *responseHandler = + [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:authorizationResponse + emmSupport:nil + flowName:GIDFlowNameVerifyAccountDetail + configuration:nil + error:nil]; + + GIDAuthFlow *authFlow = [responseHandler generateAuthFlowFromAuthorizationResponse]; + XCTAssertNotNil(authFlow); + XCTAssertNil(authFlow.authState); + XCTAssertNotNil(authFlow.error); + XCTAssertEqualObjects(authFlow.error, expectedError); +} +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (void)testGenerateAuthFlowWithMissingAuthorizationResponse { + NSError *error = [NSError errorWithDomain:kGIDVerifyErrorDomain + code:GIDVerifyErrorCodeUnknown + userInfo:nil]; + + NSString *errorString = [error localizedDescription]; + NSDictionary *errorDict = @{ NSLocalizedDescriptionKey : errorString }; + NSError *expectedError = [NSError errorWithDomain:kGIDVerifyErrorDomain + code:GIDVerifyErrorCodeUnknown + userInfo:errorDict]; + + GIDAuthorizationResponseHandler *responseHandler = + [[GIDAuthorizationResponseHandler alloc] initWithAuthorizationResponse:nil + emmSupport:nil + flowName:GIDFlowNameVerifyAccountDetail + configuration:nil + error:error]; + + GIDAuthFlow *authFlow = [responseHandler generateAuthFlowFromAuthorizationResponse]; + XCTAssertNotNil(authFlow); + XCTAssertNil(authFlow.authState); + XCTAssertNotNil(authFlow.error); + XCTAssertEqualObjects(authFlow.error, expectedError); +} +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +- (void)testMaybeFetchToken_authFlowError { + NSError *expectedError = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeUnknown + userInfo:nil]; + + OIDTokenResponse *tokenResponse = + [OIDTokenResponse testInstanceWithAccessTokenExpiration:@(kAccessTokenExpiration)]; + OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; + + GIDAuthorizationResponseHandlingFake *responseHandler = + [[GIDAuthorizationResponseHandlingFake alloc] initWithAuthState:authState error:nil]; + GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:nil + error:expectedError + emmSupport:nil + profileData:nil]; + + [responseHandler maybeFetchToken:authFlow]; + XCTAssertNil(authFlow.authState); + XCTAssertNotNil(authFlow.error); + XCTAssertEqualObjects(authFlow.error, expectedError); +} + +- (void)testMaybeFetchToken_noRefresh { + NSError *expectedError = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeUnknown + userInfo:nil]; + + OIDAuthState *authState = [OIDAuthState testInstance]; + GIDAuthorizationResponseHandlingFake *responseHandler = + [[GIDAuthorizationResponseHandlingFake alloc] initWithAuthState:authState error:nil]; + GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] initWithAuthState:authState + error:expectedError + emmSupport:nil + profileData:nil]; + + [responseHandler maybeFetchToken:authFlow]; + XCTAssertNotNil(authFlow.authState); + XCTAssertEqualObjects(authFlow.authState, authState); + XCTAssertNotNil(authFlow.error); + XCTAssertEqualObjects(authFlow.error, expectedError); +} + +@end diff --git a/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h b/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h index 4e15ed45..e0f69141 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h @@ -28,4 +28,8 @@ (NSDictionary *)additionalParameters errorString:(NSString *)errorString; ++ (instancetype)testInstanceNoAuthCodeWithAdditionalParameters: + (NSDictionary *)additionalParameters + errorString:(NSString *)errorString; + @end diff --git a/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m b/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m index 78608b66..fbc8560c 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.m @@ -49,4 +49,22 @@ + (instancetype)testInstanceWithAdditionalParameters: parameters:parameters]; } ++ (instancetype)testInstanceNoAuthCodeWithAdditionalParameters: + (NSDictionary *)additionalParameters + errorString:(NSString *)errorString { + NSMutableDictionary *parameters; + if (errorString) { + parameters = [NSMutableDictionary dictionaryWithDictionary:@{ @"error" : errorString }]; + } else { + parameters = [NSMutableDictionary dictionaryWithDictionary:@{ + @"code" : @"", + }]; + if (additionalParameters) { + [parameters addEntriesFromDictionary:additionalParameters]; + } + } + return [[OIDAuthorizationResponse alloc] initWithRequest:[OIDAuthorizationRequest testInstance] + parameters:parameters]; +} + @end diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h index b565a392..2bff75ef 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h @@ -53,6 +53,8 @@ extern NSString * const kFatPictureURL; + (instancetype)testInstanceWithIDToken:(NSString *)idToken; ++ (instancetype)testInstanceWithAccessTokenExpiration:(NSNumber *)expiration; + + (instancetype)testInstanceWithIDToken:(NSString *)idToken accessToken:(NSString *)accessToken expiresIn:(NSNumber *)expiresIn diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m index 49823be7..45b0e77e 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m @@ -65,6 +65,14 @@ + (instancetype)testInstanceWithIDToken:(NSString *)idToken { tokenRequest:nil]; } ++ (instancetype)testInstanceWithAccessTokenExpiration:(NSNumber *)expiration { + return [OIDTokenResponse testInstanceWithIDToken:[self idToken] + accessToken:nil + expiresIn:expiration + refreshToken:nil + tokenRequest:nil]; +} + + (instancetype)testInstanceWithIDToken:(NSString *)idToken accessToken:(NSString *)accessToken expiresIn:(NSNumber *)expiresIn