// Copyright 2022 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/Public/GoogleSignIn/GIDGoogleUser.h" #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h" #import "GoogleSignIn/Sources/GIDAuthentication.h" #import "GoogleSignIn/Sources/GIDProfileData_Private.h" #import "GoogleSignIn/Sources/GIDToken_Private.h" #ifdef SWIFT_PACKAGE @import AppAuth; #else #import #endif // The ID Token claim key for the hosted domain value. static NSString *const kHostedDomainIDTokenClaimKey = @"hd"; // Key constants used for encode and decode. static NSString *const kAuthenticationKey = @"authentication"; static NSString *const kProfileDataKey = @"profileData"; static NSString *const kAuthState = @"authState"; // Parameters for the token exchange endpoint. static NSString *const kAudienceParameter = @"audience"; static NSString *const kOpenIDRealmParameter = @"openid.realm"; NS_ASSUME_NONNULL_BEGIN @implementation GIDGoogleUser { OIDAuthState *_authState; GIDConfiguration *_cachedConfiguration; GIDToken *_cachedAccessToken; GIDToken *_cachedRefreshToken; GIDToken *_cachedIdToken; } - (nullable NSString *)userID { NSString *idTokenString = self.idToken.tokenString; if (idTokenString) { OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idTokenString]; if (idTokenDecoded && idTokenDecoded.subject) { return [idTokenDecoded.subject copy]; } } return nil; } - (nullable NSArray *)grantedScopes { NSArray *grantedScopes; NSString *grantedScopeString = _authState.lastTokenResponse.scope; if (grantedScopeString) { // If we have a 'scope' parameter from the backend, this is authoritative. // Remove leading and trailing whitespace. grantedScopeString = [grantedScopeString stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; // Tokenize with space as a delimiter. NSMutableArray *parsedScopes = [[grantedScopeString componentsSeparatedByString:@" "] mutableCopy]; // Remove empty strings. [parsedScopes removeObject:@""]; grantedScopes = [parsedScopes copy]; } return grantedScopes; } - (GIDConfiguration *)configuration { @synchronized(self) { // Caches the configuration since it would not change for one GIDGoogleUser instance. if (!_cachedConfiguration) { NSString *clientID = _authState.lastAuthorizationResponse.request.clientID; NSString *serverClientID = _authState.lastTokenResponse.request.additionalParameters[kAudienceParameter]; NSString *openIDRealm = _authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter]; _cachedConfiguration = [[GIDConfiguration alloc] initWithClientID:clientID serverClientID:serverClientID hostedDomain:[self hostedDomain] openIDRealm:openIDRealm]; }; } return _cachedConfiguration; } - (GIDToken *)accessToken { @synchronized(self) { return _cachedAccessToken; } } - (GIDToken *)refreshToken { @synchronized(self) { return _cachedRefreshToken; } } - (nullable GIDToken *)idToken { @synchronized(self) { return _cachedIdToken; } } - (id) fetcherAuthorizer { return [_authentication fetcherAuthorizer]; } - (void)doWithFreshTokens:(void (^)(GIDGoogleUser *_Nullable user, NSError *_Nullable error))completion { [_authentication doWithFreshTokens:^(OIDAuthState *authState, NSError *error) { if (authState) { NSString *accessTokenString = authState.lastTokenResponse.accessToken; NSDate *accessTokenExpirationDate = authState.lastTokenResponse.accessTokenExpirationDate; NSString *refreshTokenString = authState.refreshToken; NSString *idTokenString = authState.lastTokenResponse.idToken; NSDate *idTokenExpirationDate = [[[OIDIDToken alloc] initWithIDTokenString:idTokenString] expiresAt]; // Check if it is the current authState. if ([accessTokenString isEqual:self.accessToken.tokenString] && [self isTheSameExpirationDate:accessTokenExpirationDate with:self.accessToken.expirationDate] && [refreshTokenString isEqual:self.refreshToken.tokenString] && [idTokenString isEqual:self.idToken.tokenString] && [self isTheSameExpirationDate:idTokenExpirationDate with:self.idToken.expirationDate]) { completion(self, nil);; } else { [self updateAuthState:authState notifyTokenChanges:YES]; completion(self, nil); } } else { completion(nil, error); } }]; } #pragma mark - Private Methods - (instancetype)initWithAuthState:(OIDAuthState *)authState profileData:(nullable GIDProfileData *)profileData { self = [super init]; if (self) { [self updateAuthState:authState profileData:profileData notifyTokenChanges:NO]; } return self; } - (void)updateAuthState:(OIDAuthState *)authState profileData:(nullable GIDProfileData *)profileData notifyTokenChanges:(BOOL)notify{ _profile = profileData; [self updateAuthState:authState notifyTokenChanges:notify]; } - (void)updateAuthState:(OIDAuthState *)authState notifyTokenChanges:(BOOL)notify{ @synchronized(self) { _authState = authState; _authentication = [[GIDAuthentication alloc] initWithAuthState:authState]; if (notify) { [self willChangeValueForKey:NSStringFromSelector(@selector(accessToken))]; [self willChangeValueForKey:NSStringFromSelector(@selector(idToken))]; [self willChangeValueForKey:NSStringFromSelector(@selector(refreshToken))]; } // Update three tokens _cachedAccessToken = [[GIDToken alloc] initWithTokenString:_authState.lastTokenResponse.accessToken expirationDate:_authState.lastTokenResponse. accessTokenExpirationDate]; _cachedRefreshToken = [[GIDToken alloc] initWithTokenString:_authState.refreshToken expirationDate:nil]; _cachedIdToken = nil; NSString *idTokenString = _authState.lastTokenResponse.idToken; if (idTokenString) { NSDate *idTokenExpirationDate = [[[OIDIDToken alloc] initWithIDTokenString:idTokenString] expiresAt]; _cachedIdToken = [[GIDToken alloc] initWithTokenString:idTokenString expirationDate:idTokenExpirationDate]; } if (notify) { [self didChangeValueForKey:NSStringFromSelector(@selector(accessToken))]; [self didChangeValueForKey:NSStringFromSelector(@selector(idToken))]; [self didChangeValueForKey:NSStringFromSelector(@selector(refreshToken))]; } } } #pragma mark - Helpers - (nullable NSString *)hostedDomain { NSString *idTokenString = self.idToken.tokenString; if (idTokenString) { OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idTokenString]; if (idTokenDecoded && idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) { return idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]; } } return nil; } // The date is nullable in GIDToken. If they are both nil they are considered equal. - (BOOL)isTheSameExpirationDate:(nullable NSDate *)date1 with:(nullable NSDate *)date2 { if (!date1 && !date2) { return YES; } return [date1 isEqualToDate:date2]; } #pragma mark - NSSecureCoding + (BOOL)supportsSecureCoding { return YES; } - (nullable instancetype)initWithCoder:(NSCoder *)decoder { self = [super init]; if (self) { GIDProfileData *profileData = [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey]; OIDAuthState *authState; if ([decoder containsValueForKey:kAuthState]) { // Current encoding authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthState]; } else { // Old encoding GIDAuthentication *authentication = [decoder decodeObjectOfClass:[GIDAuthentication class] forKey:kAuthenticationKey]; authState = authentication.authState; } [self updateAuthState:authState profileData:profileData notifyTokenChanges:NO]; } return self; } - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:_profile forKey:kProfileDataKey]; [encoder encodeObject:_authState forKey:kAuthState]; } @end NS_ASSUME_NONNULL_END