You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

519 lines
22 KiB

/*
* 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 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
authentication flow.
@param error The error that occured 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 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
@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 assocaited 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 (![self isCallbackSchemeRegistered]) {
[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;
}
NSMutableString *eventID = [[NSMutableString alloc] init];
for (int i=0; i<10; i++) {
[eventID appendString:
[NSString stringWithFormat:@"%c", 'a' + arc4random_uniform('z' - 'a' + 1)]];
}
[self reCAPTCHAURLWithEventID:eventID completion:^(NSURL *_Nullable reCAPTCHAURL,
NSError *_Nullable error) {
if (error) {
callBackOnMainThread(nil, error);
return;
}
FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nullable callbackURL) {
return [self isVerifyAppURL:callbackURL eventID:eventID];
};
[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 isCallbackSchemeRegistered
@brief Checks whether or not the expected callback scheme has been registered by the app.
@remarks This method is thread-safe.
*/
- (BOOL)isCallbackSchemeRegistered {
NSString *expectedCustomScheme = [_callbackScheme lowercaseString];
NSArray *urlTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"];
for (NSDictionary *urlType in urlTypes) {
NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"];
for (NSString *urlTypeScheme in urlTypeSchemes) {
if ([urlTypeScheme.lowercaseString isEqualToString:expectedCustomScheme]) {
return YES;
}
}
}
return NO;
}
/** @fn reCAPTCHATokenForURL:error:
@brief Parses the reCAPTCHA URL and returns.
@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 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 eventID:(NSString *)eventID {
if (!URL) {
return NO;
}
NSURLComponents *actualURLComponents =
[NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
actualURLComponents.query = nil;
actualURLComponents.fragment = nil;
NSURLComponents *expectedURLComponents = [NSURLComponents new];
expectedURLComponents.scheme = _callbackScheme;
expectedURLComponents.host = @"firebaseauth";
expectedURLComponents.path = @"/link";
if (!([[expectedURLComponents URL] isEqual:[actualURLComponents URL]])) {
return NO;
}
actualURLComponents = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
NSArray<NSURLQueryItem *> *queryItems = [actualURLComponents queryItems];
NSString *deepLinkURL = [FIRAuthWebUtils queryItemValue:@"deep_link_id" from:queryItems];
if (deepLinkURL == nil) {
return NO;
}
NSURLComponents *deepLinkURLComponents = [NSURLComponents componentsWithString:deepLinkURL];
NSArray<NSURLQueryItem *> *deepLinkQueryItems = [deepLinkURLComponents queryItems];
NSString *deepLinkAuthType = [FIRAuthWebUtils queryItemValue:@"authType" from:deepLinkQueryItems];
NSString *deepLinkEventID = [FIRAuthWebUtils queryItemValue:@"eventId" from:deepLinkQueryItems];
if ([deepLinkAuthType isEqualToString:kAuthTypeVerifyApp] &&
[deepLinkEventID isEqualToString:eventID]) {
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.
@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 {
[self fetchAuthDomainWithCompletion:^(NSString *_Nullable authDomain,
NSError *_Nullable error) {
if (error) {
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);
}];
}
/** @fn fetchAuthDomainWithCompletion:completion:
@brief Fetches the auth domain associated with the Firebase Project.
@param completion The callback invoked after the auth domain has been constructed or an error
has been encountered.
*/
- (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