diff options
author | 2017-09-04 17:37:46 -0700 | |
---|---|---|
committer | 2017-09-04 17:37:46 -0700 | |
commit | 85e81371801f9cd3e94a1505ce3b2f4f66086b08 (patch) | |
tree | 220fdf17ff9aed20e865bf019dd7dad805458fab | |
parent | 540e21c0cc3206a8e911554227cc1f081ef40dda (diff) |
Adds app verification alternative (#228)
* Adds app verification alternative
-rw-r--r-- | AuthSamples/Sample/MainViewController.m | 119 | ||||
-rw-r--r-- | Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m | 3 | ||||
-rw-r--r-- | Example/Auth/Tests/FIRPhoneAuthProviderTests.m | 110 | ||||
-rw-r--r-- | Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m | 237 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuth.m | 14 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuthURLPresenter.m | 2 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuth_Internal.h | 8 | ||||
-rw-r--r-- | Firebase/Auth/Source/Public/FIRAuth.h | 12 | ||||
-rw-r--r-- | Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h | 24 | ||||
-rw-r--r-- | Firebase/Auth/Source/RPCs/FIRAuthBackend.h | 6 | ||||
-rw-r--r-- | Firebase/Auth/Source/RPCs/FIRAuthBackend.m | 8 |
11 files changed, 492 insertions, 51 deletions
diff --git a/AuthSamples/Sample/MainViewController.m b/AuthSamples/Sample/MainViewController.m index bb8b07d..fb9bf67 100644 --- a/AuthSamples/Sample/MainViewController.m +++ b/AuthSamples/Sample/MainViewController.m @@ -37,6 +37,12 @@ #import "UserInfoViewController.h" #import "UserTableViewCell.h" + +/*! @typedef textInputCompletionBlock + @brief The type of callback used to report text input prompt results. + */ +typedef void (^textInputCompletionBlock)(NSString *_Nullable userInput); + /** @var kTokenGetButtonText @brief The text of the "Get Token" button. */ @@ -506,6 +512,11 @@ static NSString *const kPhoneAuthSectionTitle = @"Phone Auth"; */ static NSString *const kPhoneNumberSignInTitle = @"Sign in With Phone Number"; +/** @var kPhoneNumberSignInTitle + @brief The title for button to sign in with phone number using reCAPTCHA. + */ +static NSString *const kPhoneNumberSignInReCaptchaTitle = @"Sign in With Phone Number (reCAPTCHA)"; + /** @typedef showEmailPasswordDialogCompletion @brief The type of block which gets called to complete the Email/Password dialog flow. */ @@ -644,6 +655,8 @@ typedef enum { action:^{ [weakSelf presentSettings]; }] ]], [StaticContentTableViewSection sectionWithTitle:kPhoneAuthSectionTitle cells:@[ + [StaticContentTableViewCell cellWithTitle:kPhoneNumberSignInReCaptchaTitle + action:^{ [weakSelf signInWithPhoneNumberRecaptcha]; }], [StaticContentTableViewCell cellWithTitle:kPhoneNumberSignInTitle action:^{ [weakSelf signInWithPhoneNumber]; }], [StaticContentTableViewCell cellWithTitle:kUpdatePhoneNumber @@ -2399,14 +2412,9 @@ static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) { @brief Allows sign in with phone number. */ - (void)signInWithPhoneNumber { - [self showTextInputPromptWithMessage:@"Phone #:" - keyboardType:UIKeyboardTypePhonePad - completionBlock:^(BOOL userPressedOK, NSString *_Nullable phoneNumber) { - if (!userPressedOK || !phoneNumber.length) { - return; - } + [self commonPhoneNumberInputWithTitle:@"Phone #" Completion:^(NSString *_Nullable phone) { [self showSpinner:^{ - [[AppManager phoneAuthProvider] verifyPhoneNumber:phoneNumber + [[AppManager phoneAuthProvider] verifyPhoneNumber:phone completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { [self hideSpinner:^{ @@ -2417,29 +2425,10 @@ static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) { } [self logSuccess:@"Code sent"]; - [self showTextInputPromptWithMessage:@"Verification code:" - keyboardType:UIKeyboardTypeNumberPad - completionBlock:^(BOOL userPressedOK, - NSString *_Nullable verificationCode) { - if (!userPressedOK || !verificationCode.length) { - return; - } - [self showSpinner:^{ - FIRAuthCredential *credential = - [[AppManager phoneAuthProvider] credentialWithVerificationID:verificationID - verificationCode:verificationCode]; - [[AppManager auth] signInWithCredential:credential - completion:^(FIRUser *_Nullable user, - NSError *_Nullable error) { - [self hideSpinner:^{ - if (error) { - [self logFailure:@"failed to verify phone number" error:error]; - [self showMessagePrompt:error.localizedDescription]; - return; - } - }]; - }]; - }]; + [self commonPhoneNumberInputWithTitle:@"Code" + Completion:^(NSString *_Nullable verificationCode) { + [self commontPhoneVerificationWithVerificationID:verificationID + verificationCode:verificationCode]; }]; }]; }]; @@ -2447,6 +2436,76 @@ static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) { }]; } +/** @fn signInWithPhoneNumberRecaptcha + @brief Allows sign in with phone number using reCAPTCHA + */ +- (void)signInWithPhoneNumberRecaptcha { + [self commonPhoneNumberInputWithTitle:@"Phone #" Completion:^(NSString *_Nullable phone) { + [self showSpinner:^{ + [[AppManager phoneAuthProvider] verifyPhoneNumber:phone + UIDelegate:nil + completion:^(NSString *_Nullable verificationID, + NSError *_Nullable error) { + [self hideSpinner:^{ + if (error) { + [self logFailure:@"failed to send verification code" error:error]; + [self showMessagePrompt:error.localizedDescription]; + return; + } + [self logSuccess:@"Code sent"]; + + [self commonPhoneNumberInputWithTitle:@"Code" + Completion:^(NSString *_Nullable verificationCode) { + [self commontPhoneVerificationWithVerificationID:verificationID + verificationCode:verificationCode]; + }]; + }]; + }]; + }]; + }]; +} + +/** @fn commonPhoneNumberInputWithLabel:Completion + @brief Allows user input into a text field. + @param title of the promt. + */ +- (void)commonPhoneNumberInputWithTitle:(NSString *)title + Completion:(textInputCompletionBlock)completion { + [self showTextInputPromptWithMessage:title + keyboardType:UIKeyboardTypePhonePad + completionBlock:^(BOOL userPressedOK, NSString *_Nullable phoneNumber) { + if (!userPressedOK || !phoneNumber.length) { + return; + } + completion(phoneNumber); + }]; +} + +/** @fn commonPhoneNumberInputWithLabel:Completion + @brief Finishes the phone number verification flow. + @param verificationID The verificationID from the backend. + @param verificationCode The verificationCode from the SMS message. + */ +- (void)commontPhoneVerificationWithVerificationID:(NSString *)verificationID + verificationCode:(NSString *)verificationCode { + [self showSpinner:^{ + FIRAuthCredential *credential = + [[AppManager phoneAuthProvider] credentialWithVerificationID:verificationID + verificationCode:verificationCode]; + [[AppManager auth] signInWithCredential:credential + completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + [self hideSpinner:^{ + if (error) { + [self logFailure:@"failed to verify phone number" error:error]; + [self showMessagePrompt:error.localizedDescription]; + return; + } + }]; + }]; + }]; +} + /** @fn updatePhoneNumber @brief Allows adding a verified phone number to the currently signed user. */ diff --git a/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m index 7159452..b6d34bb 100644 --- a/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m +++ b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m @@ -28,7 +28,8 @@ return [[FIROptions alloc] initInternalWithOptionsDictionary:@{ @"GOOGLE_APP_ID" : @"1:1085102361755:ios:f790a919483d5bdf", @"API_KEY" : @"FAKE_API_KEY", - @"GCM_SENDER_ID": @"217397612173" + @"GCM_SENDER_ID": @"217397612173", + @"CLIENT_ID" : @"123456.apps.googleusercontent.com", }]; } diff --git a/Example/Auth/Tests/FIRPhoneAuthProviderTests.m b/Example/Auth/Tests/FIRPhoneAuthProviderTests.m index 9a0a219..5b29b02 100644 --- a/Example/Auth/Tests/FIRPhoneAuthProviderTests.m +++ b/Example/Auth/Tests/FIRPhoneAuthProviderTests.m @@ -20,6 +20,8 @@ #import "FIRPhoneAuthProvider.h" #import "Phone/FIRPhoneAuthCredential_Internal.h" #import "Phone/NSString+FIRAuth.h" +#import "FIRApp.h" +#import "FIRApp+FIRAuthUnitTests.h" #import "FIRAuthAPNSToken.h" #import "FIRAuthAPNSTokenManager.h" #import "FIRAuthAppCredential.h" @@ -30,14 +32,20 @@ #import "FIRAuthErrorUtils.h" #import "FIRAuthGlobalWorkQueue.h" #import "FIRAuthBackend.h" +#import "FIRAuthURLPresenter.h" +#import "FIRGetProjectConfigRequest.h" +#import "FIRGetProjectConfigResponse.h" #import "FIRSendVerificationCodeRequest.h" #import "FIRSendVerificationCodeResponse.h" +#import "FIRAuthUIDelegate.h" #import "FIRVerifyClientRequest.h" #import "FIRVerifyClientResponse.h" #import "FIRApp+FIRAuthUnitTests.h" #import "OCMStubRecorder+FIRAuthUnitTests.h" #import <OCMock/OCMock.h> +@import SafariServices; + NS_ASSUME_NONNULL_BEGIN /** @var kTestPhoneNumber @@ -75,12 +83,23 @@ static NSString *const kTestOldReceipt = @"old_receipt"; */ static NSString *const kTestOldSecret = @"old_secret"; - /** @var kTestVerificationCode @brief A fake verfication code. */ static NSString *const kTestVerificationCode = @"verificationCode"; +/** @var kFakeReCAPTCHAToken + @brief A fake reCAPTCHA token. + */ +static NSString *const kFakeReCAPTCHAToken = @"fakeReCAPTCHAToken"; + +/** @var kFakeRedirectURLStringFormat + @brief The format for a fake redirect URL string. + */ +static NSString *const kFakeRedirectURLStringWithoutReCAPTCHAToken = @"com.googleusercontent.apps.1" + "23456://firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" + "lback%3FauthType%3DverifyApp%26recaptchaToken%3D"; + /** @var kTestTimeout @brief A fake timeout value for waiting for push notification. */ @@ -141,6 +160,7 @@ static const NSTimeInterval kExpectationTimeout = 1; _mockNotificationManager = OCMClassMock([FIRAuthNotificationManager class]); OCMStub([_mockAuth notificationManager]).andReturn(_mockNotificationManager); _provider = [FIRPhoneAuthProvider providerWithAuth:_mockAuth]; + OCMStub([_mockAuth authURLPresenter]).andReturn([FIRAuthURLPresenter new]); } - (void)tearDown { @@ -248,6 +268,94 @@ static const NSTimeInterval kExpectationTimeout = 1; OCMVerifyAll(_mockAppCredentialManager); } +/** @fn testVerifyPhoneNumberUIDelegate + @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:. + */ +- (void)testVerifyPhoneNumberUIDelegate { + if ([SFSafariViewController class]) { + FIRApp *app = [FIRApp appForAuthUnitTestsWithName:@"testApp"]; + OCMStub([_mockAuth app]).andReturn(app); + // Simulate missing app token error. + OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); }); + OCMExpect([_mockAppCredentialManager credential]).andReturn(nil); + OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthAPNSTokenCallback callback) { + NSError *error = [NSError errorWithDomain:FIRAuthErrorDomain + code:FIRAuthErrorCodeMissingAppToken + userInfo:nil]; + callback(nil, error); + }); + OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetProjectConfigRequest *request, + FIRGetProjectConfigResponseCallback callback) { + XCTAssertNotNil(request); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetProjectConfigResponse = OCMClassMock([FIRGetProjectConfigResponse class]); + OCMStub([mockGetProjectConfigResponse authorizedDomains]). + andReturn(@[ @"test.firebaseapp.com"]); + callback(mockGetProjectConfigResponse, nil); + }); + }); + // Mock UIDelegate. + id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate)); + // Expect view controller presentation by UIDelegate. + id presenterArg = [OCMArg isKindOfClass:[SFSafariViewController class]]; + OCMExpect([mockUIDelegate presentViewController:presenterArg + animated:YES + completion:nil]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `presentViewController` is at index 2. + [invocation getArgument:&unretainedArgument atIndex:2]; + SFSafariViewController *viewController = unretainedArgument; + XCTAssertEqual(viewController.delegate, [_mockAuth authURLPresenter]); + XCTAssertTrue([viewController isKindOfClass:[SFSafariViewController class]]); + NSMutableString *fakeRedirectURLString = + [NSMutableString stringWithString:kFakeRedirectURLStringWithoutReCAPTCHAToken]; + [fakeRedirectURLString appendString:kFakeReCAPTCHAToken]; + [[_mockAuth authURLPresenter] canHandleURL:[NSURL URLWithString:fakeRedirectURLString]]; + }); + // Expect view controller dismissal by UIDelegate. + OCMExpect([mockUIDelegate dismissViewControllerAnimated:OCMOCK_ANY completion:OCMOCK_ANY]). + andDo(^(NSInvocation *invocation) { + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `completion` is at index 3. + [invocation getArgument:&unretainedArgument atIndex:3]; + void (^finishBlock)() = unretainedArgument; + finishBlock(); + }); + + OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSendVerificationCodeRequest *request, + FIRSendVerificationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber); + XCTAssertNil(request.appCredential); + XCTAssertEqualObjects(request.reCAPTCHAToken, kFakeReCAPTCHAToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSendVerificationCodeResponse = OCMClassMock([FIRSendVerificationCodeResponse class]); + OCMStub([mockSendVerificationCodeResponse verificationID]).andReturn(kTestVerificationID); + callback(mockSendVerificationCodeResponse, nil); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider verifyPhoneNumber:kTestPhoneNumber + UIDelegate:mockUIDelegate + completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + XCTAssertEqualObjects(verificationID, kTestVerificationID); + XCTAssertEqualObjects(verificationID.fir_authPhoneNumber, kTestPhoneNumber); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + OCMVerifyAll(_mockNotificationManager); + } +} + /** @fn testNotForwardingNotification @brief Tests returning an error for the app failing to forward notification. */ diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m index 4a0e7e7..1fe4d15 100644 --- a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m @@ -19,22 +19,37 @@ #import "FIRLogger.h" #import "FIRPhoneAuthCredential_Internal.h" #import "NSString+FIRAuth.h" +#import "FIRApp.h" #import "FIRAuthAPNSToken.h" #import "FIRAuthAPNSTokenManager.h" #import "FIRAuthAppCredential.h" #import "FIRAuthAppCredentialManager.h" #import "FIRAuthGlobalWorkQueue.h" #import "FIRAuth_Internal.h" +#import "FIRAuthURLPresenter.h" #import "FIRAuthNotificationManager.h" #import "FIRAuthErrorUtils.h" #import "FIRAuthBackend.h" +#import "FirebaseAuthVersion.h" +#import "FIROptions.h" +#import "FIRGetProjectConfigRequest.h" +#import "FIRGetProjectConfigResponse.h" #import "FIRSendVerificationCodeRequest.h" #import "FIRSendVerificationCodeResponse.h" #import "FIRVerifyClientRequest.h" #import "FIRVerifyClientResponse.h" +#import <GoogleToolboxForMac/GTMNSDictionary+URLArguments.h> NS_ASSUME_NONNULL_BEGIN +/** @typedef FIRReCAPTCHAURLCallBack + @brief The callback invoked at the end of the flow to fetch a reCAPTCHA URL. + @param reCAPTCHAURL The reCAPTCHA URL. + @param error The error that occured while fetching the reCAPTCHAURL, if any. + */ +typedef void (^FIRReCAPTCHAURLCallBack)(NSURL *_Nullable reCAPTCHAURL, + NSError *_Nullable error); + /** @typedef FIRVerifyClientCallback @brief The callback invoked at the end of a client verification flow. @param appCredential credential that proves the identity of the app during a phone @@ -44,6 +59,28 @@ NS_ASSUME_NONNULL_BEGIN typedef void (^FIRVerifyClientCallback)(FIRAuthAppCredential *_Nullable appCredential, NSError *_Nullable error); +/** @typedef FIRFetchAuthDomainCallback + @brief The callback invoked at the end of the flow to fetch the Auth domain. + @param authDomain The Auth domain. + @param error The error that occured while fetching the auth domain, if any. + */ +typedef void (^FIRFetchAuthDomainCallback)(NSString *_Nullable authDomain, + NSError *_Nullable error); +/** @var kAuthDomainSuffix + @brief The suffix of the auth domain pertiaining to a given Firebase project. + */ +static NSString *const kAuthDomainSuffix = @"firebaseapp.com"; + +/** @var kauthTypeVerifyApp + @brief The auth type to be specified in the app verification request. + */ +static NSString *const kAuthTypeVerifyApp = @"verifyApp"; + +/** @var kReCAPTCHAURLStringFormat + @brief The format of the URL used to open the reCAPTCHA page during app verification. + */ +NSString *const kReCAPTCHAURLStringFormat = @"https://%@/__/auth/handler?%@"; + @implementation FIRPhoneAuthProvider { /** @var _auth @@ -68,6 +105,21 @@ typedef void (^FIRVerifyClientCallback)(FIRAuthAppCredential *_Nullable appCrede - (void)verifyPhoneNumber:(NSString *)phoneNumber completion:(nullable FIRVerificationResultCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + [self internalVerifyPhoneNumber:phoneNumber completion:^(NSString *_Nullable verificationID, + NSError *_Nullable error) { + if (completion) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(verificationID, error); + }); + } + }]; + }); +} + +- (void)verifyPhoneNumber:(NSString *)phoneNumber + UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate + completion:(nullable FIRVerificationResultCallback)completion { + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ FIRVerificationResultCallback callBackOnMainThread = ^(NSString *_Nullable verificationID, NSError *_Nullable error) { if (completion) { @@ -76,21 +128,59 @@ typedef void (^FIRVerifyClientCallback)(FIRAuthAppCredential *_Nullable appCrede }); } }; - - if (!phoneNumber.length) { - callBackOnMainThread(nil, - [FIRAuthErrorUtils missingPhoneNumberErrorWithMessage:nil]); - return; - } - [_auth.notificationManager checkNotificationForwardingWithCallback: - ^(BOOL isNotificationBeingForwarded) { - if (!isNotificationBeingForwarded) { - callBackOnMainThread(nil, [FIRAuthErrorUtils notificationNotForwardedError]); + [self internalVerifyPhoneNumber:phoneNumber completion:^(NSString *_Nullable verificationID, + NSError *_Nullable error) { + if (!error) { + callBackOnMainThread(verificationID, nil); return; } - [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber - retryOnInvalidAppCredential:YES - callback:callBackOnMainThread]; + NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey]; + BOOL isInvalidAppCredential = error.code == FIRAuthErrorCodeInternalError && + underlyingError.code == FIRAuthErrorCodeInvalidAppCredential; + if (error.code != FIRAuthErrorCodeMissingAppToken && !isInvalidAppCredential) { + completion(nil, error); + return; + } + [self reCAPTCHAURLWithCompletion:^(NSURL *_Nullable reCAPTCHAURL, + NSError *_Nullable error) { + if (error) { + callBackOnMainThread(nil, error); + return; + } + [_auth.authURLPresenter presentURL:reCAPTCHAURL + UIDelegate:UIDelegate + callbackMatcher:^BOOL(NSURL * _Nullable callbackURL) { + return [self isVerifyAppURL:callbackURL]; + } + completion:^(NSURL *_Nullable callbackURL, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + NSDictionary<NSString *, NSString *> *URLQueryItems = + [NSDictionary gtm_dictionaryWithHttpArgumentsString:callbackURL.query];; + URLQueryItems = + [NSDictionary gtm_dictionaryWithHttpArgumentsString:URLQueryItems[@"deep_link_id"]]; + NSString *reCAPTCHA = URLQueryItems[@"recaptchaToken"]; + FIRSendVerificationCodeRequest *request = + [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber + appCredential:nil + reCAPTCHAToken:reCAPTCHA + requestConfiguration:_auth.requestConfiguration]; + [FIRAuthBackend sendVerificationCode:request + callback:^(FIRSendVerificationCodeResponse + *_Nullable response, NSError *_Nullable error) { + if (error) { + callBackOnMainThread(nil, error); + return; + } + // Associate the phone number with the verification ID. + response.verificationID.fir_authPhoneNumber = phoneNumber; + callBackOnMainThread(response.verificationID, nil); + }]; + }]; + }]; }]; }); } @@ -111,6 +201,71 @@ typedef void (^FIRVerifyClientCallback)(FIRAuthAppCredential *_Nullable appCrede } #pragma mark - Internal Methods +/** @fn isVerifyAppURL: + @brief Parses a URL into all available query items. + @param URL The url to be checked against the authType string. + @return Whether or not the URL matches authType. + */ +- (BOOL)isVerifyAppURL:(nullable NSURL *)URL { + if (!URL) { + return NO; + } + NSURLComponents *actualURLComponents = + [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO]; + actualURLComponents.query = nil; + actualURLComponents.fragment = nil; + + NSURLComponents *expectedURLComponents = [NSURLComponents new]; + NSArray *strings = [_auth.app.options.clientID componentsSeparatedByString:@"."]; + expectedURLComponents.scheme = + [[strings reverseObjectEnumerator].allObjects componentsJoinedByString:@"."]; + expectedURLComponents.host = @"firebaseauth"; + expectedURLComponents.path = @"/link"; + + if (!([[expectedURLComponents URL] isEqual:[actualURLComponents URL]])) { + return NO; + } + NSDictionary<NSString *, NSString *> *URLQueryItems = + [NSDictionary gtm_dictionaryWithHttpArgumentsString:URL.query]; + NSURL *deeplinkURL = [NSURL URLWithString:URLQueryItems[@"deep_link_id"]]; + NSDictionary<NSString *, NSString *> *deeplinkQueryItems = + [NSDictionary gtm_dictionaryWithHttpArgumentsString:deeplinkURL.query]; + if ([deeplinkQueryItems[@"authType"] isEqualToString:kAuthTypeVerifyApp]) { + return YES; + } + return NO; +} + +/** @fn internalVerifyPhoneNumber:completion: + @brief Starts the phone number authentication flow by sending a verifcation code to the + specified phone number. + @param phoneNumber The phone number to be verified. + @param completion The callback to be invoked when the verification flow is finished. + */ + +- (void)internalVerifyPhoneNumber:(NSString *)phoneNumber + completion:(nullable FIRVerificationResultCallback)completion { + if (!phoneNumber.length) { + completion(nil, [FIRAuthErrorUtils missingPhoneNumberErrorWithMessage:nil]); + return; + } + [_auth.notificationManager checkNotificationForwardingWithCallback: + ^(BOOL isNotificationBeingForwarded) { + if (!isNotificationBeingForwarded) { + completion(nil, [FIRAuthErrorUtils notificationNotForwardedError]); + return; + } + FIRVerificationResultCallback callback = ^(NSString *_Nullable verificationID, + NSError *_Nullable error) { + if (completion) { + completion(verificationID, error); + } + }; + [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber + retryOnInvalidAppCredential:YES + callback:callback]; + }]; +} /** @fn verifyClientAndSendVerificationCodeToPhoneNumber:retryOnInvalidAppCredential:callback: @brief Starts the flow to verify the client via silent push notification. @@ -201,6 +356,62 @@ typedef void (^FIRVerifyClientCallback)(FIRAuthAppCredential *_Nullable appCrede }]; } +- (void)reCAPTCHAURLWithCompletion:(FIRReCAPTCHAURLCallBack)completion { + [self fetchAuthDomainWithCompletion:^(NSString *_Nullable authDomain, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + NSString *bundleID = [NSBundle mainBundle].bundleIdentifier; + NSString *clienID = _auth.app.options.clientID; + NSString *apiKey = _auth.app.options.APIKey; + NSMutableDictionary *urlArguments = [[NSMutableDictionary alloc] initWithDictionary: @{ + @"apiKey" : apiKey, + @"authType" : kAuthTypeVerifyApp, + @"ibi" : bundleID, + @"clientId" : clienID, + @"v" : [FIRAuthBackend authUserAgent] + }]; + if (_auth.requestConfiguration.languageCode) { + urlArguments[@"hl"] = _auth.requestConfiguration.languageCode; + } + NSString *argumentsString = [urlArguments gtm_httpArgumentsString]; + NSString *URLString = + [NSString stringWithFormat:kReCAPTCHAURLStringFormat, authDomain, argumentsString]; + completion([NSURL URLWithString:URLString], nil); + }]; +} + +- (void)fetchAuthDomainWithCompletion:(FIRFetchAuthDomainCallback)completion { + FIRGetProjectConfigRequest *request = + [[FIRGetProjectConfigRequest alloc] initWithRequestConfiguration:_auth.requestConfiguration]; + + [FIRAuthBackend getProjectConfig:request + callback:^(FIRGetProjectConfigResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + NSString *authDomain; + for (NSString *domain in response.authorizedDomains) { + NSInteger index = domain.length - kAuthDomainSuffix.length; + if (index >= 2) { + if ([domain hasSuffix:kAuthDomainSuffix] && domain.length >= kAuthDomainSuffix.length + 2) { + authDomain = domain; + break; + } + } + } + if (!authDomain.length) { + completion(nil, [FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:response]); + return; + } + completion(authDomain, nil); + }]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuth.m b/Firebase/Auth/Source/FIRAuth.m index d2915a3..dd0b0bf 100644 --- a/Firebase/Auth/Source/FIRAuth.m +++ b/Firebase/Auth/Source/FIRAuth.m @@ -64,6 +64,7 @@ #import "FIRAuthAppDelegateProxy.h" #import "AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h" #import "FIRAuthNotificationManager.h" +#import "FIRAuthURLPresenter.h" #endif #pragma mark - Constants @@ -387,6 +388,9 @@ static NSMutableDictionary *gKeychainServiceNameForAppName; }); return uid; }; + #if TARGET_OS_IOS + _authURLPresenter = [[FIRAuthURLPresenter alloc] init]; + #endif } return self; } @@ -984,11 +988,15 @@ static NSMutableDictionary *gKeychainServiceNameForAppName; }); return result; } -#endif -- (BOOL)canHandleURL:(NSURL *)url { - return NO; +- (BOOL)canHandleURL:(NSURL *)URL { + __block BOOL result = NO; + dispatch_sync(FIRAuthGlobalWorkQueue(), ^{ + result = [_authURLPresenter canHandleURL:URL]; + }); + return result; } +#endif #pragma mark - Internal Methods diff --git a/Firebase/Auth/Source/FIRAuthURLPresenter.m b/Firebase/Auth/Source/FIRAuthURLPresenter.m index 90ceed6..fc84383 100644 --- a/Firebase/Auth/Source/FIRAuthURLPresenter.m +++ b/Firebase/Auth/Source/FIRAuthURLPresenter.m @@ -68,7 +68,7 @@ NS_ASSUME_NONNULL_BEGIN } - (BOOL)canHandleURL:(NSURL *)URL { - if (_callbackMatcher(URL)) { + if (_callbackMatcher && _callbackMatcher(URL)) { _callbackMatcher = nil; [self finishPresentationWithURL:URL error:nil]; return YES; diff --git a/Firebase/Auth/Source/FIRAuth_Internal.h b/Firebase/Auth/Source/FIRAuth_Internal.h index d568d45..a89ebf9 100644 --- a/Firebase/Auth/Source/FIRAuth_Internal.h +++ b/Firebase/Auth/Source/FIRAuth_Internal.h @@ -24,12 +24,20 @@ @class FIRAuthAPNSTokenManager; @class FIRAuthAppCredentialManager; @class FIRAuthNotificationManager; +@class FIRAuthURLPresenter; #endif NS_ASSUME_NONNULL_BEGIN @interface FIRAuth () +#if TARGET_OS_IOS +/** @property authURLPresenter + @brief An object that takes care of presenting URLs via the auth instance. + */ +@property(nonatomic, strong, readonly) FIRAuthURLPresenter *authURLPresenter; +#endif + /** @property requestConfiguration @brief The configuration object comprising of paramters needed to make a request to Firebase Auth's backend. diff --git a/Firebase/Auth/Source/Public/FIRAuth.h b/Firebase/Auth/Source/Public/FIRAuth.h index ad6b635..eeb7ff9 100644 --- a/Firebase/Auth/Source/Public/FIRAuth.h +++ b/Firebase/Auth/Source/Public/FIRAuth.h @@ -280,6 +280,18 @@ FIR_SWIFT_NAME(Auth) */ - (instancetype)init NS_UNAVAILABLE; +/** @fn canHandleURL: + @brief Whether the specific URL is handled by @c FIRAuth . + @param URL The URL received by the application delegate from any of the openURL method. + @return Whether or the URL is handled. YES means the URL is for Firebase Auth + so the caller should ignore the URL from further processing, and NO means the + the URL is for the app (or another libaray) so the caller should continue handling + this URL as usual. + @remarks If swizzling is disabled, URLs received by the application delegate must be forwarded + to this method for phone number auth to work. + */ +- (BOOL)canHandleURL:(nonnull NSURL *)URL; + /** @fn fetchProvidersForEmail:completion: @brief Fetches the list of IdPs that can be used for signing in with the provided email address. Useful for an "identifier-first" sign-in flow. diff --git a/Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h b/Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h index 138028a..5acc500 100644 --- a/Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h +++ b/Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h @@ -20,6 +20,7 @@ @class FIRAuth; @class FIRPhoneAuthCredential; +@protocol FIRAuthUIDelegate; NS_ASSUME_NONNULL_BEGIN @@ -85,6 +86,29 @@ FIR_SWIFT_NAME(PhoneAuthProvider) - (void)verifyPhoneNumber:(NSString *)phoneNumber completion:(nullable FIRVerificationResultCallback)completion; +/** @fn verifyPhoneNumber:UIDelegate:completion: + @brief Starts the phone number authentication flow by sending a verifcation code to the + specified phone number. + @param phoneNumber The phone number to be verified. + @param UIDelegate A view controller object used to present the SFSafariViewController or + WKWebview. + @param completion The callback to be invoked when the verification flow is finished. + @remarks Possible error codes: + <ul> + <li>@c FIRAuthErrorCodeCaptchaCheckFailed - Indicates that the reCAPTCHA token obtained by + the Firebase Auth is invalid or has expired.</li> + <li>@c FIRAuthErrorCodeQuotaExceeded - Indicates that the phone verification quota for this + project has been exceeded.</li> + <li>@c FIRAuthErrorCodeInvalidPhoneNumber - Indicates that the phone number provided is + invalid.</li> + <li>@c FIRAuthErrorCodeMissingPhoneNumber - Indicates that a phone number was not provided. + </li> + </ul> + */ +- (void)verifyPhoneNumber:(NSString *)phoneNumber + UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate + completion:(nullable FIRVerificationResultCallback)completion; + /** @fn credentialWithVerificationID:verificationCode: @brief Creates an @c FIRAuthCredential for the phone number provider identified by the verification ID and verification code. diff --git a/Firebase/Auth/Source/RPCs/FIRAuthBackend.h b/Firebase/Auth/Source/RPCs/FIRAuthBackend.h index 65f93ce..a82c3a7 100644 --- a/Firebase/Auth/Source/RPCs/FIRAuthBackend.h +++ b/Firebase/Auth/Source/RPCs/FIRAuthBackend.h @@ -210,6 +210,12 @@ typedef void (^FIRVerifyClientResponseCallback) */ @interface FIRAuthBackend : NSObject +/** @fn authUserAgent + @brief Retrieves the Firebase Auth user agent. + @return The Firebase Auth user agent. + */ ++ (NSString *)authUserAgent; + /** @fn setBackendImplementation: @brief Changes the default backend implementation to something else. @param backendImplementation The backend implementation to use. diff --git a/Firebase/Auth/Source/RPCs/FIRAuthBackend.m b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m index afe05bd..2a70a80 100644 --- a/Firebase/Auth/Source/RPCs/FIRAuthBackend.m +++ b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m @@ -465,6 +465,11 @@ static id<FIRAuthBackendImplementation> gBackendImplementation; [[self implementation] resetPassword:request callback:callback]; } ++ (NSString *)authUserAgent { + return [NSString stringWithFormat:@"FirebaseAuth.iOS/%s %@", + FirebaseAuthVersionString, GTMFetcherStandardUserAgentString(nil)]; +} + @end @interface FIRAuthBackendRPCIssuerImplementation : NSObject <FIRAuthBackendRPCIssuer> @@ -480,8 +485,7 @@ static id<FIRAuthBackendImplementation> gBackendImplementation; self = [super init]; if (self) { _fetcherService = [[GTMSessionFetcherService alloc] init]; - _fetcherService.userAgent = [NSString stringWithFormat:@"FirebaseAuth.iOS/%s %@", - FirebaseAuthVersionString, GTMFetcherStandardUserAgentString(nil)]; + _fetcherService.userAgent = [FIRAuthBackend authUserAgent]; _fetcherService.callbackQueue = FIRAuthGlobalWorkQueue(); } return self; |