diff options
author | 2017-09-04 17:37:46 -0700 | |
---|---|---|
committer | 2017-09-04 17:37:46 -0700 | |
commit | 85e81371801f9cd3e94a1505ce3b2f4f66086b08 (patch) | |
tree | 220fdf17ff9aed20e865bf019dd7dad805458fab /Firebase | |
parent | 540e21c0cc3206a8e911554227cc1f081ef40dda (diff) |
Adds app verification alternative (#228)
* Adds app verification alternative
Diffstat (limited to 'Firebase')
-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 |
8 files changed, 292 insertions, 19 deletions
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; |