From 66f32472d6fa5b39a1a747715ec8843d21a888e8 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:31:08 -0700 Subject: [PATCH 1/3] g-orchestrated: Build the revoke URL with NSURLComponents Assemble the token revocation URL from components and query items rather than by string formatting, so the token and the logging parameters are percent-encoded rather than interpolated raw into a URL string. --- GoogleSignIn/Sources/GIDSignIn.m | 37 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 82a8b039..b4b2a865 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -83,8 +83,14 @@ // The URL template for the URL to get user info. static NSString *const kUserInfoURLTemplate = @"https://%@/oauth2/v3/userinfo?access_token=%@"; -// The URL template for the URL to revoke the token. -static NSString *const kRevokeTokenURLTemplate = @"https://%@/o/oauth2/revoke?token=%@"; +// The path for the endpoint to revoke the token. +static NSString *const kRevokeTokenPath = @"/o/oauth2/revoke"; + +// The name of the query parameter carrying the token to be revoked. +static NSString *const kRevokeTokenParameter = @"token"; + +// The scheme used for requests to Google's servers. +static NSString *const kHTTPSScheme = @"https"; // Expected path in the URL scheme to be handled. static NSString *const kBrowserCallbackPath = @"/oauth2callback"; @@ -573,17 +579,22 @@ - (void)disconnectWithCompletion:(nullable GIDDisconnectCompletion)completion { } return; } - NSString *revokeURLString = [NSString stringWithFormat:kRevokeTokenURLTemplate, - [GIDSignInPreferences googleAuthorizationServer], token]; - // Append logging parameter - revokeURLString = [NSString stringWithFormat:@"%@&%@=%@&%@=%@", - revokeURLString, - kSDKVersionLoggingParameter, - [GIDSignInPreferences sdkVersion], - kEnvironmentLoggingParameter, - [GIDSignInPreferences environment]]; - NSURL *revokeURL = [NSURL URLWithString:revokeURLString]; - [self startFetchURL:revokeURL + NSURLComponents *revokeURLComponents = [[NSURLComponents alloc] init]; + revokeURLComponents.scheme = kHTTPSScheme; + revokeURLComponents.host = [GIDSignInPreferences googleAuthorizationServer]; + revokeURLComponents.path = kRevokeTokenPath; + + NSMutableArray *queryItems = [NSMutableArray array]; + [queryItems addObject:[NSURLQueryItem queryItemWithName:kRevokeTokenParameter value:token]]; + NSDictionary *loggingParameters = + [GIDSignInPreferences loggingParameters]; + for (NSString *name in [loggingParameters.allKeys sortedArrayUsingSelector:@selector(compare:)]) { + [queryItems addObject:[NSURLQueryItem queryItemWithName:name + value:loggingParameters[name]]]; + } + revokeURLComponents.queryItems = queryItems; + + [self startFetchURL:revokeURLComponents.URL fromAuthState:authState withComment:@"GIDSignIn: revoke tokens" withCompletionHandler:^(NSData *data, NSError *error) { From ba08b36912d06ca07c27545e7efb056c6cced87d Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:56:30 -0700 Subject: [PATCH 2/3] g-orchestrated: Build the user info URL with NSURLComponents Assemble the user info URL from components and query items so the access token is percent-encoded rather than interpolated raw into a URL string, matching the revoke URL construction. --- GoogleSignIn/Sources/GIDSignIn.m | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index b4b2a865..8de10d71 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -80,8 +80,11 @@ // The URL template for the token endpoint. static NSString *const kTokenURLTemplate = @"https://%@/token"; -// The URL template for the URL to get user info. -static NSString *const kUserInfoURLTemplate = @"https://%@/oauth2/v3/userinfo?access_token=%@"; +// The path for the endpoint to get user info. +static NSString *const kUserInfoPath = @"/oauth2/v3/userinfo"; + +// The name of the query parameter carrying the access token for the user info request. +static NSString *const kAccessTokenParameter = @"access_token"; // The path for the endpoint to revoke the token. static NSString *const kRevokeTokenPath = @"/o/oauth2/revoke"; @@ -1146,11 +1149,15 @@ - (void)addDecodeIdTokenCallback:(GIDAuthFlow *)authFlow { // If we can't retrieve profile data from the ID token, make a userInfo request to fetch them. if (!handlerAuthFlow.profileData) { [handlerAuthFlow wait]; - NSURL *infoURL = [NSURL URLWithString: - [NSString stringWithFormat:kUserInfoURLTemplate, - [GIDSignInPreferences googleUserInfoServer], - authState.lastTokenResponse.accessToken]]; - [self startFetchURL:infoURL + NSURLComponents *infoURLComponents = [[NSURLComponents alloc] init]; + infoURLComponents.scheme = kHTTPSScheme; + infoURLComponents.host = [GIDSignInPreferences googleUserInfoServer]; + infoURLComponents.path = kUserInfoPath; + infoURLComponents.queryItems = @[ + [NSURLQueryItem queryItemWithName:kAccessTokenParameter + value:authState.lastTokenResponse.accessToken], + ]; + [self startFetchURL:infoURLComponents.URL fromAuthState:authState withComment:@"GIDSignIn: fetch basic profile info" withCompletionHandler:^(NSData *data, NSError *error) { From a6c69021e44c8bada6e49bfd2853c75f008da867 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:06:23 -0700 Subject: [PATCH 3/3] g-orchestrated: Test that reserved characters in a token are encoded Revoke a token containing "&", "=" and "#" and assert it round-trips through the revoke URL intact, along with the logging parameters. Against the previous string-formatted URL this fails: the token is truncated at the "&" and both logging parameters are lost to the fragment. --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 36cb3047..8e4b8259 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -1349,6 +1349,23 @@ - (void)testDisconnectNoCallback_accessToken { [_tokenResponse verify]; } +// Verifies a token containing characters that are reserved in a URL query is percent-encoded +// in the revoke URL, so that it arrives at the server intact. +- (void)testDisconnectNoCallback_tokenWithReservedCharacters { + NSString *tokenWithReservedCharacters = @"token&with=reserved#characters"; + [[[_authorization expect] andReturn:_authState] authState]; + [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; + [[[_tokenResponse expect] andReturn:tokenWithReservedCharacters] accessToken]; + [[[_authorization expect] andReturn:_fetcherService] fetcherService]; + [_signIn disconnectWithCompletion:nil]; + [self verifyAndRevokeToken:tokenWithReservedCharacters + hasCallback:NO + waitingForExpectations:@[]]; + [_authorization verify]; + [_authState verify]; + [_tokenResponse verify]; +} + // Verifies disconnect calls callback with no errors if refresh token is present. - (void)testDisconnect_refreshToken { [[[_authorization expect] andReturn:_authState] authState];