123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- /*
- * 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 "FIRPhoneAuthProvider.h"
-
- #import <FirebaseCore/FIRLogger.h>
- #import "FIRPhoneAuthCredential_Internal.h"
- #import <FirebaseCore/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 "FIRAuthSettings.h"
- #import "FIRAuthWebUtils.h"
- #import "FirebaseAuthVersion.h"
- #import <FirebaseCore/FIROptions.h>
- #import "FIRGetProjectConfigRequest.h"
- #import "FIRGetProjectConfigResponse.h"
- #import "FIRSendVerificationCodeRequest.h"
- #import "FIRSendVerificationCodeResponse.h"
- #import "FIRVerifyClientRequest.h"
- #import "FIRVerifyClientResponse.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 occurred 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
- authentication flow.
- @param error The error that occurred while verifying the app, if any.
- */
- 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 occurred 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
- @brief The auth instance used for verifying the phone number.
- */
- FIRAuth *_auth;
-
- /** @var _callbackScheme
- @brief The callback URL scheme used for reCAPTCHA fallback.
- */
- NSString *_callbackScheme;
- }
-
- /** @fn initWithAuth:
- @brief returns an instance of @c FIRPhoneAuthProvider associated with the provided auth
- instance.
- @return An Instance of @c FIRPhoneAuthProvider.
- */
- - (nullable instancetype)initWithAuth:(FIRAuth *)auth {
- self = [super init];
- if (self) {
- _auth = auth;
- _callbackScheme = [[[_auth.app.options.clientID componentsSeparatedByString:@"."]
- reverseObjectEnumerator].allObjects componentsJoinedByString:@"."];
- }
- return self;
- }
-
- - (void)verifyPhoneNumber:(NSString *)phoneNumber
- UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
- completion:(nullable FIRVerificationResultCallback)completion {
- if (![FIRAuthWebUtils isCallbackSchemeRegisteredForCustomURLScheme:_callbackScheme]) {
- [NSException raise:NSInternalInconsistencyException
- format:@"Please register custom URL scheme '%@' in the app's Info.plist file.",
- _callbackScheme];
- }
- dispatch_async(FIRAuthGlobalWorkQueue(), ^{
- FIRVerificationResultCallback callBackOnMainThread = ^(NSString *_Nullable verificationID,
- NSError *_Nullable error) {
- if (completion) {
- dispatch_async(dispatch_get_main_queue(), ^{
- completion(verificationID, error);
- });
- }
- };
- [self internalVerifyPhoneNumber:phoneNumber completion:^(NSString *_Nullable verificationID,
- NSError *_Nullable error) {
- if (!error) {
- callBackOnMainThread(verificationID, nil);
- return;
- }
- NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
- BOOL isInvalidAppCredential = error.code == FIRAuthErrorCodeInternalError &&
- underlyingError.code == FIRAuthErrorCodeInvalidAppCredential;
- if (error.code != FIRAuthErrorCodeMissingAppToken && !isInvalidAppCredential) {
- callBackOnMainThread(nil, error);
- return;
- }
- NSString *eventID = [FIRAuthWebUtils randomStringWithLength:10];
- [self reCAPTCHAURLWithEventID:eventID completion:^(NSURL *_Nullable reCAPTCHAURL,
- NSError *_Nullable error) {
- if (error) {
- callBackOnMainThread(nil, error);
- return;
- }
- FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nullable callbackURL) {
- return [FIRAuthWebUtils isExpectedCallbackURL:callbackURL
- eventID:eventID
- authType:kAuthTypeVerifyApp
- callbackScheme:self->_callbackScheme];
- };
- [self->_auth.authURLPresenter presentURL:reCAPTCHAURL
- UIDelegate:UIDelegate
- callbackMatcher:callbackMatcher
- completion:^(NSURL *_Nullable callbackURL,
- NSError *_Nullable error) {
- if (error) {
- callBackOnMainThread(nil, error);
- return;
- }
- NSError *reCAPTCHAError;
- NSString *reCAPTCHAToken = [self reCAPTCHATokenForURL:callbackURL error:&reCAPTCHAError];
- if (!reCAPTCHAToken) {
- callBackOnMainThread(nil, reCAPTCHAError);
- return;
- }
- FIRSendVerificationCodeRequest *request =
- [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
- appCredential:nil
- reCAPTCHAToken:reCAPTCHAToken
- requestConfiguration:
- self->_auth.requestConfiguration];
- [FIRAuthBackend sendVerificationCode:request
- callback:^(FIRSendVerificationCodeResponse
- *_Nullable response, NSError *_Nullable error) {
- if (error) {
- callBackOnMainThread(nil, error);
- return;
- }
- callBackOnMainThread(response.verificationID, nil);
- }];
- }];
- }];
- }];
- });
- }
-
- - (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID
- verificationCode:(NSString *)verificationCode {
- return [[FIRPhoneAuthCredential alloc] initWithProviderID:FIRPhoneAuthProviderID
- verificationID:verificationID
- verificationCode:verificationCode];
- }
-
- + (instancetype)provider {
- return [[self alloc]initWithAuth:[FIRAuth auth]];
- }
-
- + (instancetype)providerWithAuth:(FIRAuth *)auth {
- return [[self alloc]initWithAuth:auth];
- }
-
- #pragma mark - Internal Methods
-
- /** @fn reCAPTCHATokenForURL:error:
- @brief Parses the reCAPTCHA URL and returns the reCAPTCHA token.
- @param URL The url to be parsed for a reCAPTCHA token.
- @param error The error that occurred if any.
- @return The reCAPTCHA token if successful.
- */
- - (NSString *)reCAPTCHATokenForURL:(NSURL *)URL error:(NSError **)error {
- NSURLComponents *actualURLComponents = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
- NSArray<NSURLQueryItem *> *queryItems = [actualURLComponents queryItems];
- NSString *deepLinkURL = [FIRAuthWebUtils queryItemValue:@"deep_link_id" from:queryItems];
- NSData *errorData;
- if (deepLinkURL) {
- actualURLComponents = [NSURLComponents componentsWithString:deepLinkURL];
- queryItems = [actualURLComponents queryItems];
- NSString *recaptchaToken = [FIRAuthWebUtils queryItemValue:@"recaptchaToken" from:queryItems];
- if (recaptchaToken) {
- return recaptchaToken;
- }
- NSString *firebaseError = [FIRAuthWebUtils queryItemValue:@"firebaseError" from:queryItems];
- errorData = [firebaseError dataUsingEncoding:NSUTF8StringEncoding];
- } else {
- errorData = nil;
- }
- NSError *jsonError;
- NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData:errorData
- options:0
- error:&jsonError];
- if (jsonError) {
- *error = [FIRAuthErrorUtils JSONSerializationErrorWithUnderlyingError:jsonError];
- return nil;
- }
- *error = [FIRAuthErrorUtils URLResponseErrorWithCode:errorDict[@"code"]
- message:errorDict[@"message"]];
- if (!*error) {
- NSString *reason;
- if(errorDict[@"code"] && errorDict[@"message"]) {
- reason = [NSString stringWithFormat:@"[%@] - %@",errorDict[@"code"], errorDict[@"message"]];
- } else {
- reason = [NSString stringWithFormat:@"An unknown error occurred with the following "
- "response: %@", deepLinkURL];
- }
- *error = [FIRAuthErrorUtils appVerificationUserInteractionFailureWithReason:reason];
- }
- return nil;
- }
-
- /** @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.
- @param retryOnInvalidAppCredential Whether of not the flow should be retried if an
- FIRAuthErrorCodeInvalidAppCredential error is returned from the backend.
- @param phoneNumber The phone number to be verified.
- @param callback The callback to be invoked on the global work queue when the flow is
- finished.
- */
- - (void)verifyClientAndSendVerificationCodeToPhoneNumber:(NSString *)phoneNumber
- retryOnInvalidAppCredential:(BOOL)retryOnInvalidAppCredential
- callback:(FIRVerificationResultCallback)callback {
- if (_auth.settings.isAppVerificationDisabledForTesting) {
- FIRSendVerificationCodeRequest *request =
- [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
- appCredential:nil
- reCAPTCHAToken:nil
- requestConfiguration:
- _auth.requestConfiguration];
- [FIRAuthBackend sendVerificationCode:request
- callback:^(FIRSendVerificationCodeResponse *_Nullable response,
- NSError *_Nullable error) {
- callback(response.verificationID, error);
- }];
- return;
- }
- [self verifyClientWithCompletion:^(FIRAuthAppCredential *_Nullable appCredential,
- NSError *_Nullable error) {
- if (error) {
- callback(nil, error);
- return;
- }
- FIRSendVerificationCodeRequest *request =
- [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
- appCredential:appCredential
- reCAPTCHAToken:nil
- requestConfiguration:
- self->_auth.requestConfiguration];
- [FIRAuthBackend sendVerificationCode:request
- callback:^(FIRSendVerificationCodeResponse *_Nullable response,
- NSError *_Nullable error) {
- if (error) {
- if (error.code == FIRAuthErrorCodeInvalidAppCredential) {
- if (retryOnInvalidAppCredential) {
- [self->_auth.appCredentialManager clearCredential];
- [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
- retryOnInvalidAppCredential:NO
- callback:callback];
- return;
- }
- callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:nil
- underlyingError:error]);
- return;
- }
- callback(nil, error);
- return;
- }
- callback(response.verificationID, nil);
- }];
- }];
- }
-
- /** @fn verifyClientWithCompletion:completion:
- @brief Continues the flow to verify the client via silent push notification.
- @param completion The callback to be invoked when the client verification flow is finished.
- */
- - (void)verifyClientWithCompletion:(FIRVerifyClientCallback)completion {
- if (_auth.appCredentialManager.credential) {
- completion(_auth.appCredentialManager.credential, nil);
- return;
- }
- [_auth.tokenManager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token,
- NSError *_Nullable error) {
- if (!token) {
- completion(nil, [FIRAuthErrorUtils missingAppTokenErrorWithUnderlyingError:error]);
- return;
- }
- FIRVerifyClientRequest *request =
- [[FIRVerifyClientRequest alloc] initWithAppToken:token.string
- isSandbox:token.type == FIRAuthAPNSTokenTypeSandbox
- requestConfiguration:self->_auth.requestConfiguration];
- [FIRAuthBackend verifyClient:request callback:^(FIRVerifyClientResponse *_Nullable response,
- NSError *_Nullable error) {
- if (error) {
- completion(nil, error);
- return;
- }
- NSTimeInterval timeout = [response.suggestedTimeOutDate timeIntervalSinceNow];
- [self->_auth.appCredentialManager
- didStartVerificationWithReceipt:response.receipt
- timeout:timeout
- callback:^(FIRAuthAppCredential *credential) {
- if (!credential.secret) {
- FIRLogWarning(kFIRLoggerAuth, @"I-AUT000014",
- @"Failed to receive remote notification to verify app identity within "
- @"%.0f second(s)", timeout);
- }
- completion(credential, nil);
- }];
- }];
- }];
- }
-
- /** @fn reCAPTCHAURLWithEventID:completion:
- @brief Constructs a URL used for opening a reCAPTCHA app verification flow using a given event
- ID.
- @param eventID The event ID used for this purpose.
- @param completion The callback invoked after the URL has been constructed or an error
- has been encountered.
- */
- - (void)reCAPTCHAURLWithEventID:(NSString *)eventID completion:(FIRReCAPTCHAURLCallBack)completion {
- [FIRAuthWebUtils fetchAuthDomainWithRequestConfiguration:_auth.requestConfiguration
- completion:^(NSString *_Nullable authDomain,
- NSError *_Nullable error) {
- if (error) {
- if (completion) {
- completion(nil, error);
- return;
- }
- }
- NSString *bundleID = [NSBundle mainBundle].bundleIdentifier;
- NSString *clientID = self->_auth.app.options.clientID;
- NSString *apiKey = self->_auth.requestConfiguration.APIKey;
- NSMutableArray<NSURLQueryItem *> *queryItems = [@[
- [NSURLQueryItem queryItemWithName:@"apiKey" value:apiKey],
- [NSURLQueryItem queryItemWithName:@"authType" value:kAuthTypeVerifyApp],
- [NSURLQueryItem queryItemWithName:@"ibi" value:bundleID ?: @""],
- [NSURLQueryItem queryItemWithName:@"clientId" value:clientID],
- [NSURLQueryItem queryItemWithName:@"v" value:[FIRAuthBackend authUserAgent]],
- [NSURLQueryItem queryItemWithName:@"eventId" value:eventID]
- ] mutableCopy
- ];
-
- if (self->_auth.requestConfiguration.languageCode) {
- [queryItems addObject:[NSURLQueryItem queryItemWithName:@"hl"value:
- self->_auth.requestConfiguration.languageCode]];
- }
- NSURLComponents *components = [[NSURLComponents alloc] initWithString:
- [NSString stringWithFormat:kReCAPTCHAURLStringFormat, authDomain]];
- [components setQueryItems:queryItems];
- completion([components URL], nil);
- }];
- }
-
- @end
-
- NS_ASSUME_NONNULL_END
|