diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 82a8b039..8de10d71 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -80,11 +80,20 @@ // 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 URL template for the URL to revoke the token. -static NSString *const kRevokeTokenURLTemplate = @"https://%@/o/oauth2/revoke?token=%@"; +// 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"; + +// 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 +582,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) { @@ -1135,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) { 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];