Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 74 additions & 36 deletions GoogleSignIn/Sources/GIDGoogleUser.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021 Google LLC
// 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.
Expand All @@ -20,15 +20,14 @@

#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
#import "GoogleSignIn/Sources/GIDProfileData_Private.h"
#import "GoogleSignIn/Sources/GIDToken_Private.h"

#ifdef SWIFT_PACKAGE
@import AppAuth;
#else
#import <AppAuth/AppAuth.h>
#endif

NS_ASSUME_NONNULL_BEGIN

// The ID Token claim key for the hosted domain value.
static NSString *const kHostedDomainIDTokenClaimKey = @"hd";

Expand All @@ -41,15 +40,21 @@
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 *idToken = [self idToken];
if (idToken) {
OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken];
NSString *idTokenString = self.idToken.tokenString;
if (idTokenString) {
OIDIDToken *idTokenDecoded =
[[OIDIDToken alloc] initWithIDTokenString:idTokenString];
if (idTokenDecoded && idTokenDecoded.subject) {
return [idTokenDecoded.subject copy];
}
Expand Down Expand Up @@ -80,16 +85,56 @@ - (GIDConfiguration *)configuration {
@synchronized(self) {
// Caches the configuration since it would not change for one GIDGoogleUser instance.
if (!_cachedConfiguration) {
_cachedConfiguration = [[GIDConfiguration alloc] initWithClientID:[self clientID]
serverClientID:[self serverClientID]
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:[self openIDRealm]];
openIDRealm:openIDRealm];
};
}

return _cachedConfiguration;
}

- (GIDToken *)accessToken {
@synchronized(self) {
if (!_cachedAccessToken) {
_cachedAccessToken = [[GIDToken alloc] initWithTokenString:_authState.lastTokenResponse.accessToken
expirationDate:_authState.lastTokenResponse.
accessTokenExpirationDate];
}
}
return _cachedAccessToken;
}

- (GIDToken *)refreshToken {
@synchronized(self) {
if (!_cachedRefreshToken) {
_cachedRefreshToken = [[GIDToken alloc] initWithTokenString:_authState.refreshToken
expirationDate:nil];
}
}
return _cachedRefreshToken;
}

- (nullable GIDToken *)idToken {
@synchronized(self) {
NSString *idTokenString = _authState.lastTokenResponse.idToken;
if (!_cachedIdToken && idTokenString) {
NSDate *idTokenExpirationDate = [[[OIDIDToken alloc]
initWithIDTokenString:idTokenString] expiresAt];
_cachedIdToken = [[GIDToken alloc] initWithTokenString:idTokenString
expirationDate:idTokenExpirationDate];
}
}
return _cachedIdToken;
}

#pragma mark - Private Methods

- (instancetype)initWithAuthState:(OIDAuthState *)authState
Expand All @@ -103,40 +148,31 @@ - (instancetype)initWithAuthState:(OIDAuthState *)authState

- (void)updateAuthState:(OIDAuthState *)authState
profileData:(nullable GIDProfileData *)profileData {
_authState = authState;
_authentication = [[GIDAuthentication alloc] initWithAuthState:authState];
_profile = profileData;
@synchronized(self) {
_authState = authState;
_authentication = [[GIDAuthentication alloc] initWithAuthState:authState];
_profile = profileData;

// These three tokens will be generated in the getter and cached .
_cachedAccessToken = nil;
_cachedRefreshToken = nil;
_cachedIdToken = nil;
}
}

#pragma mark - Helpers

- (NSString *)clientID {
return _authState.lastAuthorizationResponse.request.clientID;
}

- (nullable NSString *)hostedDomain {
NSString *idToken = [self idToken];
if (idToken) {
OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken];
NSString *idTokenString = self.idToken.tokenString;
if (idTokenString) {
OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idTokenString];
if (idTokenDecoded && idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) {
return [idTokenDecoded.claims[kHostedDomainIDTokenClaimKey] copy];
return idTokenDecoded.claims[kHostedDomainIDTokenClaimKey];
}
}
return nil;
}

- (NSString *)idToken {
return _authState ? _authState.lastTokenResponse.idToken : nil;
}

- (nullable NSString *)serverClientID {
return [_authState.lastTokenResponse.request.additionalParameters[kAudienceParameter] copy];
}

- (nullable NSString *)openIDRealm {
return [_authState.lastTokenResponse.request.additionalParameters[kOpenIDRealmParameter] copy];
}

#pragma mark - NSSecureCoding

+ (BOOL)supportsSecureCoding {
Expand All @@ -146,15 +182,17 @@ + (BOOL)supportsSecureCoding {
- (nullable instancetype)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (self) {
_profile = [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey];
GIDProfileData *profileData =
[decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey];
OIDAuthState *authState;
if ([decoder containsValueForKey:kAuthState]) { // Current encoding
_authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthState];
authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthState];
} else { // Old encoding
GIDAuthentication *authentication = [decoder decodeObjectOfClass:[GIDAuthentication class]
forKey:kAuthenticationKey];
_authState = authentication.authState;
authState = authentication.authState;
}
_authentication = [[GIDAuthentication alloc] initWithAuthState:_authState];
[self updateAuthState:authState profileData:profileData];
}
return self;
}
Expand Down
42 changes: 40 additions & 2 deletions GoogleSignIn/Sources/GIDToken.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
static NSString *const kTokenStringKey = @"tokenString";
static NSString *const kExpirationDateKey = @"expirationDate";

NS_ASSUME_NONNULL_BEGIN

@implementation GIDToken

- (instancetype)initWithTokenString:(NSString *)tokenString
expirationDate:(NSDate *)expirationDate {
expirationDate:(nullable NSDate *)expirationDate {
self = [super init];
if (self) {
_tokenString = tokenString;
_tokenString = [tokenString copy];
_expirationDate = expirationDate;
}

Expand All @@ -55,4 +57,40 @@ - (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:_expirationDate forKey:kExpirationDateKey];
}

#pragma mark - isEqual

- (BOOL)isEqual:(nullable id)object {
if (object == nil) {
return NO;
}
if (self == object) {
return YES;
}
if (![object isKindOfClass:[GIDToken class]]) {
return NO;
}
return [self isEqualToToken:(GIDToken *)object];
}

- (BOOL)isEqualToToken:(GIDToken *)otherToken {
return [_tokenString isEqual:otherToken.tokenString] &&
[self isTheSameDate:_expirationDate with:otherToken.expirationDate];
}

// The date is nullable in GIDToken. Two `nil` dates are considered equal so
// token equality check succeeds if token strings are equal and have no expiration.
- (BOOL)isTheSameDate:(nullable NSDate *)date1
with:(nullable NSDate *)date2 {
if (!date1 && !date2) {
return YES;
}
return [date1 isEqualToDate:date2];
}

- (NSUInteger)hash {
return [self.tokenString hash] ^ [self.expirationDate hash];
}

@end

NS_ASSUME_NONNULL_END
4 changes: 2 additions & 2 deletions GoogleSignIn/Sources/GIDToken_Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDUserAuth.h"
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h"

NS_ASSUME_NONNULL_BEGIN

Expand All @@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
// @param token The token String.
// @param expirationDate The expiration date of the token.
- (instancetype)initWithTokenString:(NSString *)tokenString
expirationDate:(NSDate *)expirationDate;
expirationDate:(nullable NSDate *)expirationDate;

@end

Expand Down
15 changes: 14 additions & 1 deletion GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Google LLC
* 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.
Expand All @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN

@class GIDAuthentication;
@class GIDConfiguration;
@class GIDToken;
@class GIDProfileData;

/// This class represents a user account.
Expand All @@ -40,6 +41,18 @@ NS_ASSUME_NONNULL_BEGIN
/// The configuration that was used to sign in this user.
@property(nonatomic, readonly) GIDConfiguration *configuration;

/// The OAuth2 access token to access Google services.
@property(nonatomic, readonly) GIDToken *accessToken;

/// The OAuth2 refresh token to exchange for new access tokens.
@property(nonatomic, readonly) GIDToken *refreshToken;

/// An OpenID Connect ID token that identifies the user.
///
/// Send this token to your server to authenticate the user there. For more information on this topic,
/// see https://developers.google.com/identity/sign-in/ios/backend-auth.
@property(nonatomic, readonly, nullable) GIDToken *idToken;

@end

NS_ASSUME_NONNULL_END
5 changes: 5 additions & 0 deletions GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ NS_ASSUME_NONNULL_BEGIN
/// The estimated expiration date of the token.
@property(nonatomic, readonly, nullable) NSDate *expirationDate;

/// Check if current token is equal to another one.
///
/// @param otherToken - Another token to compare.
- (BOOL)isEqualToToken:(GIDToken *)otherToken;

/// Unsupported.
+ (instancetype)new NS_UNAVAILABLE;

Expand Down
10 changes: 8 additions & 2 deletions GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#import "GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h"

#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h"

#import "GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h"
#import "GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.h"
#import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h"
Expand All @@ -35,13 +37,17 @@ - (BOOL)isEqualToGoogleUser:(GIDGoogleUser *)other {
return [self.authentication isEqual:other.authentication] &&
[self.userID isEqual:other.userID] &&
[self.profile isEqual:other.profile] &&
[self.configuration isEqual:other.configuration];
[self.configuration isEqual:other.configuration] &&
[self.idToken isEqual:other.idToken] &&
[self.refreshToken isEqual:other.refreshToken] &&
[self.accessToken isEqual:other.accessToken];
}

// Not the hash implemention you want to use on prod, but just to match |isEqual:| here.
- (NSUInteger)hash {
return [self.authentication hash] ^ [self.userID hash] ^ [self.configuration hash] ^
[self.profile hash] ;
[self.profile hash] ^ [self.idToken hash] ^ [self.refreshToken hash] ^
[self.accessToken hash];
}

@end
7 changes: 7 additions & 0 deletions GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h"
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h"
#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h"

#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
#import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
Expand Down Expand Up @@ -53,6 +54,12 @@ - (void)testInitWithAuthState {
XCTAssertEqualObjects(user.configuration.hostedDomain, kHostedDomain);
XCTAssertEqualObjects(user.configuration.clientID, OIDAuthorizationRequestTestingClientID);
XCTAssertEqualObjects(user.profile, [GIDProfileData testInstance]);
XCTAssertEqualObjects(user.accessToken.tokenString, kAccessToken);
XCTAssertEqualObjects(user.refreshToken.tokenString, kRefreshToken);

OIDIDToken *idToken = [[OIDIDToken alloc]
initWithIDTokenString:authState.lastTokenResponse.idToken];
XCTAssertEqualObjects(user.idToken.expirationDate, [idToken expiresAt]);
}

- (void)testCoding {
Expand Down
1 change: 1 addition & 0 deletions GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ - (void)testDefaultOptions {
id presentingWindow = OCMStrictClassMock([NSWindow class]);
#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
NSString *loginHint = @"login_hint";

void (^completion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error) =
^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {};
GIDSignInInternalOptions *options =
Expand Down
9 changes: 7 additions & 2 deletions GoogleSignIn/Tests/Unit/GIDSignInTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ - (void)testShareInstance {
- (void)testRestorePreviousSignInNoRefresh_hasPreviousUser {
[[[_authorization expect] andReturn:_authState] authState];
OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse);
OCMStub([_authState refreshToken]).andReturn(kRefreshToken);

id idTokenDecoded = OCMClassMock([OIDIDToken class]);
OCMStub([idTokenDecoded alloc]).andReturn(idTokenDecoded);
Expand All @@ -412,6 +413,9 @@ - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser {
OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken);
OCMStub([_tokenResponse request]).andReturn(_tokenRequest);
OCMStub([_tokenRequest additionalParameters]).andReturn(nil);
OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken);
OCMStub([_tokenResponse accessTokenExpirationDate]).andReturn(nil);


[_signIn restorePreviousSignInNoRefresh];

Expand Down Expand Up @@ -1228,8 +1232,9 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
}
} else {
XCTestExpectation *expectation = [self expectationWithDescription:@"Callback called"];
void (^completion)(GIDUserAuth *_Nullable userAuth, NSError *_Nullable error) =
^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {
GIDUserAuthCompletion completion =
^(GIDUserAuth *_Nullable userAuth, NSError * _Nullable error) {

[expectation fulfill];
if (userAuth) {
XCTAssertEqualObjects(userAuth.serverAuthCode, kServerAuthCode);
Expand Down
Loading