From 8ef0f1490a72fd700f609dc9971ec16868d6747b Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 7 Mar 2018 07:47:09 -0800 Subject: [PATCH] Adds Email link sign-in (#882) --- 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..914c58b7404 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..387fab7932a 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..c9856580925 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; }