diff --git a/FirebaseAuth/CHANGELOG.md b/FirebaseAuth/CHANGELOG.md index 8203aeebb22..1b2090cbcf0 100644 --- a/FirebaseAuth/CHANGELOG.md +++ b/FirebaseAuth/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [added] Added an API for developers to pass the fullName from the Sign in with Apple credential to Firebase. (#10068) + # 10.6.0 - [fixed] Fixed a bug where user is created in a specific tenant although tenantID was not specified. (#10748) - [fixed] Fixed a bug where the resolver exposed in MFA is not associated to the correct app. (#10690) diff --git a/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthCredential.m b/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthCredential.m index 3c88a26b294..d066a6ffa49 100644 --- a/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthCredential.m +++ b/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthCredential.m @@ -45,6 +45,7 @@ - (instancetype)initWithProviderID:(NSString *)providerID rawNonce:(nullable NSString *)rawNonce accessToken:(nullable NSString *)accessToken secret:(nullable NSString *)secret + fullName:(nullable NSPersonNameComponents *)fullName pendingToken:(nullable NSString *)pendingToken { self = [super initWithProvider:providerID]; if (self) { @@ -53,6 +54,7 @@ - (instancetype)initWithProviderID:(NSString *)providerID _accessToken = accessToken; _pendingToken = pendingToken; _secret = secret; + _fullName = fullName; } return self; } @@ -65,6 +67,7 @@ - (instancetype)initWithProviderID:(NSString *)providerID rawNonce:nil accessToken:nil secret:nil + fullName:nil pendingToken:nil]; if (self) { _OAuthResponseURLString = OAuthResponseURLString; @@ -81,6 +84,7 @@ - (nullable instancetype)initWithVerifyAssertionResponse:(FIRVerifyAssertionResp rawNonce:nil accessToken:response.oauthAccessToken secret:response.oauthSecretToken + fullName:nil pendingToken:response.pendingToken]; } return nil; @@ -94,6 +98,7 @@ - (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request { request.sessionID = _sessionID; request.providerOAuthTokenSecret = _secret; request.pendingToken = _pendingToken; + request.fullName = _fullName; } #pragma mark - NSSecureCoding @@ -108,11 +113,14 @@ - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { NSString *accessToken = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"accessToken"]; NSString *pendingToken = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"pendingToken"]; NSString *secret = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"secret"]; + NSPersonNameComponents *fullName = [aDecoder decodeObjectOfClass:[NSPersonNameComponents class] + forKey:@"fullName"]; self = [self initWithProviderID:self.provider IDToken:IDToken rawNonce:rawNonce accessToken:accessToken secret:secret + fullName:fullName pendingToken:pendingToken]; return self; } @@ -123,6 +131,7 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.accessToken forKey:@"accessToken"]; [aCoder encodeObject:self.pendingToken forKey:@"pendingToken"]; [aCoder encodeObject:self.secret forKey:@"secret"]; + [aCoder encodeObject:self.fullName forKey:@"fullName"]; } @end diff --git a/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthCredential_Internal.h b/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthCredential_Internal.h index 9f076016530..2d05bccb9f0 100644 --- a/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthCredential_Internal.h +++ b/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthCredential_Internal.h @@ -41,13 +41,19 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, readonly, nullable) NSString *pendingToken; -/** @fn initWithProviderId:IDToken:accessToken:secret:pendingToken +/** @property fullName + @brief The full name of the user associated with this OAuthCredential. + */ +@property(nonatomic, readonly, nullable) NSPersonNameComponents *fullName; + +/** @fn initWithProviderId:IDToken:rawNonce:accessToken:secret:fullName:pendingToken @brief Designated initializer. @param providerID The provider ID associated with the credential being created. @param IDToken The ID Token associated with the credential being created. @param rawNonce The raw nonce associated with the Auth credential being created. @param accessToken The access token associated with the credential being created. @param secret The secret associated with the credential being created. + @param fullName The full name associated with the credential being created. @param pendingToken The pending token associated with the credential being created. */ - (instancetype)initWithProviderID:(NSString *)providerID @@ -55,6 +61,7 @@ NS_ASSUME_NONNULL_BEGIN rawNonce:(nullable NSString *)rawNonce accessToken:(nullable NSString *)accessToken secret:(nullable NSString *)secret + fullName:(nullable NSPersonNameComponents *)fullName pendingToken:(nullable NSString *)pendingToken NS_DESIGNATED_INITIALIZER; /** @fn initWithProviderId:sessionID:OAuthResponseURLString: diff --git a/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthProvider.m b/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthProvider.m index 80cd2adef66..2f40fdbc438 100644 --- a/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthProvider.m +++ b/FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthProvider.m @@ -88,6 +88,7 @@ + (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID rawNonce:nil accessToken:accessToken secret:nil + fullName:nil pendingToken:nil]; } @@ -98,6 +99,7 @@ + (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID rawNonce:nil accessToken:accessToken secret:nil + fullName:nil pendingToken:nil]; } @@ -110,6 +112,7 @@ + (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID rawNonce:rawNonce accessToken:accessToken secret:nil + fullName:nil pendingToken:nil]; } @@ -121,6 +124,19 @@ + (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID rawNonce:rawNonce accessToken:nil secret:nil + fullName:nil + pendingToken:nil]; +} + ++ (FIROAuthCredential *)appleCredentialWithIDToken:(NSString *)IDToken + rawNonce:(nullable NSString *)rawNonce + fullName:(nullable NSPersonNameComponents *)fullName { + return [[FIROAuthCredential alloc] initWithProviderID:@"apple.com" + IDToken:IDToken + rawNonce:rawNonce + accessToken:nil + secret:nil + fullName:fullName pendingToken:nil]; } diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.h b/FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.h index 0a1aa370b78..f69c12b948e 100644 --- a/FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.h +++ b/FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.h @@ -96,6 +96,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, assign) BOOL autoCreate; +/** @property fullName + @brief A full name from the IdP. + */ +@property(nonatomic, copy, nullable) NSPersonNameComponents *fullName; + /** @fn initWithEndpoint:requestConfiguration: @brief Please use initWithProviderID:requestConfifuration instead. */ diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.m b/FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.m index 7412af836c9..57e6ec084d0 100644 --- a/FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.m +++ b/FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.m @@ -84,6 +84,28 @@ */ static NSString *const kReturnSecureTokenKey = @"returnSecureToken"; +/** @var kUserKey + @brief The key for the "user" value in the request. The value is a JSON object that contains the + name of the user. + */ +static NSString *const kUserKey = @"user"; + +/** @var kNameKey + @brief The key for the "name" value in the request. The value is a JSON object that contains the + first and/or last name of the user. + */ +static NSString *const kNameKey = @"name"; + +/** @var kFirstNameKey + @brief The key for the "firstName" value in the request. + */ +static NSString *const kFirstNameKey = @"firstName"; + +/** @var kLastNameKey + @brief The key for the "lastName" value in the request. + */ +static NSString *const kLastNameKey = @"lastName"; + /** @var kReturnIDPCredentialKey @brief The key for the "returnIdpCredential" value in the request. */ @@ -148,6 +170,24 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) if (_inputEmail) { [queryItems addObject:[NSURLQueryItem queryItemWithName:kIdentifierKey value:_inputEmail]]; } + + if (_fullName.givenName || _fullName.familyName) { + NSMutableDictionary *nameDict = [[NSMutableDictionary alloc] init]; + if (_fullName.givenName) { + nameDict[kFirstNameKey] = _fullName.givenName; + } + if (_fullName.familyName) { + nameDict[kLastNameKey] = _fullName.familyName; + } + NSDictionary *userDict = [NSDictionary dictionaryWithObject:nameDict forKey:kNameKey]; + NSData *userJson = [NSJSONSerialization dataWithJSONObject:userDict options:0 error:error]; + [queryItems + addObject:[NSURLQueryItem + queryItemWithName:kUserKey + value:[[NSString alloc] initWithData:userJson + encoding:NSUTF8StringEncoding]]]; + } + [components setQueryItems:queryItems]; NSMutableDictionary *body = [@{ kRequestURIKey : _requestURI ?: @"http://localhost", // Unused by server, but required @@ -171,6 +211,7 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) if (_sessionID) { body[kSessionIDKey] = _sessionID; } + if (self.tenantID) { body[kTenantIDKey] = self.tenantID; } diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIROAuthProvider.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIROAuthProvider.h index 170c19e5219..2c2128c1128 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIROAuthProvider.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIROAuthProvider.h @@ -114,6 +114,22 @@ NS_SWIFT_NAME(OAuthProvider) IDToken:(NSString *)IDToken rawNonce:(nullable NSString *)rawNonce; +/** @fn appleCredentialWithIDToken:rawNonce:fullName: + * @brief Creates an `AuthCredential` for the Sign in with Apple OAuth 2 provider identified by ID + * token, raw nonce, and full name. This method is specific to the Sign in with Apple OAuth 2 + * provider as this provider requires the full name to be passed explicitly. + * + * @param IDToken The IDToken associated with the Sign in with Apple Auth credential being created. + * @param rawNonce The raw nonce associated with the Sign in with Apple Auth credential being + * created. + * @param fullName The full name associated with the Sign in with Apple Auth credential being + * created. + * @return An `AuthCredential`. + */ ++ (FIROAuthCredential *)appleCredentialWithIDToken:(NSString *)IDToken + rawNonce:(nullable NSString *)rawNonce + fullName:(nullable NSPersonNameComponents *)fullName; + /** @fn init @brief This class is not meant to be initialized. */ diff --git a/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m b/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m index 7043373913a..03d47cb1af5 100644 --- a/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m +++ b/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m @@ -410,10 +410,10 @@ - (void)reauthenticateWithApple { - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) { ASAuthorizationAppleIDCredential* appleIDCredential = authorization.credential; NSString *IDToken = [NSString stringWithUTF8String:[appleIDCredential.identityToken bytes]]; - FIROAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:@"apple.com" - IDToken:IDToken - rawNonce:self.appleRawNonce - accessToken:nil]; + FIROAuthCredential *credential = + [FIROAuthProvider appleCredentialWithIDToken:IDToken + rawNonce:self.appleRawNonce + fullName:appleIDCredential.fullName]; if ([appleIDCredential.state isEqualToString:@"signIn"]) { [FIRAuth.auth signInWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { diff --git a/FirebaseAuth/Tests/Unit/FIRAuthTests.m b/FirebaseAuth/Tests/Unit/FIRAuthTests.m index 989654b5052..22741b583cc 100644 --- a/FirebaseAuth/Tests/Unit/FIRAuthTests.m +++ b/FirebaseAuth/Tests/Unit/FIRAuthTests.m @@ -135,6 +135,16 @@ */ static NSString *const kDisplayName = @"User Doe"; +/** @var kFakeGivenName + @brief The fake user given name. + */ +static NSString *const kFakeGivenName = @"Firstname"; + +/** @var kFakeFamilyName + @brief The fake user family name. + */ +static NSString *const kFakeFamilyName = @"Lastname"; + /** @var kGoogleUD @brief The fake user ID under Google Sign-In. */ @@ -160,6 +170,16 @@ */ static NSString *const kGoogleIDToken = @"GOOGLE_ID_TOKEN"; +/** @var kAppleAuthProviderID + @brief The provider ID for Apple Sign-In. + */ +static NSString *const kAppleAuthProviderID = @"apple.com"; + +/** @var kAppleIDToken + @brief The fake ID token from Apple Sign-In. + */ +static NSString *const kAppleIDToken = @"APPLE_ID_TOKEN"; + /** @var kCustomToken @brief The fake custom token to sign in. */ @@ -1374,6 +1394,66 @@ - (void)testSignInWithGoogleCredentialFailure { OCMVerifyAll(_mockBackend); } +/** @fn testSignInWithAppleCredentialFullNameInRequest + @brief Tests the flow of a successful @c signInWithCredential:completion: call + with an Apple Sign-In credential with a full name. This test differentiates from + @c testSignInWithCredentialSuccess only in verifying the full name. + */ +- (void)testSignInWithAppleCredentialFullNameInRequestSuccess { + NSPersonNameComponents *fullName = [[NSPersonNameComponents alloc] init]; + fullName.givenName = kFakeGivenName; + fullName.familyName = kFakeFamilyName; + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.providerID, kAppleAuthProviderID); + XCTAssertEqualObjects(request.providerIDToken, kAppleIDToken); + // Verify that the full name is passed to the backend request. + XCTAssertEqualObjects(request.fullName, fullName); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVerifyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]); + OCMStub([mockVerifyAssertionResponse providerID]).andReturn(kAppleAuthProviderID); + [self stubTokensWithMockResponse:mockVerifyAssertionResponse]; + callback(mockVerifyAssertionResponse, nil); + }); + }); + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request, + FIRGetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockAppleUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]); + OCMStub([mockAppleUserInfo providerID]).andReturn(kAppleAuthProviderID); + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser providerUserInfo]) + .andReturn((@[ mockAppleUserInfo ])); + id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]); + OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ + mockGetAccountInfoResponseUser + ]); + callback(mockGetAccountInfoResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *appleCredential = [FIROAuthProvider appleCredentialWithIDToken:kAppleIDToken + rawNonce:nil + fullName:fullName]; + [[FIRAuth auth] + signInWithCredential:appleCredential + completion:^(FIRAuthDataResult *_Nullable authResult, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNotNil([FIRAuth auth].currentUser); + OCMVerifyAll(_mockBackend); +} + /** @fn testSignInAnonymouslySuccess @brief Tests the flow of a successful @c signInAnonymouslyWithCompletion: call. */ diff --git a/FirebaseAuth/Tests/Unit/FIROAuthProviderTests.m b/FirebaseAuth/Tests/Unit/FIROAuthProviderTests.m index 59e7da9b70a..20619ab344d 100644 --- a/FirebaseAuth/Tests/Unit/FIROAuthProviderTests.m +++ b/FirebaseAuth/Tests/Unit/FIROAuthProviderTests.m @@ -67,6 +67,16 @@ */ static NSString *const kFakeProviderID = @"fakeProviderID"; +/** @var kFakeGivenName + @brief A fake given name for testing. + */ +static NSString *const kFakeGivenName = @"fakeGivenName"; + +/** @var kFakeFamilyName + @brief A fake family name for testing. + */ +static NSString *const kFakeFamilyName = @"fakeFamilyName"; + /** @var kFakeAPIKey @brief A fake API key. */ @@ -238,6 +248,25 @@ - (void)testObtainingOAuthCredentialNoIDToken { XCTAssertNil(OAuthCredential.IDToken); } +/** @fn testObtainingOAuthCredentialWithFullName + @brief Tests the correct creation of an OAuthCredential with a fullName. + */ +- (void)testObtainingOAuthCredentialWithFullName { + NSPersonNameComponents *fullName = [[NSPersonNameComponents alloc] init]; + fullName.givenName = kFakeGivenName; + fullName.familyName = kFakeFamilyName; + FIRAuthCredential *credential = [FIROAuthProvider appleCredentialWithIDToken:kFakeIDToken + rawNonce:nil + fullName:fullName]; + + XCTAssertTrue([credential isKindOfClass:[FIROAuthCredential class]]); + FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential; + XCTAssertEqualObjects(OAuthCredential.provider, @"apple.com"); + XCTAssertEqualObjects(OAuthCredential.IDToken, kFakeIDToken); + XCTAssertEqualObjects(OAuthCredential.fullName, fullName); + XCTAssertNil(OAuthCredential.accessToken); +} + /** @fn testObtainingOAuthCredentialWithIDToken @brief Tests the correct creation of an OAuthCredential with an IDToken */ diff --git a/FirebaseAuth/Tests/Unit/FIRVerifyAssertionRequestTests.m b/FirebaseAuth/Tests/Unit/FIRVerifyAssertionRequestTests.m index 16622521069..53a5188aabd 100644 --- a/FirebaseAuth/Tests/Unit/FIRVerifyAssertionRequestTests.m +++ b/FirebaseAuth/Tests/Unit/FIRVerifyAssertionRequestTests.m @@ -125,6 +125,21 @@ */ static NSString *const kAutoCreateKey = @"autoCreate"; +/** @var kUserKey + @brief The key for the "user" value in the request. + */ +static NSString *const kUserKey = @"user"; + +/** @var kFakeGivenName + @brief Fake given name used for testing the request. + */ +static NSString *const kFakeGivenName = @"Firstname"; + +/** @var kFakeFamilyName + @brief Fake family name used for testing the request. + */ +static NSString *const kFakeFamilyName = @"Lastname"; + /** @class FIRVerifyAssertionRequestTests @brief Tests for @c FIRVerifyAssertionReuqest */ @@ -225,6 +240,14 @@ - (void)testVerifyAssertionRequestOptionalFields { request.pendingToken = kTestPendingToken; request.providerOAuthTokenSecret = kTestProviderOAuthTokenSecret; request.autoCreate = NO; + NSPersonNameComponents *fullName = [[NSPersonNameComponents alloc] init]; + fullName.givenName = kFakeGivenName; + fullName.familyName = kFakeFamilyName; + request.fullName = fullName; + + NSString *userJSON = + [NSString stringWithFormat:@"{\"name\":{\"firstName\":\"%@\",\"lastName\":\"%@\"}}", + kFakeGivenName, kFakeFamilyName]; [FIRAuthBackend verifyAssertion:request @@ -238,6 +261,7 @@ - (void)testVerifyAssertionRequestOptionalFields { [NSURLQueryItem queryItemWithName:kProviderOAuthTokenSecretKey value:kTestProviderOAuthTokenSecret], [NSURLQueryItem queryItemWithName:kInputEmailKey value:kTestInputEmail], + [NSURLQueryItem queryItemWithName:kUserKey value:userJSON], ]; NSURLComponents *components = [[NSURLComponents alloc] init]; [components setQueryItems:queryItems];