Skip to content

Commit

Permalink
feat: allow providing custom nonce (#402)
Browse files Browse the repository at this point in the history
  • Loading branch information
vonovak authored Sep 13, 2024
1 parent 65fb3f1 commit b34f5a9
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 24 deletions.
34 changes: 30 additions & 4 deletions GoogleSignIn/Sources/GIDSignIn.m
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,25 @@ - (void)signInWithPresentingViewController:(UIViewController *)presentingViewCon
hint:(nullable NSString *)hint
additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
completion:(nullable GIDSignInCompletion)completion {
[self signInWithPresentingViewController:presentingViewController
hint:hint
additionalScopes:additionalScopes
nonce:nil
completion:completion];
}

- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
hint:(nullable NSString *)hint
additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
nonce:(nullable NSString *)nonce
completion:(nullable GIDSignInCompletion)completion {
GIDSignInInternalOptions *options =
[GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
presentingViewController:presentingViewController
loginHint:hint
addScopesFlow:NO
scopes:additionalScopes
nonce:nonce
completion:completion];
[self signInWithOptions:options];
}
Expand Down Expand Up @@ -350,12 +363,25 @@ - (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
hint:(nullable NSString *)hint
additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
completion:(nullable GIDSignInCompletion)completion {
[self signInWithPresentingWindow:presentingWindow
hint:hint
additionalScopes:additionalScopes
nonce:nil
completion:completion];
}

- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
hint:(nullable NSString *)hint
additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
nonce:(nullable NSString *)nonce
completion:(nullable GIDSignInCompletion)completion {
GIDSignInInternalOptions *options =
[GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
presentingWindow:presentingWindow
loginHint:hint
addScopesFlow:NO
scopes:additionalScopes
nonce:nonce
completion:completion];
[self signInWithOptions:options];
}
Expand Down Expand Up @@ -573,7 +599,7 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options {
if (!_configuration) {
// NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
[NSException raise:NSInvalidArgumentException
format:@"No active configuration. Make sure GIDClientID is set in Info.plist."];
format:@"No active configuration. Make sure GIDClientID is set in Info.plist."];
return;
}

Expand Down Expand Up @@ -667,7 +693,6 @@ - (void)authorizationRequestWithOptions:(GIDSignInInternalOptions *)options comp
[_timedLoader startTiming];
[self->_appCheck getLimitedUseTokenWithCompletion:^(GACAppCheckToken * _Nullable token,
NSError * _Nullable error) {
OIDAuthorizationRequest *request = nil;
if (token) {
additionalParameters[kClientAssertionTypeParameter] = kClientAssertionTypeParameterValue;
additionalParameters[kClientAssertionParameter] = token.token;
Expand All @@ -677,7 +702,7 @@ - (void)authorizationRequestWithOptions:(GIDSignInInternalOptions *)options comp
NSLog(@"[Google Sign-In iOS]: Error retrieving App Check limited use token: %@", error);
}
#endif
request = [self authorizationRequestWithOptions:options
OIDAuthorizationRequest *request = [self authorizationRequestWithOptions:options
additionalParameters:additionalParameters];
if (self->_timedLoader.animationStatus == GIDTimedLoaderAnimationStatusAnimating) {
[self->_timedLoader stopTimingWithCompletion:^{
Expand Down Expand Up @@ -707,6 +732,7 @@ - (void)authorizationRequestWithOptions:(GIDSignInInternalOptions *)options comp
scopes:options.scopes
redirectURL:[self redirectURLWithOptions:options]
responseType:OIDResponseTypeCode
nonce:options.nonce
additionalParameters:additionalParameters];
return request;
}
Expand Down Expand Up @@ -758,7 +784,7 @@ - (NSURL *)redirectURLWithOptions:(GIDSignInInternalOptions *)options {

- (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse
error:(NSError *)error
emmSupport:(NSString *)emmSupport{
emmSupport:(NSString *)emmSupport {
if (_restarting) {
// The auth flow is restarting, so the work here would be performed in the next round.
_restarting = NO;
Expand Down
6 changes: 6 additions & 0 deletions GoogleSignIn/Sources/GIDSignInInternalOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ NS_ASSUME_NONNULL_BEGIN
/// The login hint to be used during the flow.
@property(nonatomic, copy, nullable) NSString *loginHint;

/// A cryptographically random value used to associate a Client session with an ID Token,
/// and to mitigate replay attacks.
@property(nonatomic, readonly, copy, nullable) NSString *nonce;

/// Creates the default options.
#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+ (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
Expand All @@ -77,6 +81,7 @@ NS_ASSUME_NONNULL_BEGIN
loginHint:(nullable NSString *)loginHint
addScopesFlow:(BOOL)addScopesFlow
scopes:(nullable NSArray *)scopes
nonce:(nullable NSString *)nonce
completion:(nullable GIDSignInCompletion)completion;

#elif TARGET_OS_OSX
Expand All @@ -91,6 +96,7 @@ NS_ASSUME_NONNULL_BEGIN
loginHint:(nullable NSString *)loginHint
addScopesFlow:(BOOL)addScopesFlow
scopes:(nullable NSArray *)scopes
nonce:(nullable NSString *)nonce
completion:(nullable GIDSignInCompletion)completion;
#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST

Expand Down
4 changes: 4 additions & 0 deletions GoogleSignIn/Sources/GIDSignInInternalOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con
loginHint:(nullable NSString *)loginHint
addScopesFlow:(BOOL)addScopesFlow
scopes:(nullable NSArray *)scopes
nonce:(nullable NSString *)nonce
completion:(nullable GIDSignInCompletion)completion {
#elif TARGET_OS_OSX
+ (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
presentingWindow:(nullable NSWindow *)presentingWindow
loginHint:(nullable NSString *)loginHint
addScopesFlow:(BOOL)addScopesFlow
scopes:(nullable NSArray *)scopes
nonce:(nullable NSString *)nonce
completion:(nullable GIDSignInCompletion)completion {
#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
Expand All @@ -54,6 +56,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con
options->_loginHint = loginHint;
options->_completion = completion;
options->_scopes = [GIDScopes scopesWithBasicProfile:scopes];
options->_nonce = nonce;
}
return options;
}
Expand All @@ -80,6 +83,7 @@ + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)con
loginHint:loginHint
addScopesFlow:addScopesFlow
scopes:@[]
nonce:nil
completion:completion];
return options;
}
Expand Down
49 changes: 48 additions & 1 deletion GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,31 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.")
NSError *_Nullable error))completion
NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");


/// Starts an interactive sign-in flow on iOS using the provided hint, additional scopes, and nonce.
///
/// The completion will be called at the end of this process. Any saved sign-in state will be
/// replaced by the result of this flow. Note that this method should not be called when the app is
/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the
/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in.
///
/// @param presentingViewController The view controller used to present `SFSafariViewController` on
/// iOS 9 and 10.
/// @param hint An optional hint for the authorization server, for example the user's ID or email
/// address, to be prefilled if possible.
/// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes.
/// @param nonce A custom nonce.
/// @param completion The optional block that is called on completion. This block will
/// be called asynchronously on the main queue.
- (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
hint:(nullable NSString *)hint
additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
nonce:(nullable NSString *)nonce
completion:
(nullable void (^)(GIDSignInResult *_Nullable signInResult,
NSError *_Nullable error))completion
NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");

#elif TARGET_OS_OSX

/// Starts an interactive sign-in flow on macOS.
Expand Down Expand Up @@ -229,7 +254,7 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.")
completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult,
NSError *_Nullable error))completion;

/// Starts an interactive sign-in flow on macOS using the provided hint.
/// Starts an interactive sign-in flow on macOS using the provided hint and additional scopes.
///
/// The completion will be called at the end of this process. Any saved sign-in state will be
/// replaced by the result of this flow. Note that this method should not be called when the app is
Expand All @@ -248,6 +273,28 @@ NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.")
completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult,
NSError *_Nullable error))completion;

/// Starts an interactive sign-in flow on macOS using the provided hint, additional scopes, and nonce.
///
/// The completion will be called at the end of this process. Any saved sign-in state will be
/// replaced by the result of this flow. Note that this method should not be called when the app is
/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the
/// `restorePreviousSignInWithCompletion:` method to restore a previous sign-in.
///
/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`.
/// @param hint An optional hint for the authorization server, for example the user's ID or email
/// address, to be prefilled if possible.
/// @param additionalScopes An optional array of scopes to request in addition to the basic profile scopes.
/// @param nonce A custom nonce.
/// @param completion The optional block that is called on completion. This block will
/// be called asynchronously on the main queue.
- (void)signInWithPresentingWindow:(NSWindow *)presentingWindow
hint:(nullable NSString *)hint
additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
nonce:(nullable NSString *)nonce
completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult,
NSError *_Nullable error))completion;


#endif

@end
Expand Down
54 changes: 46 additions & 8 deletions GoogleSignIn/Tests/Unit/GIDSignInTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,7 @@ - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser {

// Mock generating a GIDConfiguration when initializing GIDGoogleUser.
OIDAuthorizationResponse *authResponse =
[OIDAuthorizationResponse testInstanceWithAdditionalParameters:nil
errorString:nil];
[OIDAuthorizationResponse testInstance];

OCMStub([_authState lastAuthorizationResponse]).andReturn(authResponse);
OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken);
Expand Down Expand Up @@ -676,7 +675,8 @@ - (void)testOAuthLogin_AdditionalScopes {
oldAccessToken:NO
modalCancel:NO
useAdditionalScopes:YES
additionalScopes:nil];
additionalScopes:nil
manualNonce:nil];

expectedScopeString = [@[ @"email", @"profile" ] componentsJoinedByString:@" "];
XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
Expand All @@ -690,7 +690,8 @@ - (void)testOAuthLogin_AdditionalScopes {
oldAccessToken:NO
modalCancel:NO
useAdditionalScopes:YES
additionalScopes:@[ kScope ]];
additionalScopes:@[ kScope ]
manualNonce:nil];

expectedScopeString = [@[ kScope, @"email", @"profile" ] componentsJoinedByString:@" "];
XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
Expand All @@ -704,7 +705,8 @@ - (void)testOAuthLogin_AdditionalScopes {
oldAccessToken:NO
modalCancel:NO
useAdditionalScopes:YES
additionalScopes:@[ kScope, kScope2 ]];
additionalScopes:@[ kScope, kScope2 ]
manualNonce:nil];

expectedScopeString = [@[ kScope, kScope2, @"email", @"profile" ] componentsJoinedByString:@" "];
XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
Expand Down Expand Up @@ -796,6 +798,37 @@ - (void)testOpenIDRealm {
XCTAssertEqual(params[kOpenIDRealmKey], kOpenIDRealm, @"OpenID Realm should match.");
}

- (void)testManualNonce {
_signIn.configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
serverClientID:nil
hostedDomain:nil
openIDRealm:kOpenIDRealm];

OCMStub(
[_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
).andDo(^(NSInvocation *invocation) {
self->_keychainSaved = self->_saveAuthorizationReturnValue;
});

NSString* manualNonce = @"manual_nonce";

[self OAuthLoginWithAddScopesFlow:NO
authError:nil
tokenError:nil
emmPasscodeInfoRequired:NO
keychainError:NO
restoredSignIn:NO
oldAccessToken:NO
modalCancel:NO
useAdditionalScopes:NO
additionalScopes:@[]
manualNonce:manualNonce];

XCTAssertEqualObjects(_savedAuthorizationRequest.nonce,
manualNonce,
@"Provided nonce should match nonce in authorization request.");
}

- (void)testOAuthLogin_LoginHint {
_hint = kUserEmail;

Expand Down Expand Up @@ -1375,7 +1408,8 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
oldAccessToken:oldAccessToken
modalCancel:modalCancel
useAdditionalScopes:NO
additionalScopes:nil];
additionalScopes:nil
manualNonce:nil];
}

// The authorization flow with parameters to control which branches to take.
Expand All @@ -1388,18 +1422,20 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
oldAccessToken:(BOOL)oldAccessToken
modalCancel:(BOOL)modalCancel
useAdditionalScopes:(BOOL)useAdditionalScopes
additionalScopes:(NSArray *)additionalScopes {
additionalScopes:(NSArray *)additionalScopes
manualNonce:(NSString *)nonce {
if (restoredSignIn) {
// clearAndAuthenticateWithOptions
[[[_authorization expect] andReturn:_authState] authState];
BOOL isAuthorized = restoredSignIn ? YES : NO;
BOOL isAuthorized = restoredSignIn;
[[[_authState expect] andReturnValue:[NSNumber numberWithBool:isAuthorized]] isAuthorized];
}

NSDictionary<NSString *, NSString *> *additionalParameters = emmPasscodeInfoRequired ?
@{ @"emm_passcode_info_required" : @"1" } : nil;
OIDAuthorizationResponse *authResponse =
[OIDAuthorizationResponse testInstanceWithAdditionalParameters:additionalParameters
nonce:nonce
errorString:authError];

OIDTokenResponse *tokenResponse =
Expand Down Expand Up @@ -1475,6 +1511,8 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
[_signIn signInWithPresentingWindow:_presentingWindow
#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
hint:_hint
additionalScopes:nil
nonce:nonce
completion:completion];
}
}
Expand Down
12 changes: 7 additions & 5 deletions GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
#import <AppAuth/OIDAuthorizationRequest.h>
#endif

extern NSString *const OIDAuthorizationRequestTestingClientID;
extern NSString *const OIDAuthorizationRequestTestingScope;
extern NSString *const OIDAuthorizationRequestTestingScope2;
extern NSString *const OIDAuthorizationRequestTestingCodeVerifier;
extern NSString * _Nonnull const OIDAuthorizationRequestTestingClientID;
extern NSString * _Nonnull const OIDAuthorizationRequestTestingScope;
extern NSString * _Nonnull const OIDAuthorizationRequestTestingScope2;
extern NSString * _Nonnull const OIDAuthorizationRequestTestingCodeVerifier;

@interface OIDAuthorizationRequest (Testing)

+ (instancetype)testInstance;
+ (instancetype _Nonnull)testInstance;

+ (instancetype _Nonnull)testInstanceWithNonce:(nullable NSString *)nonce;

@end
Loading

0 comments on commit b34f5a9

Please sign in to comment.