Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
8 changes: 8 additions & 0 deletions GoogleSignIn/Sources/GIDSignIn.m
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,14 @@ + (GIDSignIn *)sharedInstance {
return sharedInstance;
}

+ (nullable NSString *)wrapperIdentifier {
return [GIDSignInPreferences wrapperIdentifier];
}

+ (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier {
[GIDSignInPreferences setWrapperIdentifier:wrapperIdentifier];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to enforce the "set once" language in property's doc comment?

}

#pragma mark - Configuring and pre-warming

#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
Expand Down
14 changes: 13 additions & 1 deletion GoogleSignIn/Sources/GIDSignInPreferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN

extern NSString *const kSDKVersionLoggingParameter;
extern NSString *const kEnvironmentLoggingParameter;
extern NSString *const kSDKWrapperLoggingParameter;

@interface GIDSignInPreferences : NSObject

Expand All @@ -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<NSString *, NSString *> *)loggingParameters;

+ (NSString *)googleAuthorizationServer;
Expand Down
88 changes: 85 additions & 3 deletions GoogleSignIn/Sources/GIDSignInPreferences.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#import "GoogleSignIn/Sources/GIDSignInPreferences.h"

#import <os/lock.h>

NS_ASSUME_NONNULL_BEGIN

static NSString *const kLSOServer = @"accounts.google.com";
Expand All @@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -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]) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the "set once" enforcement is here. That's a little surprising, but I think I understand the reasoning. I don't know if this can be addressed, but I do still feel like it's confusing to see the doc comment on the property of the other class, and then for the actual enforcement to be here. ¯_(ツ)_/¯

Probably no big deal. Feel free to close.

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<NSString *, NSString *> *)loggingParameters {
return @{
NSMutableDictionary<NSString *, NSString *> *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 {
Expand Down
16 changes: 16 additions & 0 deletions GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
97 changes: 96 additions & 1 deletion GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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]
Expand All @@ -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];
}];
}
Expand All @@ -89,6 +94,7 @@ - (void)tearDown {
[GULSwizzler unswizzleClass:[OIDAuthorizationService class]
selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
isClassSelector:YES];
[GIDSignInPreferences resetWrapperIdentifier];
}

#pragma mark - Tests
Expand Down Expand Up @@ -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"];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the description more descriptive? I know the pattern in the library is to use "Callback is called," but I've had trouble in past with debugging failed tests when I see a wall of that output. It's hard to find which callback wasn't 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 {
Expand Down
Loading
Loading