From 2deb20d95bdb0e59808e5b60f7b65f28fbf4b804 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 6 Mar 2018 16:38:08 -0800 Subject: [PATCH 1/2] Squash and stage - Adds Email link sign-in (#246) --- Example/Auth/Sample/MainViewController.m | 127 +++++++- Example/Auth/Tests/FIRAuthTests.m | 286 +++++++++++++++++- Example/Auth/Tests/FIREmailLinkRequestTests.m | 137 +++++++++ .../Tests/FIREmailLinkSignInResponseTests.m | 195 ++++++++++++ .../FIRGetOOBConfirmationCodeRequestTests.m | 43 +++ Example/Firebase.xcodeproj/project.pbxproj | 10 + .../EmailPassword/FIREmailAuthProvider.m | 4 + .../FIREmailPasswordAuthCredential.h | 13 + .../FIREmailPasswordAuthCredential.m | 9 + Firebase/Auth/Source/FIRAuth.m | 176 ++++++++++- Firebase/Auth/Source/Public/FIRAuth.h | 81 +++++ .../Auth/Source/Public/FIREmailAuthProvider.h | 9 + Firebase/Auth/Source/RPCs/FIRAuthBackend.h | 32 +- Firebase/Auth/Source/RPCs/FIRAuthBackend.m | 19 ++ .../Source/RPCs/FIRCreateAuthURIResponse.h | 5 + .../Source/RPCs/FIRCreateAuthURIResponse.m | 1 + .../Source/RPCs/FIREmailLinkSignInRequest.h | 66 ++++ .../Source/RPCs/FIREmailLinkSignInRequest.m | 70 +++++ .../Source/RPCs/FIREmailLinkSignInResponse.h | 54 ++++ .../Source/RPCs/FIREmailLinkSignInResponse.m | 32 ++ .../RPCs/FIRGetOOBConfirmationCodeRequest.h | 22 +- .../RPCs/FIRGetOOBConfirmationCodeRequest.m | 24 ++ 22 files changed, 1397 insertions(+), 18 deletions(-) create mode 100644 Example/Auth/Tests/FIREmailLinkRequestTests.m create mode 100644 Example/Auth/Tests/FIREmailLinkSignInResponseTests.m create mode 100644 Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.h create mode 100644 Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.m create mode 100644 Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.h create mode 100644 Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.m diff --git a/Example/Auth/Sample/MainViewController.m b/Example/Auth/Sample/MainViewController.m index 31c103a474b..36ef92d1ae1 100644 --- a/Example/Auth/Sample/MainViewController.m +++ b/Example/Auth/Sample/MainViewController.m @@ -91,6 +91,16 @@ */ static NSString *const kSignInGoogleButtonText = @"Sign in with Google"; +/** @var kSignInWithEmailLink + @brief The text of the "Sign in with Email Link" button. + */ +static NSString *const kSignInWithEmailLink = @"Sign in with Email Link"; + +/** @var kSendEmailSignInLink + @brief The text of the "Send Email SignIn link" button +*/ +static NSString *const kSendEmailSignInLink = @"Send Email Sign in Link"; + /** @var kSignInAndRetrieveGoogleButtonText @brief The text of the "Sign in with Google and retrieve data" button. */ @@ -279,6 +289,11 @@ */ static NSString *const kGetProvidersForEmail = @"Get Provider IDs for Email"; +/** @var kGetAllSignInMethodsForEmail + @brief The text of the "Get sign-in methods for Email" button. + */ +static NSString *const kGetAllSignInMethodsForEmail = @"Get Sign-in methods for Email"; + /** @var kActionCodeTypeDescription @brief The description of the "Action Type" entry. */ @@ -722,6 +737,10 @@ - (void)updateTable { action:^{ [weakSelf createUserAuthDataResult]; }], [StaticContentTableViewCell cellWithTitle:kSignInGoogleButtonText action:^{ [weakSelf signInGoogle]; }], + [StaticContentTableViewCell cellWithTitle:kSignInWithEmailLink + action:^{ [weakSelf signInWithEmailLink]; }], + [StaticContentTableViewCell cellWithTitle:kSendEmailSignInLink + action:^{ [weakSelf sendEmailSignInLink]; }], [StaticContentTableViewCell cellWithTitle:kSignInGoogleAndRetrieveDataButtonText action:^{ [weakSelf signInGoogleAndRetrieveData]; }], [StaticContentTableViewCell cellWithTitle:kSignInFacebookButtonText @@ -754,6 +773,8 @@ - (void)updateTable { action:^{ [weakSelf reloadUser]; }], [StaticContentTableViewCell cellWithTitle:kGetProvidersForEmail action:^{ [weakSelf getProvidersForEmail]; }], + [StaticContentTableViewCell cellWithTitle:kGetAllSignInMethodsForEmail + action:^{ [weakSelf getAllSignInMethodsForEmail]; }], [StaticContentTableViewCell cellWithTitle:kUpdateEmailText action:^{ [weakSelf updateEmail]; }], [StaticContentTableViewCell cellWithTitle:kUpdatePasswordText @@ -1657,7 +1678,7 @@ - (void)signInFacebookAndRetrieveData { } /** @fn signInEmailPassword - @brief Invoked when "sign in with Email/Password" row is pressed. + @brief Invoked when "Sign in with Email/Password" row is pressed. */ - (void)signInEmailPassword { [self showTextInputPromptWithMessage:@"Email Address:" @@ -1724,6 +1745,75 @@ - (void)signInEmailPasswordAuthDataResult { }]; } +/** @fn signInWithEmailLink + @brief Invoked when "Sign in with email link" row is pressed. + */ +- (void)signInWithEmailLink { + [self showTextInputPromptWithMessage:@"Email Address:" + keyboardType:UIKeyboardTypeEmailAddress + completionBlock:^(BOOL userPressedOK, NSString *_Nullable email) { + if (!userPressedOK || !email.length) { + return; + } + [self showTextInputPromptWithMessage:@"Email Sign-In Link:" + completionBlock:^(BOOL userPressedOK, NSString *_Nullable link) { + if (!userPressedOK) { + return; + } + if ([[FIRAuth auth] isSignInWithEmailLink:link]) { + [self showSpinner:^{ + [[AppManager auth] signInWithEmail:email + link:link + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + [self hideSpinner:^{ + if (error) { + [self logFailure:@"sign-in with Email/Sign-In failed" error:error]; + } else { + [self logSuccess:@"sign-in with Email/Sign-In link succeeded."]; + [self log:[NSString stringWithFormat:@"UID: %@",authResult.user.uid]]; + } + [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In Error" error:error]; + }]; + }]; + }]; + } else { + [self log:@"The sign-in link is invalid"]; + } + }]; + }]; +} + +/** @fn sendEmailSignInLink + @brief Invoked when "Send email sign-in link" row is pressed. + */ +- (void)sendEmailSignInLink { + [self showTextInputPromptWithMessage:@"Email:" + completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) { + if (!userPressedOK) { + return; + } + [self showSpinner:^{ + void (^requestEmailSignInLink)(void (^)(NSError *)) = ^(void (^completion)(NSError *)) { + [[AppManager auth] sendSignInLinkToEmail:userInput + actionCodeSettings:[self actionCodeSettings] + completion:completion]; + }; + requestEmailSignInLink(^(NSError *_Nullable error) { + [self hideSpinner:^{ + if (error) { + [self logFailure:@"Email Link request failed" error:error]; + [self showMessagePrompt:error.localizedDescription]; + return; + } + [self logSuccess:@"Email Link request succeeded."]; + [self showMessagePrompt:@"Sent"]; + }]; + }); + }]; + }]; +} + /** @fn signUpNewEmail @brief Invoked if sign-in is attempted with new email/password. @remarks Should only be called if @c FIRAuthErrorCodeInvalidEmail is encountered on attepmt to @@ -2245,6 +2335,39 @@ - (void)getProvidersForEmail { }]; } +/** @fn getAllSignInMethodsForEmail + @brief Prompts user for an email address, calls @c FIRAuth.getAllSignInMethodsForEmail:callback: + and displays the result. + */ +- (void)getAllSignInMethodsForEmail { + [self showTextInputPromptWithMessage:@"Email:" + completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) { + if (!userPressedOK || !userInput.length) { + return; + } + + [self showSpinner:^{ + [[AppManager auth] fetchSignInMethodsForEmail:userInput + completion:^(NSArray *_Nullable signInMethods, + NSError *_Nullable error) { + if (error) { + [self logFailure:@"get sign-in methods for email failed" error:error]; + } else { + [self logSuccess:@"get sign-in methods for email succeeded."]; + } + [self hideSpinner:^{ + if (error) { + [self showMessagePrompt:error.localizedDescription]; + return; + } + [self showMessagePrompt:[signInMethods componentsJoinedByString:@", "]]; + }]; + }]; + }]; + }]; +} + + /** @fn actionCodeRequestTypeString @brief Returns a string description for the type of the next action code request. */ @@ -2486,6 +2609,8 @@ - (NSString *)nameForActionCodeOperation:(FIRActionCodeOperation)operation { return @"Recover Email"; case FIRActionCodeOperationPasswordReset: return @"Password Reset"; + case FIRActionCodeOperationEmailLink: + return @"Email Sign-In Link"; case FIRActionCodeOperationUnknown: return @"Unknown action"; } diff --git a/Example/Auth/Tests/FIRAuthTests.m b/Example/Auth/Tests/FIRAuthTests.m index b22c6004999..27edfcf0b42 100644 --- a/Example/Auth/Tests/FIRAuthTests.m +++ b/Example/Auth/Tests/FIRAuthTests.m @@ -33,6 +33,8 @@ #import "FIRAuthBackend.h" #import "FIRCreateAuthURIRequest.h" #import "FIRCreateAuthURIResponse.h" +#import "FIREmailLinkSignInRequest.h" +#import "FIREmailLinkSignInResponse.h" #import "FIRGetAccountInfoRequest.h" #import "FIRGetAccountInfoResponse.h" #import "FIRGetOOBConfirmationCodeRequest.h" @@ -56,6 +58,7 @@ #import "FIRApp+FIRAuthUnitTests.h" #import "OCMStubRecorder+FIRAuthUnitTests.h" #import +#import "FIRActionCodeSettings.h" #if TARGET_OS_IOS #import "FIRPhoneAuthCredential.h" @@ -167,6 +170,38 @@ */ static NSString *const kVerificationID = @"55432"; +/** @var kContinueURL + @brief Fake string value of continue url. + */ +static NSString *const kContinueURL = @"continueURL"; + +/** @var kCanHandleCodeInAppKey + @brief The key for the request parameter indicating whether the action code can be handled in + the app or not. + */ +static NSString *const kCanHandleCodeInAppKey = @"canHandleCodeInApp"; + +/** @var kFIREmailLinkAuthSignInMethod + @brief Fake email link sign-in method for testing. + */ +static NSString *const kFIREmailLinkAuthSignInMethod = @"emailLink"; + +/** @var kFIRFacebookAuthSignInMethod + @brief Fake Facebook sign-in method for testing. + */ +static NSString *const kFIRFacebookAuthSignInMethod = @"facebook.com"; + +/** @var kBadSignInEmailLink + @brief Bad sign-in link to test email link sign-in + */ +static NSString *const kBadSignInEmailLink = @"http://www.facebook.com"; + +/** @var kFakeEmailSignInlink + @brief Fake email sign-in link + */ +static NSString *const kFakeEmailSignInlink = @"https://fex9s.app.goo.gl?link=" + "https://fb-sa-upgraded.firebaseapp.com/_?mode%3DsignIn%26oobCode%3Dtestoobcode"; + /** @var kExpectationTimeout @brief The maximum time waiting for expectations to fulfill. */ @@ -360,6 +395,39 @@ - (void)testFetchProvidersForEmailSuccess { OCMVerifyAll(_mockBackend); } +/** @fn testFetchSignInMethodsForEmailSuccess + @brief Tests the flow of a successful @c fetchSignInMethodsForEmail:completion: call. + */ +- (void)testFetchSignInMethodsForEmailSuccess { + NSArray *allSignInMethods = + @[ kFIREmailLinkAuthSignInMethod, kFIRFacebookAuthSignInMethod ]; + OCMExpect([_mockBackend createAuthURI:[OCMArg any] + callback:[OCMArg any]]) + .andCallBlock2(^(FIRCreateAuthURIRequest *_Nullable request, + FIRCreateAuthURIResponseCallback callback) { + XCTAssertEqualObjects(request.identifier, kEmail); + XCTAssertNotNil(request.endpoint); + XCTAssertEqualObjects(request.APIKey, kAPIKey); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockCreateAuthURIResponse = OCMClassMock([FIRCreateAuthURIResponse class]); + OCMStub([mockCreateAuthURIResponse signinMethods]).andReturn(allSignInMethods); + callback(mockCreateAuthURIResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] fetchSignInMethodsForEmail:kEmail + completion:^(NSArray *_Nullable signInMethods, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqualObjects(signInMethods, allSignInMethods); + XCTAssertTrue([allSignInMethods isKindOfClass:[NSArray class]]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + /** @fn testFetchProvidersForEmailSuccessDeprecatedProviderID @brief Tests the flow of a successful @c fetchProvidersForEmail:completion: call using the deprecated FIREmailPasswordAuthProviderID. @@ -416,6 +484,25 @@ - (void)testFetchProvidersForEmailFailure { OCMVerifyAll(_mockBackend); } +/** @fn testFetchSignInMethodsForEmailFailure + @brief Tests the flow of a failed @c fetchSignInMethodsForEmail:completion: call. + */ +- (void)testFetchSignInMethodsForEmailFailure { + OCMExpect([_mockBackend createAuthURI:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils tooManyRequestsErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] fetchSignInMethodsForEmail:kEmail + completion:^(NSArray *_Nullable signInMethods, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(signInMethods); + XCTAssertEqual(error.code, FIRAuthErrorCodeTooManyRequests); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} #if TARGET_OS_IOS /** @fn testPhoneAuthSuccess @brief Tests the flow of a successful @c signInWithCredential:completion for phone auth. @@ -501,6 +588,63 @@ - (void)testPhoneAuthMissingVerificationID { } #endif +/** @fn testSignInWithEmailLinkSuccess + @brief Tests the flow of a successful @c signInWithEmail:link:completion: call. + */ +- (void)testSignInWithEmailLinkSuccess { + NSString *fakeCode = @"testoobcode"; + OCMExpect([_mockBackend emailLinkSignin:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIREmailLinkSignInRequest *_Nullable request, + FIREmailLinkSigninResponseCallback callback) { + XCTAssertEqualObjects(request.email, kEmail); + XCTAssertEqualObjects(request.oobCode, fakeCode); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockEmailLinkSignInResponse = OCMClassMock([FIREmailLinkSignInResponse class]); + [self stubTokensWithMockResponse:mockEmailLinkSignInResponse]; + callback(mockEmailLinkSignInResponse, nil); + OCMStub([mockEmailLinkSignInResponse refreshToken]).andReturn(kRefreshToken); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithEmail:kEmail + link:kFakeEmailSignInlink + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNotNil(authResult.user); + XCTAssertEqualObjects(authResult.user.refreshToken, kRefreshToken); + XCTAssertFalse(authResult.user.anonymous); + XCTAssertEqualObjects(authResult.user.email, kEmail); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithEmailLinkFailure + @brief Tests the flow of a failed @c signInWithEmail:link:completion: call. + */ +- (void)testSignInWithEmailLinkFailure { + OCMExpect([_mockBackend emailLinkSignin:[OCMArg any] callback:[OCMArg any]]) + ._andDispatchError2([FIRAuthErrorUtils invalidActionCodeErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithEmail:kEmail + link:kFakeEmailSignInlink + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidActionCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + /** @fn testSignInWithEmailPasswordSuccess @brief Tests the flow of a successful @c signInWithEmail:password:completion: call. */ @@ -521,8 +665,10 @@ - (void)testSignInWithEmailPasswordSuccess { [self expectGetAccountInfo]; XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; [[FIRAuth auth] signOut:NULL]; - [[FIRAuth auth] signInWithEmail:kEmail password:kFakePassword completion:^(FIRUser *_Nullable user, - NSError *_Nullable error) { + [[FIRAuth auth] signInWithEmail:kEmail + password:kFakePassword + completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { XCTAssertTrue([NSThread isMainThread]); [self assertUser:user]; XCTAssertNil(error); @@ -541,8 +687,10 @@ - (void)testSignInWithEmailPasswordFailure { .andDispatchError2([FIRAuthErrorUtils wrongPasswordErrorWithMessage:nil]); XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; [[FIRAuth auth] signOut:NULL]; - [[FIRAuth auth] signInWithEmail:kEmail password:kFakePassword completion:^(FIRUser *_Nullable user, - NSError *_Nullable error) { + [[FIRAuth auth] signInWithEmail:kEmail + password:kFakePassword + completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { XCTAssertTrue([NSThread isMainThread]); XCTAssertNil(user); XCTAssertEqual(error.code, FIRAuthErrorCodeWrongPassword); @@ -829,6 +977,68 @@ - (void)testVeridyPasswordResetCodeFailure { OCMVerifyAll(_mockBackend); } +/** @fn testSignInWithEmailLinkCredentialSuccess + @brief Tests the flow of a successfully @c signInWithCredential:completion: call with an + email sign-in link credential using FIREmailAuthProvider. + */ +- (void)testSignInWithEmailLinkCredentialSuccess { + NSString *fakeCode = @"testoobcode"; + OCMExpect([_mockBackend emailLinkSignin:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIREmailLinkSignInRequest *_Nullable request, + FIREmailLinkSigninResponseCallback callback) { + XCTAssertEqualObjects(request.email, kEmail); + XCTAssertEqualObjects(request.oobCode, fakeCode); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockEmailLinkSigninResponse = OCMClassMock([FIREmailLinkSignInResponse class]); + [self stubTokensWithMockResponse:mockEmailLinkSigninResponse]; + callback(mockEmailLinkSigninResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *emailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail link:kFakeEmailSignInlink]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:emailCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNotNil(authResult.user); + XCTAssertEqualObjects(authResult.user.refreshToken, kRefreshToken); + XCTAssertFalse(authResult.user.anonymous); + XCTAssertEqualObjects(authResult.user.email, kEmail); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithEmailLinkCredentialFailure + @brief Tests the flow of a failed @c signInWithCredential:completion: call with an + email-email sign-in link credential using FIREmailAuthProvider. + */ +- (void)testSignInWithEmailLinkCredentialFailure { + OCMExpect([_mockBackend emailLinkSignin:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils userDisabledErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *emailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail link:kFakeEmailSignInlink]; + [[FIRAuth auth] signInWithCredential:emailCredential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeUserDisabled); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil([FIRAuth auth].currentUser); + OCMVerifyAll(_mockBackend); +} + /** @fn testSignInWithEmailCredentialSuccess @brief Tests the flow of a successfully @c signInWithCredential:completion: call with an email-password credential. @@ -1504,6 +1714,65 @@ - (void)testSendPasswordResetEmailFailure { OCMVerifyAll(_mockBackend); } +/** @fn testSendSignInLinkToEmailSuccess + @brief Tests the flow of a successful @c sendSignInLinkToEmail:actionCodeSettings:completion: + call. + */ +- (void)testSendSignInLinkToEmailSuccess { + OCMExpect([_mockBackend getOOBConfirmationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetOOBConfirmationCodeRequest *_Nullable request, + FIRGetOOBConfirmationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.email, kEmail); + XCTAssertEqualObjects(request.continueURL, kContinueURL); + XCTAssertTrue(request.handleCodeInApp); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback([[FIRGetOOBConfirmationCodeResponse alloc] init], nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] sendSignInLinkToEmail:kEmail + actionCodeSettings:[self fakeActionCodeSettings] + completion:^(NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSendSignInLinkToEmailFailure + @brief Tests the flow of a failed @c sendSignInLinkToEmail:actionCodeSettings:completion: + call. + */ +- (void)testSendSignInLinkToEmailFailure { + OCMExpect([_mockBackend getOOBConfirmationCode:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils appNotAuthorizedError]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] sendSignInLinkToEmail:kEmail + actionCodeSettings:[self fakeActionCodeSettings] + completion:^(NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(error.code, FIRAuthErrorCodeAppNotAuthorized); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn fakeActionCodeSettings + @brief Constructs and returns a fake instance of @c FIRActionCodeSettings for testing. + @return An instance of @c FIRActionCodeSettings for testing. + */ +- (FIRActionCodeSettings *)fakeActionCodeSettings { + FIRActionCodeSettings *actionCodeSettings = [[FIRActionCodeSettings alloc]init]; + actionCodeSettings.URL = [NSURL URLWithString:kContinueURL]; + actionCodeSettings.handleCodeInApp = YES; + return actionCodeSettings; +} + /** @fn testSignOut @brief Tests the @c signOut: method. */ @@ -1515,6 +1784,15 @@ - (void)testSignOut { XCTAssertNil([FIRAuth auth].currentUser); } +/** @fn testIsSignInWithEmailLink + @brief Tests the @c isSignInWithEmailLink: method. +*/ +- (void)testIsSignInWithEmailLink { + XCTAssertTrue([[FIRAuth auth] isSignInWithEmailLink:kFakeEmailSignInlink]); + XCTAssertFalse([[FIRAuth auth] isSignInWithEmailLink:kBadSignInEmailLink]); + XCTAssertFalse([[FIRAuth auth] isSignInWithEmailLink:@""]); +} + /** @fn testAuthStateChanges @brief Tests @c addAuthStateDidChangeListener: and @c removeAuthStateDidChangeListener: methods. */ diff --git a/Example/Auth/Tests/FIREmailLinkRequestTests.m b/Example/Auth/Tests/FIREmailLinkRequestTests.m new file mode 100644 index 00000000000..90d7c1842cf --- /dev/null +++ b/Example/Auth/Tests/FIREmailLinkRequestTests.m @@ -0,0 +1,137 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIREmailLinkSignInRequest.h" +#import "FIREmailLinkSignInResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kTestEmail + @brief The key for the "email" value in the request. + */ +static NSString *const kTestEmail = @"TestEmail@email.com"; + +/** @var kTestOOBCode + @brief The test value for the "oobCode" in the request. + */ +static NSString *const kTestOOBCode = @"TestOOBCode"; + +/** @var kTestIDToken + @brief The test value for "idToken" in the request. + */ +static NSString *const kTestIDToken = @"testIDToken"; + +/** @var kEmailKey + @brief The key for the "identifier" value in the request. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kEmailLinkKey + @brief The key for the "oobCode" value in the request. + */ +static NSString *const kOOBCodeKey = @"oobCode"; + +/** @var kIDTokenKey + @brief The key for the "IDToken" value in the request. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kExpectedAPIURL + @brief The value of the expected URL (including the backend endpoint) in the request. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSignin?key=APIKey"; + +/** @class FIREmailLinkRequestTests + @brief Tests for @c FIREmailLinkRequests. + */ +@interface FIREmailLinkRequestTests : XCTestCase +@end + +@implementation FIREmailLinkRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; + + /** @var _requestConfiguration + @brief This is the request configuration used for testing. + */ + FIRAuthRequestConfiguration *_requestConfiguration; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; + _requestConfiguration = [[FIRAuthRequestConfiguration alloc] initWithAPIKey:kTestAPIKey]; +} + +- (void)tearDown { + _RPCIssuer = nil; + _requestConfiguration = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testEmailLinkRequestCreation + @brief Tests the email link sign-in request with mandatory parameters. + */ +- (void)testEmailLinkRequest { + FIREmailLinkSignInRequest *request = + [[FIREmailLinkSignInRequest alloc] initWithEmail:kTestEmail + oobCode:kTestOOBCode + requestConfiguration:_requestConfiguration]; + [FIRAuthBackend emailLinkSignin:request callback:^(FIREmailLinkSignInResponse *_Nullable response, + NSError *_Nullable error) { + }]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kOOBCodeKey], kTestOOBCode); + XCTAssertNil(_RPCIssuer.decodedRequest[kIDTokenKey]); +} + +/** @fn testEmailLinkRequestCreationOptional + @brief Tests the email link sign-in request with mandatory parameters and optional ID token. + */ +- (void)testEmailLinkRequestCreationOptional { + FIREmailLinkSignInRequest *request = + [[FIREmailLinkSignInRequest alloc] initWithEmail:kTestEmail + oobCode:kTestOOBCode + requestConfiguration:_requestConfiguration]; + request.IDToken = kTestIDToken; + [FIRAuthBackend emailLinkSignin:request callback:^(FIREmailLinkSignInResponse *_Nullable response, + NSError *_Nullable error) { + }]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kOOBCodeKey], kTestOOBCode); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestIDToken); +} + +@end diff --git a/Example/Auth/Tests/FIREmailLinkSignInResponseTests.m b/Example/Auth/Tests/FIREmailLinkSignInResponseTests.m new file mode 100644 index 00000000000..cc2c5442323 --- /dev/null +++ b/Example/Auth/Tests/FIREmailLinkSignInResponseTests.m @@ -0,0 +1,195 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "FIRAuthErrors.h" +#import "FIRAuthErrorUtils.h" +#import "FIRAuthBackend.h" +#import "FIREmailLinkSignInRequest.h" +#import "FIREmailLinkSignInResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kTestEmail + @brief The key for the "email" value in the request. + */ +static NSString *const kTestEmail = @"TestEmail@email.com"; + +/** @var kTestOOBCode + @brief The test value for the "oobCode" in the request. + */ +static NSString *const kTestOOBCode = @"TestOOBCode"; + +/** @var kTestIDToken + @brief The test value for "idToken" in the request. + */ +static NSString *const kTestIDToken = @"testIDToken"; + +/** @var kEmailKey + @brief The key for the "identifier" value in the request. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kEmailLinkKey + @brief The key for the "emailLink" value in the request. + */ +static NSString *const kOOBCodeKey = @"oobCode"; + +/** @var kIDTokenKey + @brief The key for the "IDToken" value in the request. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kTestIDTokenResponse + @brief A fake ID Token in the server response. + */ +static NSString *const kTestIDTokenResponse = @"fakeToken"; + +/** @var kTestEmailResponse + @brief A fake email in the server response. + */ +static NSString *const kTestEmailResponse = @"fake email"; + +/** @var kTestRefreshToken + @brief A fake refresh token in the server response. + */ +static NSString *const kTestRefreshToken = @"testRefreshToken"; + +/** @var kInvalidEmailErrorMessage + @brief The error returned by the server if the email is invalid. + */ +static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL"; + +/** @var kTestTokenExpirationTimeInterval + @brief The fake time interval that it takes a token to expire. + */ +static const NSTimeInterval kTestTokenExpirationTimeInterval = 55 * 60; + +/** @var kMaxDifferenceBetweenDates + @brief The maximum difference between time two dates (in seconds), after which they will be + considered different. + */ +static const NSTimeInterval kMaxDifferenceBetweenDates = 0.0001; + +/** @var kFakeIsNewUSerFlag + @brief The fake fake isNewUser flag in the response. + */ +static const BOOL kFakeIsNewUSerFlag = YES; + +/** @class FIREmailLinkRequestTests + @brief Tests for @c FIREmailLinkRequests. + */ +@interface FIREmailLinkSignInResponseTests : XCTestCase +@end + +@implementation FIREmailLinkSignInResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; + + /** @var _requestConfiguration + @brief This is the request configuration used for testing. + */ + FIRAuthRequestConfiguration *_requestConfiguration; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; + _requestConfiguration = [[FIRAuthRequestConfiguration alloc] initWithAPIKey:kTestAPIKey]; +} + +/** @fn testFailedEmailLinkSignInResponse + @brief Tests a failed email link sign-in response. + */ +- (void)testFailedEmailLinkSignInResponse { + FIREmailLinkSignInRequest *request = + [[FIREmailLinkSignInRequest alloc] initWithEmail:kTestEmail + oobCode:kTestOOBCode + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked = NO; + __block FIREmailLinkSignInResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend emailLinkSignin:request + callback:^(FIREmailLinkSignInResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail); +} + +/** @fn testSuccessfulEmailLinkSignInResponse + @brief Tests a succesful email link sign-in response. + */ +- (void)testSuccessfulEmailLinkSignInResponse { + FIREmailLinkSignInRequest *request = + [[FIREmailLinkSignInRequest alloc] initWithEmail:kTestEmail + oobCode:kTestOOBCode + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked = NO; + __block FIREmailLinkSignInResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend emailLinkSignin:request + callback:^(FIREmailLinkSignInResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"idToken" : kTestIDTokenResponse, + @"email" : kTestEmailResponse, + @"isNewUser" : kFakeIsNewUSerFlag ? @YES : @NO, + @"expiresIn" : [NSString stringWithFormat:@"%f",kTestTokenExpirationTimeInterval], + @"refreshToken" : kTestRefreshToken, + }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDTokenResponse); + XCTAssertEqualObjects(RPCResponse.email, kTestEmailResponse); + XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken); + XCTAssertTrue(RPCResponse.isNewUser); + NSTimeInterval expirationTimeInterval = + [RPCResponse.approximateExpirationDate timeIntervalSinceNow]; + NSTimeInterval testTimeInterval = + [[NSDate dateWithTimeIntervalSinceNow:kTestTokenExpirationTimeInterval] timeIntervalSinceNow]; + NSTimeInterval timeIntervalDifference = + fabs(expirationTimeInterval - testTimeInterval); + XCTAssert(timeIntervalDifference < kMaxDifferenceBetweenDates); +} + +@end diff --git a/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m b/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m index 965af8a1941..b11c7599b3d 100644 --- a/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m +++ b/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m @@ -49,6 +49,11 @@ */ static NSString *const kVerifyEmailRequestTypeValue = @"VERIFY_EMAIL"; +/** @var kEmailLinkSignInTypeValue + @brief The value for the "EMAIL_SIGNIN" request type. + */ +static NSString *const kEmailLinkSignInTypeValue = @"EMAIL_SIGNIN"; + /** @var kEmailKey @brief The name of the "email" property in the request. */ @@ -124,6 +129,7 @@ /** @class FIRGetOOBConfirmationCodeRequestTests @brief Tests for @c FIRGetOOBConfirmationCodeRequest. */ + @interface FIRGetOOBConfirmationCodeRequestTests : XCTestCase @end @implementation FIRGetOOBConfirmationCodeRequestTests { @@ -190,6 +196,43 @@ - (void)testPasswordResetRequest { [NSNumber numberWithBool:YES]); } +/** @fn testSignInWithEmailLinkRequest + @brief Tests the encoding of a email sign-in link request. + */ +- (void)testSignInWithEmailLinkRequest { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest signInWithEmailLinkRequest:kTestEmail + actionCodeSettings:[self fakeActionCodeSettings] + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kRequestTypeKey], kEmailLinkSignInTypeValue); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kContinueURLKey], kContinueURL); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIosBundleIDKey], kIosBundleID); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAndroidPackageNameKey], kAndroidPackageName); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAndroidMinimumVersionKey], + kAndroidMinimumVersion); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAndroidInstallAppKey], + [NSNumber numberWithBool:YES]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kCanHandleCodeInAppKey], + [NSNumber numberWithBool:YES]); +} + + /** @fn testEmailVerificationRequest @brief Tests the encoding of an email verification request. */ diff --git a/Example/Firebase.xcodeproj/project.pbxproj b/Example/Firebase.xcodeproj/project.pbxproj index c8400d60752..7c00d1c1f24 100644 --- a/Example/Firebase.xcodeproj/project.pbxproj +++ b/Example/Firebase.xcodeproj/project.pbxproj @@ -135,6 +135,8 @@ 7E26CF28514D041D284F00A5 /* Pods_Database_Tests_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5318F3AE32EEBCC9FF608813 /* Pods_Database_Tests_tvOS.framework */; }; 7E5BD38D202BFB8CD9CCEB53 /* Pods_Auth_EarlGreyTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FAA82401DA4259800B142EA /* Pods_Auth_EarlGreyTests.framework */; }; 7E9485421F578AC4005A3939 /* FIRAuthURLPresenterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E94853F1F578A9D005A3939 /* FIRAuthURLPresenterTests.m */; }; + 7EE21F7A1FE89193009B1370 /* FIREmailLinkRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7EE21F791FE89193009B1370 /* FIREmailLinkRequestTests.m */; }; + 7EE21F7C1FE8919E009B1370 /* FIREmailLinkSignInResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7EE21F7B1FE8919D009B1370 /* FIREmailLinkSignInResponseTests.m */; }; 7EFA2E041F71C93300DD354F /* FIRUserMetadataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7EFA2E031F71C93300DD354F /* FIRUserMetadataTests.m */; }; 7F41B0EFBDDA90CB9CE6CE5B /* Pods_Database_Tests_macOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61377AC9FE132A8D7BF71881 /* Pods_Database_Tests_macOS.framework */; }; 822CE316AE9827F7F0889B30 /* Pods_Auth_Example_macOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9878B57CF73D2F865992E6EA /* Pods_Auth_Example_macOS.framework */; }; @@ -1037,6 +1039,8 @@ 7981511F571E13DECA09B4B1 /* Pods-Core_Example_macOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Core_Example_macOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Core_Example_macOS/Pods-Core_Example_macOS.release.xcconfig"; sourceTree = ""; }; 7E94853F1F578A9D005A3939 /* FIRAuthURLPresenterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthURLPresenterTests.m; sourceTree = ""; }; 7ED0DF69C095C21EFC81F672 /* Pods-Database_Example_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Database_Example_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Database_Example_tvOS/Pods-Database_Example_tvOS.release.xcconfig"; sourceTree = ""; }; + 7EE21F791FE89193009B1370 /* FIREmailLinkRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIREmailLinkRequestTests.m; sourceTree = ""; }; + 7EE21F7B1FE8919D009B1370 /* FIREmailLinkSignInResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIREmailLinkSignInResponseTests.m; sourceTree = ""; }; 7EFA2E031F71C93300DD354F /* FIRUserMetadataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRUserMetadataTests.m; sourceTree = ""; }; 8496034D8156555C5FCF8F14 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 84EC7975F05977AE75E90A12 /* Pods_Database_Example_macOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Database_Example_macOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2371,9 +2375,11 @@ DE9315001E86C6FF0083EDBF /* FIRAuthGlobalWorkQueueTests.m */, DE9315011E86C6FF0083EDBF /* FIRAuthKeychainTests.m */, DE750DB81EB3DD4000A75E47 /* FIRAuthNotificationManagerTests.m */, + 7EE21F791FE89193009B1370 /* FIREmailLinkRequestTests.m */, DE9315021E86C6FF0083EDBF /* FIRAuthSerialTaskQueueTests.m */, DE9315031E86C6FF0083EDBF /* FIRAuthTests.m */, 7E94853F1F578A9D005A3939 /* FIRAuthURLPresenterTests.m */, + 7EE21F7B1FE8919D009B1370 /* FIREmailLinkSignInResponseTests.m */, DE9315041E86C6FF0083EDBF /* FIRAuthUserDefaultsStorageTests.m */, DE9315051E86C6FF0083EDBF /* FIRCreateAuthURIRequestTests.m */, DE9315061E86C6FF0083EDBF /* FIRCreateAuthURIResponseTests.m */, @@ -4662,6 +4668,7 @@ name = "[CP] Check Pods Manifest.lock"; outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Database_Tests_tvOS-checkManifestLockResult.txt", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -5425,6 +5432,7 @@ ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -6188,6 +6196,7 @@ DE9315571E86C71C0083EDBF /* FIRAdditionalUserInfoTests.m in Sources */, DE750DBF1EB3DD6C00A75E47 /* FIRAuthAppCredentialManagerTests.m in Sources */, DE93157B1E86C71C0083EDBF /* FIRVerifyPasswordResponseTests.m in Sources */, + 7EE21F7A1FE89193009B1370 /* FIREmailLinkRequestTests.m in Sources */, DE93155B1E86C71C0083EDBF /* FIRAuthDispatcherTests.m in Sources */, DE9315791E86C71C0083EDBF /* FIRVerifyCustomTokenResponseTests.m in Sources */, DE9315601E86C71C0083EDBF /* FIRAuthUserDefaultsStorageTests.m in Sources */, @@ -6216,6 +6225,7 @@ DE93155A1E86C71C0083EDBF /* FIRAuthBackendRPCImplementationTests.m in Sources */, DE93157D1E86C71C0083EDBF /* FIRVerifyPhoneNumberResponseTests.m in Sources */, DE93157E1E86C71C0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.m in Sources */, + 7EE21F7C1FE8919E009B1370 /* FIREmailLinkSignInResponseTests.m in Sources */, DE9315771E86C71C0083EDBF /* FIRVerifyAssertionResponseTests.m in Sources */, DE9315721E86C71C0083EDBF /* FIRSignUpNewUserRequestTests.m in Sources */, DE9315671E86C71C0083EDBF /* FIRGetAccountInfoResponseTests.m in Sources */, diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m index d27611ea636..7a871e24b01 100644 --- a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m @@ -32,4 +32,8 @@ + (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString return [[FIREmailPasswordAuthCredential alloc] initWithEmail:email password:password]; } ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email link:(NSString *)link { + return [[FIREmailPasswordAuthCredential alloc] initWithEmail:email link:link]; +} + @end diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h index d50bf1723a1..7625685fcf8 100644 --- a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h +++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h @@ -35,6 +35,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, readonly) NSString *password; +/** @property link + @brief The email sign-in link. + */ +@property(nonatomic, readonly) NSString *link; + /** @fn initWithEmail:password: @brief Designated initializer. @param email The user's email address. @@ -43,6 +48,14 @@ NS_ASSUME_NONNULL_BEGIN - (nullable instancetype)initWithEmail:(NSString *)email password:(NSString *)password NS_DESIGNATED_INITIALIZER; +/** @fn initWithEmail:link: + @brief Designated initializer. + @param email The user's email address. + @param link The email sign-in link. + */ +- (nullable instancetype)initWithEmail:(NSString *)email link:(NSString *)link + NS_DESIGNATED_INITIALIZER; + @end NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m index 4361366138b..71cc330aa4c 100644 --- a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m +++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m @@ -43,6 +43,15 @@ - (nullable instancetype)initWithEmail:(NSString *)email password:(NSString *)pa return self; } +- (nullable instancetype)initWithEmail:(NSString *)email link:(NSString *)link { + self = [super initWithProvider:FIREmailAuthProviderID]; + if (self) { + _email = [email copy]; + _link = [link copy]; + } + return self; +} + - (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request { [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason: @"Attempt to call prepareVerifyAssertionRequest: on a FIREmailPasswordAuthCredential."]; diff --git a/Firebase/Auth/Source/FIRAuth.m b/Firebase/Auth/Source/FIRAuth.m index 0f3705f9b5e..4305ea5fa18 100644 --- a/Firebase/Auth/Source/FIRAuth.m +++ b/Firebase/Auth/Source/FIRAuth.m @@ -39,6 +39,8 @@ #import "FIRAuthRequestConfiguration.h" #import "FIRCreateAuthURIRequest.h" #import "FIRCreateAuthURIResponse.h" +#import "FIREmailLinkSignInRequest.h" +#import "FIREmailLinkSignInResponse.h" #import "FIRGetOOBConfirmationCodeRequest.h" #import "FIRGetOOBConfirmationCodeResponse.h" #import "FIRResetPasswordRequest.h" @@ -117,6 +119,11 @@ */ static NSString *const kRecoverEmailRequestType = @"RECOVER_EMAIL"; +/** @var kEmailLinkSignInRequestType + @brief The action code type value for an email sign-in link in the check action code response. +*/ +static NSString *const kEmailLinkSignInRequestType = @"EMAIL_SIGNIN"; + /** @var kMissingPasswordReason @brief The reason why the @c FIRAuthErrorCodeWeakPassword error is thrown. @remarks This error message will be localized in the future. @@ -186,6 +193,9 @@ + (FIRActionCodeOperation)actionCodeOperationForRequestType:(NSString *)requestT if ([requestType isEqualToString:kRecoverEmailRequestType]) { return FIRActionCodeOperationRecoverEmail; } + if ([requestType isEqualToString:kEmailLinkSignInRequestType]) { + return FIRActionCodeOperationEmailLink; + } return FIRActionCodeOperationUnknown; } @@ -509,6 +519,24 @@ - (void)fetchProvidersForEmail:(NSString *)email }); } +- (void)fetchSignInMethodsForEmail:(nonnull NSString *)email + completion:(nullable FIRSignInMethodQueryCallback)completion { + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + FIRCreateAuthURIRequest *request = + [[FIRCreateAuthURIRequest alloc] initWithIdentifier:email + continueURI:@"http://www.google.com/" + requestConfiguration:_requestConfiguration]; + [FIRAuthBackend createAuthURI:request callback:^(FIRCreateAuthURIResponse *_Nullable response, + NSError *_Nullable error) { + if (completion) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(response.signinMethods, error); + }); + } + }]; + }); +} + - (void)signInWithEmail:(NSString *)email password:(NSString *)password completion:(FIRAuthResultCallback)completion { @@ -524,6 +552,23 @@ - (void)signInWithEmail:(NSString *)email }); } +- (void)signInWithEmail:(NSString *)email + link:(NSString *)link + completion:(FIRAuthDataResultCallback)completion { + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + FIRAuthDataResultCallback decoratedCallback = + [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; + FIREmailPasswordAuthCredential *credential = + [[FIREmailPasswordAuthCredential alloc] initWithEmail:email link:link]; + [self internalSignInAndRetrieveDataWithCredential:credential + isReauthentication:NO + callback:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + decoratedCallback(authResult, error); + }]; + }); +} + /** @fn signInWithEmail:password:callback: @brief Signs in using an email address and password. @param email The user's email address. @@ -536,6 +581,7 @@ - (void)signInWithEmail:(NSString *)email - (void)signInWithEmail:(NSString *)email password:(NSString *)password callback:(FIRAuthResultCallback)callback { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:email password:password @@ -591,6 +637,40 @@ - (void)internalSignInAndRetrieveDataWithEmail:(NSString *)email callback:completion]; } +/** @fn internalSignInWithEmail:link:completion: + @brief Signs in using an email and email sign-in link. + @param email The user's email address. + @param link The email sign-in link. + @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked + asynchronously on the global auth work queue in the future. + */ +- (void)internalSignInWithEmail:(nonnull NSString *)email + link:(nonnull NSString *)link + callback:(nullable FIRAuthResultCallback)callback { + NSURLComponents *urlComponents = [NSURLComponents componentsWithString:link]; + NSDictionary *queryItems = FIRAuthParseURL(urlComponents.query); + NSString *actionCode = queryItems[@"oobCode"]; + + FIREmailLinkSignInRequest *request = + [[FIREmailLinkSignInRequest alloc] initWithEmail:email + oobCode:actionCode + requestConfiguration:_requestConfiguration]; + + [FIRAuthBackend emailLinkSignin:request + callback:^(FIREmailLinkSignInResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + callback(nil, error); + return; + } + [self completeSignInWithAccessToken:response.IDToken + accessTokenExpirationDate:response.approximateExpirationDate + refreshToken:response.refreshToken + anonymous:NO + callback:callback]; + }]; +} + - (void)signInWithCredential:(FIRAuthCredential *)credential completion:(FIRAuthResultCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ @@ -628,24 +708,31 @@ - (void)internalSignInAndRetrieveDataWithCredential:(FIRAuthCredential *)credent // Special case for email/password credentials FIREmailPasswordAuthCredential *emailPasswordCredential = (FIREmailPasswordAuthCredential *)credential; - [self signInWithEmail:emailPasswordCredential.email - password:emailPasswordCredential.password - callback:^(FIRUser *_Nullable user, NSError *_Nullable error) { + FIRAuthResultCallback completeEmailSignIn = ^(FIRUser *user, NSError *error) { if (callback) { if (error) { callback(nil, error); return; } - FIRAdditionalUserInfo *additionalUserInfo = - [[FIRAdditionalUserInfo alloc] initWithProviderID:FIREmailAuthProviderID - profile:nil - username:nil - isNewUser:NO]; + FIRAdditionalUserInfo *additionalUserInfo = + [[FIRAdditionalUserInfo alloc] initWithProviderID:FIREmailAuthProviderID + profile:nil + username:nil + isNewUser:NO]; FIRAuthDataResult *result = [[FIRAuthDataResult alloc] initWithUser:user additionalUserInfo:additionalUserInfo]; - callback(result, nil); + callback(result, error); } - }]; + }; + if (emailPasswordCredential.link) { + [self internalSignInWithEmail:emailPasswordCredential.email + link:emailPasswordCredential.link + callback:completeEmailSignIn]; + } else { + [self signInWithEmail:emailPasswordCredential.email + password:emailPasswordCredential.password + callback:completeEmailSignIn]; + } return; } @@ -1013,6 +1100,31 @@ - (void)sendPasswordResetWithNullableActionCodeSettings:(nullable FIRActionCodeS }); } +- (void)sendSignInLinkToEmail:(nonnull NSString *)email + actionCodeSettings:(nonnull FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendSignInLinkToEmailCallback)completion { + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + if (!email) { + [FIRAuthExceptionUtils raiseInvalidParameterExceptionWithReason: + kMissingEmailInvalidParameterExceptionReason]; + } + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest signInWithEmailLinkRequest:email + actionCodeSettings:actionCodeSettings + requestConfiguration:_requestConfiguration]; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + if (completion) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(error); + }); + } + }]; + }); +} + + - (BOOL)signOut:(NSError *_Nullable __autoreleasing *_Nullable)error { __block BOOL result = YES; dispatch_sync(FIRAuthGlobalWorkQueue(), ^{ @@ -1031,6 +1143,50 @@ - (BOOL)signOutByForceWithUserID:(NSString *)userID error:(NSError *_Nullable *_ return [self updateCurrentUser:nil byForce:YES savingToDisk:YES error:error]; } +- (BOOL)isSignInWithEmailLink:(NSString *)link { + if (link.length == 0) { + return NO; + } + NSURLComponents *urlComponents = [NSURLComponents componentsWithString:link]; + if (!urlComponents.query) { + return NO; + } + NSDictionary *queryItems = FIRAuthParseURL(urlComponents.query); + + NSString *actionCode = queryItems[@"oobCode"]; + NSString *mode = queryItems[@"mode"]; + + if (actionCode && [mode isEqualToString:@"signIn"]) { + return YES; + } + return NO; +} + +/** @fn FIRAuthParseURL:NSString + @brief Parses an incoming URL into all available query items. + @param urlString The url to be parsed. + @return A dictionary of available query items in the target URL. + */ +static NSDictionary *FIRAuthParseURL(NSString *urlString) { + NSString *linkURL = [NSURLComponents componentsWithString:urlString].query; + NSArray *URLComponents = [linkURL componentsSeparatedByString:@"&"]; + NSMutableDictionary *queryItems = + [[NSMutableDictionary alloc] initWithCapacity:URLComponents.count]; + for (NSString *component in URLComponents) { + NSRange equalRange = [component rangeOfString:@"="]; + if (equalRange.location != NSNotFound) { + NSString *queryItemKey = + [[component substringToIndex:equalRange.location] stringByRemovingPercentEncoding]; + NSString *queryItemValue = + [[component substringFromIndex:equalRange.location + 1] stringByRemovingPercentEncoding]; + if (queryItemKey && queryItemValue) { + queryItems[queryItemKey] = queryItemValue; + } + } + } + return queryItems; +} + - (FIRAuthStateDidChangeListenerHandle)addAuthStateDidChangeListener: (FIRAuthStateDidChangeListenerBlock)listener { __block BOOL firstInvocation = YES; diff --git a/Firebase/Auth/Source/Public/FIRAuth.h b/Firebase/Auth/Source/Public/FIRAuth.h index f18a3d0b55d..0cc74ae5b82 100644 --- a/Firebase/Auth/Source/Public/FIRAuth.h +++ b/Firebase/Auth/Source/Public/FIRAuth.h @@ -114,6 +114,14 @@ typedef void (^FIRProviderQueryCallback)(NSArray *_Nullable provider NSError *_Nullable error) NS_SWIFT_NAME(ProviderQueryCallback); +/** @typedef FIRSignInMethodQueryCallback + @brief The type of block invoked when a list of sign-in methods for a given email address is + requested. + */ +typedef void (^FIRSignInMethodQueryCallback)(NSArray *_Nullable, + NSError *_Nullable) + NS_SWIFT_NAME(SignInMethodQueryCallback); + /** @typedef FIRSendPasswordResetCallback @brief The type of block invoked when sending a password reset email. @@ -123,6 +131,12 @@ typedef void (^FIRProviderQueryCallback)(NSArray *_Nullable provider typedef void (^FIRSendPasswordResetCallback)(NSError *_Nullable error) NS_SWIFT_NAME(SendPasswordResetCallback); +/** @typedef FIRSendSignInLinkToEmailCallback + @brief The type of block invoked when sending an email sign-in link email. + */ +typedef void (^FIRSendSignInLinkToEmailCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(SendSignInLinkToEmailCallback); + /** @typedef FIRConfirmPasswordResetCallback @brief The type of block invoked when performing a password reset. @@ -190,6 +204,10 @@ typedef NS_ENUM(NSInteger, FIRActionCodeOperation) { /** Action code for recover email operation. */ FIRActionCodeOperationRecoverEmail = 3, + /** Action code for email link operation. */ + FIRActionCodeOperationEmailLink = 4, + + } NS_SWIFT_NAME(ActionCodeOperation); /** @@ -297,6 +315,24 @@ NS_SWIFT_NAME(Auth) - (void)fetchProvidersForEmail:(NSString *)email completion:(nullable FIRProviderQueryCallback)completion; +/** @fn fetchSignInMethodsForEmail:completion: + @brief Fetches the list of all sign-in methods previously used for the provided email address. + + @param email The email address for which to obtain a list of sign-in methods. + @param completion Optionally; a block which is invoked when the list of sign in methods for the + specified email address is ready or an error was encountered. Invoked asynchronously on the + main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods. + */ + +- (void)fetchSignInMethodsForEmail:(NSString *)email + completion:(nullable FIRSignInMethodQueryCallback)completion; + /** @fn signInWithEmail:password:completion: @brief Signs in using an email address and password. @@ -322,6 +358,30 @@ NS_SWIFT_NAME(Auth) password:(NSString *)password completion:(nullable FIRAuthResultCallback)completion; +/** @fn signInWithEmail:link:completion: + @brief Signs in using an email address and email sign-in link. + + @param email The user's email address. + @param link The email sign-in link. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that email and email sign-in link + accounts are not enabled. Enable them in the Auth section of the + Firebase console. + + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is invalid. + + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ + +- (void)signInWithEmail:(NSString *)email + link:(NSString *)link + completion:(nullable FIRAuthDataResultCallback)completion; + /** @fn signInAndRetrieveDataWithEmail:password:completion: @brief Signs in using an email address and password. @@ -654,6 +714,19 @@ NS_SWIFT_NAME(Auth) actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings completion:(nullable FIRSendPasswordResetCallback)completion; +/** @fn sendSignInLinkToEmail:actionCodeSettings:completion: + @brief Sends a sign in with email link to provided email address. + + @param email The email address of the user. + @param actionCodeSettings An @c FIRActionCodeSettings object containing settings related to + handling action codes. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + */ +- (void)sendSignInLinkToEmail:(NSString *)email + actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendSignInLinkToEmailCallback)completion; + /** @fn signOut: @brief Signs out the current user. @@ -672,6 +745,14 @@ NS_SWIFT_NAME(Auth) */ - (BOOL)signOut:(NSError *_Nullable *_Nullable)error; +/** @fn isSignInWithEmailLink + @brief Checks if link is an email sign-in link. + + @param link The email sign-in link. + @return @YES when the link passed matches the expected format of an email sign-in link. + */ +- (BOOL)isSignInWithEmailLink:(NSString *)link; + /** @fn addAuthStateDidChangeListener: @brief Registers a block as an "auth state did change" listener. To be invoked when: diff --git a/Firebase/Auth/Source/Public/FIREmailAuthProvider.h b/Firebase/Auth/Source/Public/FIREmailAuthProvider.h index 99cd018f0ac..5823911e1d8 100644 --- a/Firebase/Auth/Source/Public/FIREmailAuthProvider.h +++ b/Firebase/Auth/Source/Public/FIREmailAuthProvider.h @@ -51,6 +51,15 @@ typedef FIREmailAuthProvider FIREmailPasswordAuthProvider __attribute__((depreca */ + (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password; +/** @fn credentialWithEmail:Link: + @brief Creates an `FIRAuthCredential` for an email & link sign in. + + @param email The user's email address. + @param link The email sign-in link. + @return A FIRAuthCredential containing the email & link credential. + */ ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email link:(NSString *)link; + /** @fn init @brief This class is not meant to be initialized. */ diff --git a/Firebase/Auth/Source/RPCs/FIRAuthBackend.h b/Firebase/Auth/Source/RPCs/FIRAuthBackend.h index a82c3a722aa..5928e71cc42 100644 --- a/Firebase/Auth/Source/RPCs/FIRAuthBackend.h +++ b/Firebase/Auth/Source/RPCs/FIRAuthBackend.h @@ -19,6 +19,8 @@ @class FIRAuthRequestConfiguration; @class FIRCreateAuthURIRequest; @class FIRCreateAuthURIResponse; +@class FIREmailLinkSignInRequest; +@class FIREmailLinkSignInResponse; @class FIRGetAccountInfoRequest; @class FIRGetAccountInfoResponse; @class FIRGetProjectConfigRequest; @@ -130,6 +132,16 @@ typedef void (^FIRVerifyAssertionResponseCallback) typedef void (^FIRVerifyPasswordResponseCallback) (FIRVerifyPasswordResponse *_Nullable response, NSError *_Nullable error); +/** @typedef FIREmailLinkSigninResponseCallback + @brief The type of block used to return the result of a call to the emailLinkSignin + endpoint. + @param response The received response, if any. + @param error The error which occurred, if any. + @remarks One of response or error will be non-nil. + */ +typedef void (^FIREmailLinkSigninResponseCallback) + (FIREmailLinkSignInResponse *_Nullable response, NSError *_Nullable error); + /** @typedef FIRVerifyCustomTokenResponseCallback @brief The type of block used to return the result of a call to the verifyCustomToken endpoint. @@ -296,6 +308,15 @@ typedef void (^FIRVerifyClientResponseCallback) + (void)verifyPassword:(FIRVerifyPasswordRequest *)request callback:(FIRVerifyPasswordResponseCallback)callback; +/** @fn emailLinkSignin:callback: + @brief Calls the emailLinkSignin endpoint, which is responsible for authenticating a + user through passwordless sign-in. + @param request The request parameters. + @param callback The callback. + */ ++ (void)emailLinkSignin:(FIREmailLinkSignInRequest *)request + callback:(FIREmailLinkSigninResponseCallback)callback; + /** @fn secureToken:callback: @brief Calls the token endpoint, which is responsible for performing STS token exchanges and token refreshes. @@ -461,6 +482,15 @@ typedef void (^FIRVerifyClientResponseCallback) - (void)verifyPassword:(FIRVerifyPasswordRequest *)request callback:(FIRVerifyPasswordResponseCallback)callback; +/** @fn emailLinkSignin:callback: + @brief Calls the emailLinkSignin endpoint, which is responsible for authenticating a + user through passwordless sign-in. + @param request The request parameters. + @param callback The callback. + */ +- (void)emailLinkSignin:(FIREmailLinkSignInRequest *)request + callback:(FIREmailLinkSigninResponseCallback)callback; + /** @fn secureToken:callback: @brief Calls the token endpoint, which is responsible for performing STS token exchanges and token refreshes. @@ -472,7 +502,7 @@ typedef void (^FIRVerifyClientResponseCallback) /** @fn getOOBConfirmationCode:callback: @brief Calls the getOOBConfirmationCode endpoint, which is responsible for sending email change - request emails, and password reset emails. + request emails, email sign-in link emails, and password reset emails. @param request The request parameters. @param callback The callback. */ diff --git a/Firebase/Auth/Source/RPCs/FIRAuthBackend.m b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m index 6b5232b89d7..e380e34f896 100644 --- a/Firebase/Auth/Source/RPCs/FIRAuthBackend.m +++ b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m @@ -51,6 +51,8 @@ #import "FIRVerifyCustomTokenResponse.h" #import "FIRVerifyPasswordRequest.h" #import "FIRVerifyPasswordResponse.h" +#import "FIREmailLinkSignInRequest.h" +#import "FIREmailLinkSignInResponse.h" #import "FIRVerifyPhoneNumberRequest.h" #import "FIRVerifyPhoneNumberResponse.h" #import @@ -430,6 +432,11 @@ + (void)verifyPassword:(FIRVerifyPasswordRequest *)request [[self implementation] verifyPassword:request callback:callback]; } ++ (void)emailLinkSignin:(FIREmailLinkSignInRequest *)request + callback:(FIREmailLinkSigninResponseCallback)callback { + [[self implementation] emailLinkSignin:request callback:callback]; +} + + (void)secureToken:(FIRSecureTokenRequest *)request callback:(FIRSecureTokenResponseCallback)callback { [[self implementation] secureToken:request callback:callback]; @@ -623,6 +630,18 @@ - (void)verifyPassword:(FIRVerifyPasswordRequest *)request }]; } +- (void)emailLinkSignin:(FIREmailLinkSignInRequest *)request + callback:(FIREmailLinkSigninResponseCallback)callback { + FIREmailLinkSignInResponse *response = [[FIREmailLinkSignInResponse alloc] init]; + [self postWithRequest:request response:response callback:^(NSError *error) { + if (error) { + callback(nil, error); + } else { + callback(response, nil); + } + }]; +} + - (void)secureToken:(FIRSecureTokenRequest *)request callback:(FIRSecureTokenResponseCallback)callback { FIRSecureTokenResponse *response = [[FIRSecureTokenResponse alloc] init]; diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h index 9f6cbae01c0..8e8f7b0f0bd 100644 --- a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h +++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h @@ -51,6 +51,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, copy, readonly, nullable) NSArray *allProviders; +/** @property signinMethods + @brief A list of sign-in methods available for the passed @c identifier. + */ +@property(nonatomic, copy, readonly, nullable) NSArray *signinMethods; + @end NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m index 12ef97c3e6f..6f2937f504f 100644 --- a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m @@ -25,6 +25,7 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary _registered = [dictionary[@"registered"] boolValue]; _forExistingProvider = [dictionary[@"forExistingProvider"] boolValue]; _allProviders = [dictionary[@"allProviders"] copy]; + _signinMethods = [dictionary[@"signinMethods"] copy]; return YES; } diff --git a/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.h b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.h new file mode 100644 index 00000000000..e1b10d8a4fe --- /dev/null +++ b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.h @@ -0,0 +1,66 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "FIRAuthRPCRequest.h" +#import "FIRIdentityToolkitRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIREmailLinkSignInRequest + @brief Represents the parameters for the emailLinkSignin endpoint. + */ +@interface FIREmailLinkSignInRequest : FIRIdentityToolkitRequest + +#pragma mark - Components of "postBody" + +/** @property email + @brief The email identifier used to complete the email link sign-in. + */ +@property(nonatomic, copy, readonly) NSString *email; + +/** @property oobCode + @brief The OOB code used to complete the email link sign-in flow. + */ +@property(nonatomic, copy, readonly) NSString *oobCode; + +/** @property idToken + @brief The ID Token code potentially used to complete the email link sign-in flow. + */ +@property(nonatomic, copy) NSString *IDToken; + +/** @fn initWithEndpoint:requestConfiguration: + @brief Please use initWithProviderID:requestConfifuration instead. + */ +- (instancetype)initWithEndpoint:(NSString *)endpoint + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration NS_UNAVAILABLE; + +/** @fn initWithProviderID:requestConfifuration + @brief Designated initializer. + @param email The email identifier used to complete hte email link sign-in flow. + @param oobCode The OOB code used to complete the email link sign-in flow. + @param requestConfiguration An object containing configurations to be added to the request. + + */ +- (instancetype)initWithEmail:(NSString *)email + oobCode:(NSString *)oobCode + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration + NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.m b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.m new file mode 100644 index 00000000000..9787e8e57b5 --- /dev/null +++ b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.m @@ -0,0 +1,70 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIREmailLinkSignInRequest.h" + +/** @var kEmailLinkSigninEndpoint + @brief The "EmailLinkSignin" endpoint. + */ +static NSString *const kEmailLinkSigninEndpoint = @"emailLinkSignin"; + +/** @var kEmailKey + @brief The key for the "identifier" value in the request. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kEmailLinkKey + @brief The key for the "emailLink" value in the request. + */ +static NSString *const kOOBCodeKey = @"oobCode"; + +/** @var kIDTokenKey + @brief The key for the "IDToken" value in the request. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kPostBodyKey + @brief The key for the "postBody" value in the request. + */ +static NSString *const kPostBodyKey = @"postBody"; + +@implementation FIREmailLinkSignInRequest + +- (instancetype)initWithEmail:(NSString *)email + oobCode:(NSString *)oobCode + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { + self = [super initWithEndpoint:kEmailLinkSigninEndpoint + requestConfiguration:requestConfiguration]; + if (self) { + _email = email; + _oobCode = oobCode; + } + return self; +} + +- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error { + NSMutableDictionary *postBody = [@{ + kEmailKey : _email, + kOOBCodeKey : _oobCode, + } mutableCopy]; + + if (_IDToken) { + postBody[kIDTokenKey] = _IDToken; + } + return postBody; +} + +@end diff --git a/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.h b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.h new file mode 100644 index 00000000000..df0a127bc2a --- /dev/null +++ b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.h @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#import + +#import "FIRAuthRPCResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRVerifyAssertionResponse + @brief Represents the response from the emailLinkSignin endpoint. + */ +@interface FIREmailLinkSignInResponse : NSObject + +/** @property IDToken + @brief The ID token in the email link sign-in response. + */ +@property(nonatomic, copy, readonly) NSString *IDToken; + +/** @property email + @brief The email returned by the IdP. + */ +@property(nonatomic, strong, readonly, nullable) NSString *email; + +/** @property refreshToken + @brief The refreshToken returned by the server. + */ +@property(nonatomic, strong, readonly, nullable) NSString *refreshToken; + +/** @property approximateExpirationDate + @brief The approximate expiration date of the access token. + */ +@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate; + +/** @property isNewUser + @brief Flag indicating that the user signing in is a new user and not a returning user. + */ +@property(nonatomic, assign) BOOL isNewUser; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.m b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.m new file mode 100644 index 00000000000..cd36d41c3b9 --- /dev/null +++ b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.m @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIREmailLinkSignInResponse.h" + +@implementation FIREmailLinkSignInResponse + +- (BOOL)setWithDictionary:(NSDictionary *)dictionary + error:(NSError *_Nullable *_Nullable)error { + _email = [dictionary[@"email"] copy]; + _IDToken = [dictionary[@"idToken"] copy]; + _isNewUser = [dictionary[@"isNewUser"] boolValue]; + _refreshToken = [dictionary[@"refreshToken"] copy]; + _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ? + [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil; + return YES; +} + +@end diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h index abd59b40977..751cfe79ca3 100644 --- a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h +++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h @@ -36,6 +36,11 @@ typedef NS_ENUM(NSInteger, FIRGetOOBConfirmationCodeRequestType) { @brief Requests an email verification code. */ FIRGetOOBConfirmationCodeRequestTypeVerifyEmail, + + /** @var FIRGetOOBConfirmationCodeRequestTypeEmailLink + @brief Requests an email sign-in link. + */ + FIRGetOOBConfirmationCodeRequestTypeEmailLink, }; /** @enum FIRGetOOBConfirmationCodeRequest @@ -91,7 +96,7 @@ typedef NS_ENUM(NSInteger, FIRGetOOBConfirmationCodeRequestType) { */ @property(assign, nonatomic) BOOL handleCodeInApp; -/** @fn passwordResetRequestWithEmail:APIKey: +/** @fn passwordResetRequestWithEmail:actionCodeSettings:requestConfiguration: @brief Creates a password reset request. @param email The user's email address. @param actionCodeSettings An object of FIRActionCodeSettings which specifies action code @@ -104,7 +109,7 @@ typedef NS_ENUM(NSInteger, FIRGetOOBConfirmationCodeRequestType) { actionCodeSettings:(nullable FIRActionCodeSettings *)actionCodeSettings requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration; -/** @fn verifyEmailRequestWithAccessToken:APIKey: +/** @fn verifyEmailRequestWithAccessToken:actionCodeSettings:requestConfiguration: @brief Creates a password reset request. @param accessToken The user's STS Access Token. @param actionCodeSettings An object of FIRActionCodeSettings which specifies action code @@ -117,6 +122,19 @@ typedef NS_ENUM(NSInteger, FIRGetOOBConfirmationCodeRequestType) { actionCodeSettings:(nullable FIRActionCodeSettings *)actionCodeSettings requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration; +/** @fn signInWithEmailLinkRequest:actionCodeSettings:requestConfiguration: + @brief Creates a sign-in with email link. + @param email The user's email address. + @param actionCodeSettings An object of FIRActionCodeSettings which specifies action code + settings to be applied to the email sign-in link. + @param requestConfiguration An object containing configurations to be added to the request. + @return An email sign-in link request. + */ ++ (nullable FIRGetOOBConfirmationCodeRequest *) + signInWithEmailLinkRequest:(NSString *)email + actionCodeSettings:(nullable FIRActionCodeSettings *)actionCodeSettings + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration; + /** @fn init @brief Please use a factory method. */ diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m index 653edddfc47..438f24b720d 100644 --- a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m @@ -79,6 +79,11 @@ */ static NSString *const kPasswordResetRequestTypeValue = @"PASSWORD_RESET"; +/** @var kEmailLinkSignInTypeValue + @brief The value for the "EMAIL_SIGNIN" request type. + */ +static NSString *const kEmailLinkSignInTypeValue= @"EMAIL_SIGNIN"; + /** @var kVerifyEmailRequestTypeValue @brief The value for the "VERIFY_EMAIL" request type. */ @@ -116,6 +121,8 @@ + (NSString *)requestTypeStringValueForRequestType: return kPasswordResetRequestTypeValue; case FIRGetOOBConfirmationCodeRequestTypeVerifyEmail: return kVerifyEmailRequestTypeValue; + case FIRGetOOBConfirmationCodeRequestTypeEmailLink: + return kEmailLinkSignInTypeValue; // No default case so that we get a compiler warning if a new value was added to the enum. } } @@ -142,6 +149,17 @@ + (NSString *)requestTypeStringValueForRequestType: requestConfiguration:requestConfiguration]; } ++ (FIRGetOOBConfirmationCodeRequest *) + signInWithEmailLinkRequest:(NSString *)email + actionCodeSettings:(nullable FIRActionCodeSettings *)actionCodeSettings + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { + return [[self alloc] initWithRequestType:FIRGetOOBConfirmationCodeRequestTypeEmailLink + email:email + accessToken:nil + actionCodeSettings:actionCodeSettings + requestConfiguration:requestConfiguration]; +} + - (nullable instancetype)initWithRequestType:(FIRGetOOBConfirmationCodeRequestType)requestType email:(nullable NSString *)email accessToken:(nullable NSString *)accessToken @@ -180,6 +198,12 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) body[kIDTokenKey] = _accessToken; } + // For email sign-in link requests, we only need an email address in addition to the already + // required fields. + if (_requestType == FIRGetOOBConfirmationCodeRequestTypeEmailLink) { + body[kEmailKey] = _email; + } + if (_continueURL) { body[kContinueURLKey] = _continueURL; } From fc4910abc88fa28d891b09887e9b68c19d0bc00b Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 6 Mar 2018 17:09:19 -0800 Subject: [PATCH 2/2] remove trailing spaces --- Example/Auth/Tests/FIRAuthTests.m | 2 +- Firebase/Auth/Source/FIRAuth.m | 2 +- Firebase/Auth/Source/Public/FIRAuth.h | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Example/Auth/Tests/FIRAuthTests.m b/Example/Auth/Tests/FIRAuthTests.m index 27edfcf0b42..914c58b7404 100644 --- a/Example/Auth/Tests/FIRAuthTests.m +++ b/Example/Auth/Tests/FIRAuthTests.m @@ -1715,7 +1715,7 @@ - (void)testSendPasswordResetEmailFailure { } /** @fn testSendSignInLinkToEmailSuccess - @brief Tests the flow of a successful @c sendSignInLinkToEmail:actionCodeSettings:completion: + @brief Tests the flow of a successful @c sendSignInLinkToEmail:actionCodeSettings:completion: call. */ - (void)testSendSignInLinkToEmailSuccess { diff --git a/Firebase/Auth/Source/FIRAuth.m b/Firebase/Auth/Source/FIRAuth.m index 4305ea5fa18..387fab7932a 100644 --- a/Firebase/Auth/Source/FIRAuth.m +++ b/Firebase/Auth/Source/FIRAuth.m @@ -714,7 +714,7 @@ - (void)internalSignInAndRetrieveDataWithCredential:(FIRAuthCredential *)credent callback(nil, error); return; } - FIRAdditionalUserInfo *additionalUserInfo = + FIRAdditionalUserInfo *additionalUserInfo = [[FIRAdditionalUserInfo alloc] initWithProviderID:FIREmailAuthProviderID profile:nil username:nil diff --git a/Firebase/Auth/Source/Public/FIRAuth.h b/Firebase/Auth/Source/Public/FIRAuth.h index 0cc74ae5b82..c9856580925 100644 --- a/Firebase/Auth/Source/Public/FIRAuth.h +++ b/Firebase/Auth/Source/Public/FIRAuth.h @@ -206,7 +206,7 @@ typedef NS_ENUM(NSInteger, FIRActionCodeOperation) { /** Action code for email link operation. */ FIRActionCodeOperationEmailLink = 4, - + } NS_SWIFT_NAME(ActionCodeOperation); @@ -317,12 +317,12 @@ NS_SWIFT_NAME(Auth) /** @fn fetchSignInMethodsForEmail:completion: @brief Fetches the list of all sign-in methods previously used for the provided email address. - + @param email The email address for which to obtain a list of sign-in methods. @param completion Optionally; a block which is invoked when the list of sign in methods for the specified email address is ready or an error was encountered. Invoked asynchronously on the main thread in the future. - + @remarks Possible error codes: + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. @@ -716,7 +716,7 @@ NS_SWIFT_NAME(Auth) /** @fn sendSignInLinkToEmail:actionCodeSettings:completion: @brief Sends a sign in with email link to provided email address. - + @param email The email address of the user. @param actionCodeSettings An @c FIRActionCodeSettings object containing settings related to handling action codes.