aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Zsika Phillip <protocol86@users.noreply.github.com>2017-09-04 17:37:46 -0700
committerGravatar GitHub <noreply@github.com>2017-09-04 17:37:46 -0700
commit85e81371801f9cd3e94a1505ce3b2f4f66086b08 (patch)
tree220fdf17ff9aed20e865bf019dd7dad805458fab
parent540e21c0cc3206a8e911554227cc1f081ef40dda (diff)
Adds app verification alternative (#228)
* Adds app verification alternative
-rw-r--r--AuthSamples/Sample/MainViewController.m119
-rw-r--r--Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m3
-rw-r--r--Example/Auth/Tests/FIRPhoneAuthProviderTests.m110
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m237
-rw-r--r--Firebase/Auth/Source/FIRAuth.m14
-rw-r--r--Firebase/Auth/Source/FIRAuthURLPresenter.m2
-rw-r--r--Firebase/Auth/Source/FIRAuth_Internal.h8
-rw-r--r--Firebase/Auth/Source/Public/FIRAuth.h12
-rw-r--r--Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h24
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthBackend.h6
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthBackend.m8
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;