diff --git a/CHANGELOG.md b/CHANGELOG.md index 180da5dd..37c4d352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- Add `GIDSignIn.wrapperIdentifier` so SDKs that embed Google Sign-In can self-identify in Google's diagnostic logs via a new `gidwrapper` parameter. It is opt-in and default behavior is unchanged. + # 9.2.0 - Expose the refresh token expiration date ([#577](https://github.com/google/GoogleSignIn-iOS/pull/577)) - Support requesting the `amr` (Authentication Methods References) claim ([#600](https://github.com/google/GoogleSignIn-iOS/pull/600)) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index f910e72d..abbb803e 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -649,6 +649,14 @@ + (GIDSignIn *)sharedInstance { return sharedInstance; } ++ (nullable NSString *)wrapperIdentifier { + return [GIDSignInPreferences wrapperIdentifier]; +} + ++ (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { + [GIDSignInPreferences setWrapperIdentifier:wrapperIdentifier]; +} + #pragma mark - Configuring and pre-warming #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.h b/GoogleSignIn/Sources/GIDSignInPreferences.h index 5bf45ec1..6e2b4d82 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.h +++ b/GoogleSignIn/Sources/GIDSignInPreferences.h @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN extern NSString *const kSDKVersionLoggingParameter; extern NSString *const kEnvironmentLoggingParameter; +extern NSString *const kSDKWrapperLoggingParameter; @interface GIDSignInPreferences : NSObject @@ -30,7 +31,18 @@ extern NSString *const kEnvironmentLoggingParameter; /// Returns the current Apple execution environment, such as `ios` or `macos`. + (NSString *)environment; -/// Returns the standard logging parameters to send with requests to Google's servers. +/// Returns the current SDK wrapper identifier, or `nil` if none has been accepted. ++ (nullable NSString *)wrapperIdentifier; + +/// Sets the SDK wrapper identifier. +/// See `GIDSignIn.wrapperIdentifier` for additional information, including formatting rules. ++ (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier; + +/// Clears any stored SDK wrapper identifier. Thread-safe. ++ (void)resetWrapperIdentifier; + +/// Returns the standard logging parameters sent with requests to Google's servers: the SDK +/// version and execution environment, plus the wrapper identifier when one is set. + (NSDictionary *)loggingParameters; + (NSString *)googleAuthorizationServer; diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.m b/GoogleSignIn/Sources/GIDSignInPreferences.m index 022a76e7..2b1de6ca 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.m +++ b/GoogleSignIn/Sources/GIDSignInPreferences.m @@ -14,6 +14,8 @@ #import "GoogleSignIn/Sources/GIDSignInPreferences.h" +#import + NS_ASSUME_NONNULL_BEGIN static NSString *const kLSOServer = @"accounts.google.com"; @@ -26,6 +28,12 @@ // The name of the query parameter used for logging the Apple execution environment. NSString *const kEnvironmentLoggingParameter = @"gidenv"; +// The name of the query parameter used for logging the SDK wrapper. +NSString *const kSDKWrapperLoggingParameter = @"gidwrapper"; + +static NSString *gWrapperIdentifier = nil; +static os_unfair_lock gWrapperIdentifierLock = OS_UNFAIR_LOCK_INIT; + // Supported Apple execution environments static NSString *const kAppleEnvironmentUnknown = @"unknown"; static NSString *const kAppleEnvironmentIOS = @"ios"; @@ -44,6 +52,36 @@ #define STR(x) STR_EXPAND(x) #define STR_EXPAND(x) #x +// Enforces the format documented on `GIDSignIn.wrapperIdentifier`: returns the accepted value, +// or nil if `candidate` must be rejected. `candidate` is non-nil. +static NSString * _Nullable GIDSanitizedWrapperIdentifier(NSString *candidate) { + static NSCharacterSet *allowedSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Printable ASCII is U+0020 through U+007E: length 0x5F starting at 0x20 (this is a + // length, not an end index). + // Note that, if this set changes, the substringToIndex: in the truncation + // may also need to change. + allowedSet = [NSCharacterSet characterSetWithRange:NSMakeRange(0x20, 0x5F)]; + }); + + // The whole value is validated before truncating. + if ([candidate rangeOfCharacterFromSet:[allowedSet invertedSet]].location != NSNotFound) { + return nil; + } + + if (candidate.length == 0) { + return nil; + } + + if (candidate.length > 100) { + // This cannot split a surrogate pair because each value is single-unit ASCII. + return [candidate substringToIndex:100]; + } + + return candidate; +} + @implementation GIDSignInPreferences + (NSString *)sdkVersion { @@ -79,11 +117,55 @@ + (NSString *)environment { return appleEnvironment; } ++ (nullable NSString *)wrapperIdentifier { + os_unfair_lock_lock(&gWrapperIdentifierLock); + NSString *wrapper = [gWrapperIdentifier copy]; + os_unfair_lock_unlock(&gWrapperIdentifierLock); + return wrapper; +} + ++ (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { + if (wrapperIdentifier == nil) { + return; + } + + NSString *sanitized = GIDSanitizedWrapperIdentifier(wrapperIdentifier); + if (sanitized == nil) { + NSLog(@"[Google Sign-In iOS]: the SDK wrapper identifier '%@' was rejected, because it must be " + "non-empty and contain only printable ASCII characters (U+0020 to U+007E).", + wrapperIdentifier); + return; + } + + os_unfair_lock_lock(&gWrapperIdentifierLock); + NSString *current = gWrapperIdentifier; + if (current != nil && ![current isEqualToString:sanitized]) { + os_unfair_lock_unlock(&gWrapperIdentifierLock); + NSLog(@"[Google Sign-In iOS]: the SDK wrapper identifier is already set to '%@', so '%@' was " + "ignored; more than one wrapper appears to be present.", current, sanitized); + return; + } + gWrapperIdentifier = [sanitized copy]; + os_unfair_lock_unlock(&gWrapperIdentifierLock); +} + ++ (void)resetWrapperIdentifier { + os_unfair_lock_lock(&gWrapperIdentifierLock); + gWrapperIdentifier = nil; + os_unfair_lock_unlock(&gWrapperIdentifierLock); +} + + (NSDictionary *)loggingParameters { - return @{ + NSMutableDictionary *parameters = [@{ kSDKVersionLoggingParameter : [self sdkVersion], - kEnvironmentLoggingParameter : [self environment], - }; + kEnvironmentLoggingParameter : [self environment] + } mutableCopy]; + + NSString *wrapperIdentifier = [self wrapperIdentifier]; + if (wrapperIdentifier) { + parameters[kSDKWrapperLoggingParameter] = wrapperIdentifier; + } + return [parameters copy]; } + (NSString *)googleAuthorizationServer { diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index a6b95ead..7e5ec0be 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -73,6 +73,22 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { /// The active configuration for this instance of `GIDSignIn`. @property(nonatomic, nullable) GIDConfiguration *configuration; +/// An optional identifier naming the SDK or wrapper that embeds Google Sign-In, reported to +/// Google as a diagnostic parameter for aggregate metrics only; it is never used for +/// authentication or authorization. +/// +/// Format: +/// * 1 to 100 printable ASCII characters (U+0020 to U+007E). +/// * Invalid values, including `nil`, will be dropped or truncated. +/// +/// Policy: +/// * Choose one stable name and keep it identical across your releases. +/// * As the value is case- and whitespace-sensitive, we suggest a lowercase value with no +/// spaces. +/// +/// Set this once, before the first sign-in call. The property is write-once. +@property(class, nonatomic, nullable, copy) NSString *wrapperIdentifier; + #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST /// Configures `GIDSignIn` for use. diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m index dec1caaf..0948a59d 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m @@ -23,6 +23,7 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" +#import "GoogleSignIn/Sources/GIDSignInPreferences.h" #import "GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h" #import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h" #import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" @@ -67,10 +68,13 @@ @interface GIDGoogleUserTest : XCTestCase @implementation GIDGoogleUserTest { // The saved token fetch handler. OIDTokenCallback _tokenFetchHandler; + // The saved token request. + OIDTokenRequest *_savedTokenRequest; } - (void)setUp { _tokenFetchHandler = nil; + _savedTokenRequest = nil; // We need to use swizzle here because OCMock can not stub class method with arguments. [GULSwizzler swizzleClass:[OIDAuthorizationService class] @@ -80,7 +84,8 @@ - (void)setUp { OIDTokenRequest *request, OIDAuthorizationResponse *authorizationResponse, OIDTokenCallback callback) { - // Save the OIDTokenCallback. + // Save the OIDTokenRequest and OIDTokenCallback. + self->_savedTokenRequest = request; self->_tokenFetchHandler = [callback copy]; }]; } @@ -89,6 +94,7 @@ - (void)tearDown { [GULSwizzler unswizzleClass:[OIDAuthorizationService class] selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:) isClassSelector:YES]; + [GIDSignInPreferences resetWrapperIdentifier]; } #pragma mark - Tests @@ -478,6 +484,95 @@ - (void)testRefreshTokensIfNeededWithCompletion_noRefresh_givenRefreshTokenExpir [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testWrapperIdentifier_PresentOnRefreshRequestWhenSet { + GIDSignIn.wrapperIdentifier = @"firebase"; + + // Both tokens expired 10 seconds ago. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + }]; + + XCTAssertEqualObjects(_savedTokenRequest.additionalParameters[@"gidwrapper"], @"firebase"); + + // Clean up the handler by providing a fake response to fulfill any internal state. + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:_savedTokenRequest]; + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testWrapperIdentifier_AbsentOnRefreshRequestWhenUnset { + [GIDSignInPreferences resetWrapperIdentifier]; + + // Both tokens expired 10 seconds ago. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + }]; + + XCTAssertNil(_savedTokenRequest.additionalParameters[@"gidwrapper"]); + + // Clean up the handler by providing a fake response. + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:_savedTokenRequest]; + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testWrapperIdentifier_AbsentOnRefreshRequestWhenDropped { + // Assert that attempting to set a dropped identifier is ignored. + XCTAssertNoThrow(GIDSignIn.wrapperIdentifier = @"firebasé"); + + // The rejection leaves the store nil. + XCTAssertNil(GIDSignIn.wrapperIdentifier); + + // Both tokens expired 10 seconds ago. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + }]; + + // Assert the captured token request additionalParameters does NOT contain key @"gidwrapper". + XCTAssertNil(_savedTokenRequest.additionalParameters[@"gidwrapper"]); + + // Assert it DOES contain kSDKVersionLoggingParameter and kEnvironmentLoggingParameter. + XCTAssertEqualObjects(_savedTokenRequest.additionalParameters[kSDKVersionLoggingParameter], + [GIDSignInPreferences sdkVersion]); + XCTAssertEqualObjects(_savedTokenRequest.additionalParameters[kEnvironmentLoggingParameter], + [GIDSignInPreferences environment]); + + // Clean up the handler by providing a fake response. + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:_savedTokenRequest]; + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + # pragma mark - Test `addScopes:` - (void)testAddScopes_success { diff --git a/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m b/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m index 3dff02e4..4d96d0fd 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m @@ -21,6 +21,11 @@ @interface GIDSignInPreferencesTest : XCTestCase @implementation GIDSignInPreferencesTest +- (void)tearDown { + [GIDSignInPreferences resetWrapperIdentifier]; + [super tearDown]; +} + - (void)testSDKVersion { NSString *version = [GIDSignInPreferences sdkVersion]; XCTAssertTrue([version hasPrefix:@"gid-"]); @@ -54,4 +59,141 @@ - (void)testLoggingParameters { [GIDSignInPreferences environment]); } +// Test that logging parameters include the wrapper identifier when set. +- (void)testLoggingParameters_includesWrapperWhenSet { + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + NSDictionary *params = [GIDSignInPreferences loggingParameters]; + + XCTAssertEqual(params.count, (NSUInteger)3); + XCTAssertEqualObjects(params[kSDKWrapperLoggingParameter], @"firebase"); + XCTAssertEqualObjects(params[kSDKVersionLoggingParameter], + [GIDSignInPreferences sdkVersion]); + XCTAssertEqualObjects(params[kEnvironmentLoggingParameter], + [GIDSignInPreferences environment]); +} + +- (void)testWrapperIdentifier_UnsetIsNil { + // Test that when no identifier is set, nil is returned. + [GIDSignInPreferences resetWrapperIdentifier]; + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_AcceptsSimpleValue { + // Test that a simple lowercase alphanumeric identifier is accepted. + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); +} + +- (void)testWrapperIdentifier_AcceptsHyphenatedValue { + // Test that a value with internal hyphens is accepted. + [GIDSignInPreferences setWrapperIdentifier:@"react-native"]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"react-native"); +} + +- (void)testWrapperIdentifier_AcceptsDigits { + // Test that a value with digits is accepted. + [GIDSignInPreferences setWrapperIdentifier:@"wrapper2"]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"wrapper2"); +} + +- (void)testWrapperIdentifier_AcceptsMaximumLength { + // Test that a 100-character valid identifier is accepted and not truncated. + NSString *maxLength = [@"a" stringByPaddingToLength:100 withString:@"a" startingAtIndex:0]; + [GIDSignInPreferences setWrapperIdentifier:maxLength]; + XCTAssertEqual([GIDSignInPreferences wrapperIdentifier].length, (NSUInteger)100); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], maxLength); +} + +- (void)testWrapperIdentifier_AcceptsMixedCaseSpacesAndPunctuation { + // Test that mixed case, spaces and punctuation are accepted. + NSString *value = @"React Native SDK (v2.0)"; + [GIDSignInPreferences setWrapperIdentifier:value]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], value); +} + +- (void)testWrapperIdentifier_DropsEmptyString { + // Test that an empty string is dropped. + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@""]); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_FirstValidWriteWins { + // Test that the first valid write is persistent and subsequent differing writes are rejected. + [GIDSignInPreferences setWrapperIdentifier:@"first"]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"second"]); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"first"); +} + +- (void)testWrapperIdentifier_RepeatedIdenticalWriteIsAccepted { + // Test that writing the same valid value again does not throw or change the state. + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"firebase"]); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); +} + +- (void)testWrapperIdentifier_NilIsIgnored { + // Test that passing nil is ignored and does not reset the store. + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + [GIDSignInPreferences setWrapperIdentifier:nil]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); + + [GIDSignInPreferences setWrapperIdentifier:@"second"]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); +} + +- (void)testWrapperIdentifier_DroppedWriteLeavesPreviousValue { + // Test that a dropped write does not clear or change a previously set valid value. + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"firebasé"]); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); +} + +- (void)testWrapperIdentifier_TruncatesOverLongValue { + // Test that a legal string longer than 100 characters is truncated to its first 100 characters. + NSString *overLong = [@"a" stringByPaddingToLength:150 withString:@"a" startingAtIndex:0]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:overLong]); + XCTAssertEqual([GIDSignInPreferences wrapperIdentifier].length, (NSUInteger)100); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], [overLong substringToIndex:100]); +} + +- (void)testWrapperIdentifier_DropsNonASCII { + // Test that a value containing a non-ASCII character is ignored and leaves the store nil. + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"firebasé"]); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_DropsControlCharacters { + // Test that values containing ASCII control characters are ignored and leave the store nil. + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"fire\nbase"]); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); + + [GIDSignInPreferences resetWrapperIdentifier]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"fire\tbase"]); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); + + [GIDSignInPreferences resetWrapperIdentifier]; + NSString *del = [NSString stringWithFormat:@"fire%Cbase", (unichar)0x7F]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:del]); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_DropsWhenDisallowedCharacterIsPastTruncationPoint { + // Test that the drop check deliberately runs on the untruncated string so a payload hidden + // past the truncation point cannot survive. + NSString *prefix = [@"a" stringByPaddingToLength:120 withString:@"a" startingAtIndex:0]; + NSString *overLong = [prefix stringByReplacingCharactersInRange:NSMakeRange(110, 1) + withString:@"é"]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:overLong]); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_WriteOnceComparesSanitizedValue { + // Test that the write-once check compares the sanitized value, allowing a repeated + // write of a value that truncates to the same result. + NSString *overLong = [@"a" stringByPaddingToLength:150 withString:@"a" startingAtIndex:0]; + [GIDSignInPreferences setWrapperIdentifier:overLong]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:overLong]); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], [overLong substringToIndex:100]); +} + @end diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index a98cdf25..ed959f12 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -389,6 +389,7 @@ - (void)tearDown { [_testUserDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; [_fakeMainBundle stopFaking]; + [GIDSignInPreferences resetWrapperIdentifier]; [super tearDown]; } @@ -1168,6 +1169,120 @@ - (void)testOAuthLogin_HostedDomain { XCTAssertEqualObjects(params[@"hd"], kHostedDomain, @"hosted domain should match"); } +- (void)testWrapperIdentifier_PresentInAuthorizationRequestWhenSet { + GIDSignIn.wrapperIdentifier = @"firebase"; + OCMStub( + [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] + ).andDo(^(NSInvocation *invocation) { + self->_keychainSaved = self->_saveAuthorizationReturnValue; + }); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + claimsAsJSONRequired:NO + keychainError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO]; + + NSDictionary *params = _savedAuthorizationRequest.additionalParameters; + XCTAssertEqualObjects(params[@"gidwrapper"], @"firebase", + @"The authorization request should contain the 'gidwrapper' parameter " + "when set."); +} + +- (void)testWrapperIdentifier_AbsentFromAuthorizationRequestWhenUnset { + [GIDSignInPreferences resetWrapperIdentifier]; + OCMStub( + [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] + ).andDo(^(NSInvocation *invocation) { + self->_keychainSaved = self->_saveAuthorizationReturnValue; + }); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + claimsAsJSONRequired:NO + keychainError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO]; + + NSDictionary *params = _savedAuthorizationRequest.additionalParameters; + XCTAssertNil(params[@"gidwrapper"], + @"The authorization request should not contain the 'gidwrapper' parameter " + "when unset."); +} + +- (void)testWrapperIdentifier_DroppedValueIsIgnored { + XCTAssertNoThrow(GIDSignIn.wrapperIdentifier = @"firebasé", + @"Setting a dropped wrapper identifier should be ignored."); + XCTAssertNil(GIDSignIn.wrapperIdentifier, + @"The wrapper identifier should be nil after a dropped assignment."); +} + +- (void)testWrapperIdentifier_PresentOnRevokeURL { + GIDSignIn.wrapperIdentifier = @"my-sdk"; + + [[[_authorization expect] andReturn:_authState] authState]; + [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; + [[[_tokenResponse expect] andReturn:kAccessToken] accessToken]; + [[[_authorization expect] andReturn:_fetcherService] fetcherService]; + + [_signIn disconnectWithCompletion:nil]; + + XCTAssertTrue([self isFetcherStarted], @"should start fetching"); + NSURL *url = [self fetchedURL]; + NSURLComponents *components = [NSURLComponents componentsWithURL:url + resolvingAgainstBaseURL:NO]; + NSArray *queryItems = components.queryItems; + + XCTAssertEqualObjects([self valueForQueryItemName:@"gidwrapper" inArray:queryItems], + @"my-sdk", @"The revoke URL should contain the 'gidwrapper' parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:kSDKVersionLoggingParameter inArray:queryItems], + [GIDSignInPreferences sdkVersion], + @"The revoke URL should contain the SDK version parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:kEnvironmentLoggingParameter + inArray:queryItems], + [GIDSignInPreferences environment], + @"The revoke URL should contain the environment parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:@"token" inArray:queryItems], + kAccessToken, @"The revoke URL should contain the 'token' parameter."); +} + +- (void)testWrapperIdentifier_AbsentFromRevokeURLWhenUnset { + [GIDSignInPreferences resetWrapperIdentifier]; + + [[[_authorization expect] andReturn:_authState] authState]; + [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; + [[[_tokenResponse expect] andReturn:kAccessToken] accessToken]; + [[[_authorization expect] andReturn:_fetcherService] fetcherService]; + + [_signIn disconnectWithCompletion:nil]; + + XCTAssertTrue([self isFetcherStarted], @"should start fetching"); + NSURL *url = [self fetchedURL]; + NSURLComponents *components = [NSURLComponents componentsWithURL:url + resolvingAgainstBaseURL:NO]; + NSArray *queryItems = components.queryItems; + + XCTAssertNil([self valueForQueryItemName:@"gidwrapper" inArray:queryItems], + @"The revoke URL should not contain the 'gidwrapper' parameter when unset."); + XCTAssertEqualObjects([self valueForQueryItemName:kSDKVersionLoggingParameter inArray:queryItems], + [GIDSignInPreferences sdkVersion], + @"The revoke URL should still contain the SDK version parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:kEnvironmentLoggingParameter + inArray:queryItems], + [GIDSignInPreferences environment], + @"The revoke URL should still contain the environment parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:@"token" inArray:queryItems], + kAccessToken, + @"The revoke URL should still contain the 'token' parameter."); +} + - (void)testOAuthLogin_ConsentCanceled { [self OAuthLoginWithAddScopesFlow:NO authError:@"access_denied" @@ -1779,6 +1894,17 @@ - (void)testTokenEndpointEMMError { #pragma mark - Helpers +// Returns the value for the query item with the given name in the array of query items. +- (nullable NSString *)valueForQueryItemName:(NSString *)name + inArray:(NSArray *)queryItems { + for (NSURLQueryItem *item in queryItems) { + if ([item.name isEqualToString:name]) { + return item.value; + } + } + return nil; +} + // Whether or not a fetcher has been started. - (BOOL)isFetcherStarted { NSUInteger count = _fetcherService.fetchers.count;