GIDAuthorization.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /*
  2. * Copyright 2025 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthorization.h"
  17. #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
  18. #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
  19. #import "GoogleSignIn/Sources/GIDAuthorizationFlow/API/GIDAuthorizationFlowCoordinator.h"
  20. #import "GoogleSignIn/Sources/GIDAuthorizationFlow/Implementations/GIDAuthorizationFlow.h"
  21. #import "GoogleSignIn/Sources/GIDAuthorization_Private.h"
  22. #import "GoogleSignIn/Sources/GIDConfiguration_Private.h"
  23. #import "GoogleSignIn/Sources/GIDEMMSupport.h"
  24. #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
  25. #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
  26. #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
  27. #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
  28. #import "GoogleSignIn/Sources/GIDSignInResult_Private.h"
  29. @import GTMAppAuth;
  30. #ifdef SWIFT_PACKAGE
  31. @import AppAuth;
  32. #else
  33. #import <AppAuth/OIDAuthState.h>
  34. #endif
  35. // TODO: Put these constants in a constants file/class
  36. static NSString *const kGSIServiceName = @"gsi_auth";
  37. /// The EMM support version
  38. static NSString *const kEMMVersion = @"1";
  39. // Parameters for the auth and token exchange endpoints.
  40. static NSString *const kAudienceParameter = @"audience";
  41. static NSString *const kOpenIDRealmParameter = @"openid.realm";
  42. static NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes";
  43. static NSString *const kLoginHintParameter = @"login_hint";
  44. static NSString *const kHostedDomainParameter = @"hd";
  45. /// Expected path in the URL scheme to be handled.
  46. static NSString *const kBrowserCallbackPath = @"/oauth2callback";
  47. /// The URL template for the authorization endpoint.
  48. static NSString *const kAuthorizationURLTemplate = @"https://%@/o/oauth2/v2/auth";
  49. /// The URL template for the token endpoint.
  50. static NSString *const kTokenURLTemplate = @"https://%@/token";
  51. @interface GIDAuthorization ()
  52. @property(nonatomic, readonly) OIDServiceConfiguration *appAuthConfiguration;
  53. @end
  54. @implementation GIDAuthorization
  55. #pragma mark - Initialization
  56. - (instancetype)init {
  57. NSBundle *mainBundle = [NSBundle mainBundle];
  58. GIDConfiguration *defaultConfiguration = [GIDConfiguration configurationFromBundle:mainBundle];
  59. return [self initWithConfiguration:defaultConfiguration];
  60. }
  61. - (instancetype)initWithConfiguration:(GIDConfiguration *)configuration {
  62. GTMKeychainStore *keychainStore = [[GTMKeychainStore alloc] initWithItemName:kGSIServiceName];
  63. return [self initWithKeychainStore:keychainStore configuration:configuration];
  64. }
  65. - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore {
  66. return [self initWithKeychainStore:keychainStore configuration:nil];
  67. }
  68. - (instancetype)initWithKeychainStore:(nullable GTMKeychainStore *)keychainStore
  69. configuration:(nullable GIDConfiguration *)configuration {
  70. return [self initWithKeychainStore:keychainStore
  71. configuration:configuration
  72. authorizationFlowCoordinator:nil];
  73. }
  74. - (instancetype)initWithKeychainStore:(nullable GTMKeychainStore *)keychainStore
  75. configuration:(nullable GIDConfiguration *)configuration
  76. authorizationFlowCoordinator:(nullable id<GIDAuthorizationFlowCoordinator>)authFlow {
  77. self = [super init];
  78. if (self) {
  79. GTMKeychainStore *defaultStore = [[GTMKeychainStore alloc] initWithItemName:kGSIServiceName];
  80. GTMKeychainStore *store = keychainStore ?: defaultStore;
  81. _keychainStore = store;
  82. NSBundle *mainBundle = [NSBundle mainBundle];
  83. GIDConfiguration *defaultConfiguration = [GIDConfiguration configurationFromBundle:mainBundle];
  84. _currentConfiguration = configuration ?: defaultConfiguration;
  85. _authFlow = authFlow;
  86. // FIXME: This should be cleaner; i.e., the options has a configuration too...
  87. _authFlow.configuration = _currentConfiguration;
  88. _currentOptions = _authFlow.options;
  89. NSString *authorizationEnpointURL = [NSString stringWithFormat:kAuthorizationURLTemplate,
  90. [GIDSignInPreferences googleAuthorizationServer]];
  91. NSString *tokenEndpointURL = [NSString stringWithFormat:kTokenURLTemplate,
  92. [GIDSignInPreferences googleTokenServer]];
  93. NSURL *authEndpoint = [NSURL URLWithString:authorizationEnpointURL];
  94. NSURL *tokenEndpoint = [NSURL URLWithString:tokenEndpointURL];
  95. _appAuthConfiguration =
  96. [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authEndpoint
  97. tokenEndpoint:tokenEndpoint];
  98. _authFlow.serviceConfiguration = _appAuthConfiguration;
  99. }
  100. return self;
  101. }
  102. #pragma mark - Restoring Previous Sign Ins
  103. - (BOOL)hasPreviousSignIn {
  104. if (self.currentUser) {
  105. return self.currentUser.authState.isAuthorized;
  106. }
  107. OIDAuthState *authState = [self loadAuthState];
  108. return authState.isAuthorized;
  109. }
  110. #pragma mark - Load Previous Authorization State
  111. - (nullable OIDAuthState *)loadAuthState {
  112. GTMAuthSession *authSession = [self.keychainStore retrieveAuthSessionWithError:nil];
  113. return authSession.authState;
  114. }
  115. #pragma mark - Signing In
  116. // FIXME: Do not pass options here; put this on `GIDAuthorizationFlow`
  117. // But perhaps `options` are needed because the presenting vc could change
  118. - (void)signInWithOptions:(GIDSignInInternalOptions *)options {
  119. // Options for continuation are not the options we want to cache. The purpose of caching the
  120. // options in the first place is to provide continuation flows with a starting place from which to
  121. // derive suitable options for the continuation!
  122. if (!options.continuation) {
  123. self.currentOptions = options;
  124. }
  125. if (options.interactive) {
  126. // Ensure that a configuration has been provided.
  127. if (!self.currentConfiguration) {
  128. // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
  129. [NSException raise:NSInvalidArgumentException
  130. format:@"No active configuration. Make sure GIDClientID is set in Info.plist."];
  131. return;
  132. }
  133. // Explicitly throw exception for missing client ID here. This must come before
  134. // scheme check because schemes rely on reverse client IDs.
  135. [self assertValidParameters];
  136. [self assertValidPresentingController];
  137. id<GIDBundle> bundle = self.authFlow.options.bundle;
  138. NSString *clientID = self.currentOptions.configuration.clientID;
  139. // If the application does not support the required URL schemes tell the developer so.
  140. GIDSignInCallbackSchemes *schemes =
  141. [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:clientID bundle:bundle];
  142. NSArray<NSString *> *unsupportedSchemes = [schemes unsupportedSchemes];
  143. if (unsupportedSchemes.count != 0) {
  144. // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
  145. [NSException raise:NSInvalidArgumentException
  146. format:@"Your app is missing support for the following URL schemes: %@",
  147. [unsupportedSchemes componentsJoinedByString:@", "]];
  148. }
  149. }
  150. // If this is a non-interactive flow, use cached authentication if possible.
  151. if (!options.interactive && self.currentUser) {
  152. [self.currentUser refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *unused, NSError *error) {
  153. if (error) {
  154. [self authenticateWithOptions:options];
  155. } else {
  156. if (options.completion) {
  157. self->_currentOptions = nil;
  158. dispatch_async(dispatch_get_main_queue(), ^{
  159. GIDSignInResult *signInResult =
  160. [[GIDSignInResult alloc] initWithGoogleUser:[self currentUser] serverAuthCode:nil];
  161. options.completion(signInResult, nil);
  162. });
  163. }
  164. }
  165. }];
  166. } else {
  167. [self authenticateWithOptions:options];
  168. }
  169. }
  170. #pragma mark - Authorization Flow
  171. // FIXME: Do not pass options here
  172. - (void)authenticateWithOptions:(GIDSignInInternalOptions *)options {
  173. // If this is an interactive flow, we're not going to try to restore any saved auth state.
  174. if (options.interactive) {
  175. [self.authFlow authorizeInteractively];
  176. return;
  177. }
  178. // Try retrieving an authorization object from the keychain.
  179. OIDAuthState *authState = [self loadAuthState];
  180. if (![authState isAuthorized]) {
  181. // No valid auth in keychain, per documentation/spec, notify callback of failure.
  182. NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
  183. code:kGIDSignInErrorCodeHasNoAuthInKeychain
  184. userInfo:nil];
  185. if (options.completion) {
  186. self.currentOptions = nil;
  187. dispatch_async(dispatch_get_main_queue(), ^{
  188. options.completion(nil, error);
  189. });
  190. }
  191. return;
  192. }
  193. // If we have an `authFlow`, then it was injected and we are testing; don't overwrite
  194. if (!self.authFlow) {
  195. self.authFlow = [[GIDAuthorizationFlow alloc] initWithSignInOptions:self.currentOptions
  196. authState:authState
  197. profileData:nil
  198. googleUser:self.currentUser
  199. externalUserAgentSession:nil
  200. emmSupport:nil
  201. error:nil];
  202. }
  203. [self.authFlow authorize];
  204. }
  205. #pragma mark - Validity Assertions
  206. - (void)assertValidParameters {
  207. if (![_currentOptions.configuration.clientID length]) {
  208. // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
  209. [NSException raise:NSInvalidArgumentException
  210. format:@"You must specify `clientID` in `GIDConfiguration`"];
  211. }
  212. }
  213. - (void)assertValidPresentingController {
  214. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  215. if (!self.currentOptions.presentingViewController)
  216. #elif TARGET_OS_OSX
  217. if (!self.currentOptions.presentingWindow)
  218. #endif // TARGET_OS_OSX
  219. {
  220. // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
  221. [NSException raise:NSInvalidArgumentException
  222. format:@"`presentingViewController` must be set."];
  223. }
  224. }
  225. #pragma mark - Current User
  226. - (nullable GIDGoogleUser *)currentUser {
  227. return self.authFlow.googleUser;
  228. }
  229. @end