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

6 years ago
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FIRPhoneAuthProvider.h"
  17. #import <FirebaseCore/FIRLogger.h>
  18. #import "FIRPhoneAuthCredential_Internal.h"
  19. #import <FirebaseCore/FIRApp.h>
  20. #import "FIRAuthAPNSToken.h"
  21. #import "FIRAuthAPNSTokenManager.h"
  22. #import "FIRAuthAppCredential.h"
  23. #import "FIRAuthAppCredentialManager.h"
  24. #import "FIRAuthGlobalWorkQueue.h"
  25. #import "FIRAuth_Internal.h"
  26. #import "FIRAuthURLPresenter.h"
  27. #import "FIRAuthNotificationManager.h"
  28. #import "FIRAuthErrorUtils.h"
  29. #import "FIRAuthBackend.h"
  30. #import "FIRAuthSettings.h"
  31. #import "FIRAuthWebUtils.h"
  32. #import "FirebaseAuthVersion.h"
  33. #import <FirebaseCore/FIROptions.h>
  34. #import "FIRGetProjectConfigRequest.h"
  35. #import "FIRGetProjectConfigResponse.h"
  36. #import "FIRSendVerificationCodeRequest.h"
  37. #import "FIRSendVerificationCodeResponse.h"
  38. #import "FIRVerifyClientRequest.h"
  39. #import "FIRVerifyClientResponse.h"
  40. NS_ASSUME_NONNULL_BEGIN
  41. /** @typedef FIRReCAPTCHAURLCallBack
  42. @brief The callback invoked at the end of the flow to fetch a reCAPTCHA URL.
  43. @param reCAPTCHAURL The reCAPTCHA URL.
  44. @param error The error that occured while fetching the reCAPTCHAURL, if any.
  45. */
  46. typedef void (^FIRReCAPTCHAURLCallBack)(NSURL *_Nullable reCAPTCHAURL, NSError *_Nullable error);
  47. /** @typedef FIRVerifyClientCallback
  48. @brief The callback invoked at the end of a client verification flow.
  49. @param appCredential credential that proves the identity of the app during a phone
  50. authentication flow.
  51. @param error The error that occured while verifying the app, if any.
  52. */
  53. typedef void (^FIRVerifyClientCallback)(FIRAuthAppCredential *_Nullable appCredential,
  54. NSError *_Nullable error);
  55. /** @typedef FIRFetchAuthDomainCallback
  56. @brief The callback invoked at the end of the flow to fetch the Auth domain.
  57. @param authDomain The Auth domain.
  58. @param error The error that occured while fetching the auth domain, if any.
  59. */
  60. typedef void (^FIRFetchAuthDomainCallback)(NSString *_Nullable authDomain,
  61. NSError *_Nullable error);
  62. /** @var kAuthDomainSuffix
  63. @brief The suffix of the auth domain pertiaining to a given Firebase project.
  64. */
  65. static NSString *const kAuthDomainSuffix = @"firebaseapp.com";
  66. /** @var kauthTypeVerifyApp
  67. @brief The auth type to be specified in the app verification request.
  68. */
  69. static NSString *const kAuthTypeVerifyApp = @"verifyApp";
  70. /** @var kReCAPTCHAURLStringFormat
  71. @brief The format of the URL used to open the reCAPTCHA page during app verification.
  72. */
  73. NSString *const kReCAPTCHAURLStringFormat = @"https://%@/__/auth/handler?";
  74. @implementation FIRPhoneAuthProvider {
  75. /** @var _auth
  76. @brief The auth instance used for verifying the phone number.
  77. */
  78. FIRAuth *_auth;
  79. /** @var _callbackScheme
  80. @brief The callback URL scheme used for reCAPTCHA fallback.
  81. */
  82. NSString *_callbackScheme;
  83. }
  84. /** @fn initWithAuth:
  85. @brief returns an instance of @c FIRPhoneAuthProvider assocaited with the provided auth
  86. instance.
  87. @return An Instance of @c FIRPhoneAuthProvider.
  88. */
  89. - (nullable instancetype)initWithAuth:(FIRAuth *)auth {
  90. self = [super init];
  91. if (self) {
  92. _auth = auth;
  93. _callbackScheme = [[[_auth.app.options.clientID componentsSeparatedByString:@"."]
  94. reverseObjectEnumerator].allObjects componentsJoinedByString:@"."];
  95. }
  96. return self;
  97. }
  98. - (void)verifyPhoneNumber:(NSString *)phoneNumber
  99. UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
  100. completion:(nullable FIRVerificationResultCallback)completion {
  101. if (![self isCallbackSchemeRegistered]) {
  102. [NSException raise:NSInternalInconsistencyException
  103. format:@"Please register custom URL scheme '%@' in the app's Info.plist file.",
  104. _callbackScheme];
  105. }
  106. dispatch_async(FIRAuthGlobalWorkQueue(), ^{
  107. FIRVerificationResultCallback callBackOnMainThread = ^(NSString *_Nullable verificationID,
  108. NSError *_Nullable error) {
  109. if (completion) {
  110. dispatch_async(dispatch_get_main_queue(), ^{
  111. completion(verificationID, error);
  112. });
  113. }
  114. };
  115. [self internalVerifyPhoneNumber:phoneNumber completion:^(NSString *_Nullable verificationID,
  116. NSError *_Nullable error) {
  117. if (!error) {
  118. callBackOnMainThread(verificationID, nil);
  119. return;
  120. }
  121. NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
  122. BOOL isInvalidAppCredential = error.code == FIRAuthErrorCodeInternalError &&
  123. underlyingError.code == FIRAuthErrorCodeInvalidAppCredential;
  124. if (error.code != FIRAuthErrorCodeMissingAppToken && !isInvalidAppCredential) {
  125. callBackOnMainThread(nil, error);
  126. return;
  127. }
  128. NSMutableString *eventID = [[NSMutableString alloc] init];
  129. for (int i=0; i<10; i++) {
  130. [eventID appendString:
  131. [NSString stringWithFormat:@"%c", 'a' + arc4random_uniform('z' - 'a' + 1)]];
  132. }
  133. [self reCAPTCHAURLWithEventID:eventID completion:^(NSURL *_Nullable reCAPTCHAURL,
  134. NSError *_Nullable error) {
  135. if (error) {
  136. callBackOnMainThread(nil, error);
  137. return;
  138. }
  139. FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nullable callbackURL) {
  140. return [self isVerifyAppURL:callbackURL eventID:eventID];
  141. };
  142. [self->_auth.authURLPresenter presentURL:reCAPTCHAURL
  143. UIDelegate:UIDelegate
  144. callbackMatcher:callbackMatcher
  145. completion:^(NSURL *_Nullable callbackURL,
  146. NSError *_Nullable error) {
  147. if (error) {
  148. callBackOnMainThread(nil, error);
  149. return;
  150. }
  151. NSError *reCAPTCHAError;
  152. NSString *reCAPTCHAToken = [self reCAPTCHATokenForURL:callbackURL error:&reCAPTCHAError];
  153. if (!reCAPTCHAToken) {
  154. callBackOnMainThread(nil, reCAPTCHAError);
  155. return;
  156. }
  157. FIRSendVerificationCodeRequest *request =
  158. [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
  159. appCredential:nil
  160. reCAPTCHAToken:reCAPTCHAToken
  161. requestConfiguration:
  162. self->_auth.requestConfiguration];
  163. [FIRAuthBackend sendVerificationCode:request
  164. callback:^(FIRSendVerificationCodeResponse
  165. *_Nullable response, NSError *_Nullable error) {
  166. if (error) {
  167. callBackOnMainThread(nil, error);
  168. return;
  169. }
  170. callBackOnMainThread(response.verificationID, nil);
  171. }];
  172. }];
  173. }];
  174. }];
  175. });
  176. }
  177. - (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID
  178. verificationCode:(NSString *)verificationCode {
  179. return [[FIRPhoneAuthCredential alloc] initWithProviderID:FIRPhoneAuthProviderID
  180. verificationID:verificationID
  181. verificationCode:verificationCode];
  182. }
  183. + (instancetype)provider {
  184. return [[self alloc]initWithAuth:[FIRAuth auth]];
  185. }
  186. + (instancetype)providerWithAuth:(FIRAuth *)auth {
  187. return [[self alloc]initWithAuth:auth];
  188. }
  189. #pragma mark - Internal Methods
  190. /** @fn isCallbackSchemeRegistered
  191. @brief Checks whether or not the expected callback scheme has been registered by the app.
  192. @remarks This method is thread-safe.
  193. */
  194. - (BOOL)isCallbackSchemeRegistered {
  195. NSString *expectedCustomScheme = [_callbackScheme lowercaseString];
  196. NSArray *urlTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"];
  197. for (NSDictionary *urlType in urlTypes) {
  198. NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"];
  199. for (NSString *urlTypeScheme in urlTypeSchemes) {
  200. if ([urlTypeScheme.lowercaseString isEqualToString:expectedCustomScheme]) {
  201. return YES;
  202. }
  203. }
  204. }
  205. return NO;
  206. }
  207. /** @fn reCAPTCHATokenForURL:error:
  208. @brief Parses the reCAPTCHA URL and returns.
  209. @param URL The url to be parsed for a reCAPTCHA token.
  210. @param error The error that occurred if any.
  211. @return The reCAPTCHA token if successful.
  212. */
  213. - (NSString *)reCAPTCHATokenForURL:(NSURL *)URL error:(NSError **)error {
  214. NSURLComponents *actualURLComponents = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
  215. NSArray<NSURLQueryItem *> *queryItems = [actualURLComponents queryItems];
  216. NSString *deepLinkURL = [FIRAuthWebUtils queryItemValue:@"deep_link_id" from:queryItems];
  217. NSData *errorData;
  218. if (deepLinkURL) {
  219. actualURLComponents = [NSURLComponents componentsWithString:deepLinkURL];
  220. queryItems = [actualURLComponents queryItems];
  221. NSString *recaptchaToken = [FIRAuthWebUtils queryItemValue:@"recaptchaToken" from:queryItems];
  222. if (recaptchaToken) {
  223. return recaptchaToken;
  224. }
  225. NSString *firebaseError = [FIRAuthWebUtils queryItemValue:@"firebaseError" from:queryItems];
  226. errorData = [firebaseError dataUsingEncoding:NSUTF8StringEncoding];
  227. } else {
  228. errorData = nil;
  229. }
  230. NSError *jsonError;
  231. NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData:errorData
  232. options:0
  233. error:&jsonError];
  234. if (jsonError) {
  235. *error = [FIRAuthErrorUtils JSONSerializationErrorWithUnderlyingError:jsonError];
  236. return nil;
  237. }
  238. *error = [FIRAuthErrorUtils URLResponseErrorWithCode:errorDict[@"code"]
  239. message:errorDict[@"message"]];
  240. if (!*error) {
  241. NSString *reason;
  242. if(errorDict[@"code"] && errorDict[@"message"]) {
  243. reason = [NSString stringWithFormat:@"[%@] - %@",errorDict[@"code"], errorDict[@"message"]];
  244. } else {
  245. reason = [NSString stringWithFormat:@"An unknown error occurred with the following "
  246. "response: %@", deepLinkURL];
  247. }
  248. *error = [FIRAuthErrorUtils appVerificationUserInteractionFailureWithReason:reason];
  249. }
  250. return nil;
  251. }
  252. /** @fn isVerifyAppURL:
  253. @brief Parses a URL into all available query items.
  254. @param URL The url to be checked against the authType string.
  255. @return Whether or not the URL matches authType.
  256. */
  257. - (BOOL)isVerifyAppURL:(nullable NSURL *)URL eventID:(NSString *)eventID {
  258. if (!URL) {
  259. return NO;
  260. }
  261. NSURLComponents *actualURLComponents =
  262. [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
  263. actualURLComponents.query = nil;
  264. actualURLComponents.fragment = nil;
  265. NSURLComponents *expectedURLComponents = [NSURLComponents new];
  266. expectedURLComponents.scheme = _callbackScheme;
  267. expectedURLComponents.host = @"firebaseauth";
  268. expectedURLComponents.path = @"/link";
  269. if (!([[expectedURLComponents URL] isEqual:[actualURLComponents URL]])) {
  270. return NO;
  271. }
  272. actualURLComponents = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
  273. NSArray<NSURLQueryItem *> *queryItems = [actualURLComponents queryItems];
  274. NSString *deepLinkURL = [FIRAuthWebUtils queryItemValue:@"deep_link_id" from:queryItems];
  275. if (deepLinkURL == nil) {
  276. return NO;
  277. }
  278. NSURLComponents *deepLinkURLComponents = [NSURLComponents componentsWithString:deepLinkURL];
  279. NSArray<NSURLQueryItem *> *deepLinkQueryItems = [deepLinkURLComponents queryItems];
  280. NSString *deepLinkAuthType = [FIRAuthWebUtils queryItemValue:@"authType" from:deepLinkQueryItems];
  281. NSString *deepLinkEventID = [FIRAuthWebUtils queryItemValue:@"eventId" from:deepLinkQueryItems];
  282. if ([deepLinkAuthType isEqualToString:kAuthTypeVerifyApp] &&
  283. [deepLinkEventID isEqualToString:eventID]) {
  284. return YES;
  285. }
  286. return NO;
  287. }
  288. /** @fn internalVerifyPhoneNumber:completion:
  289. @brief Starts the phone number authentication flow by sending a verifcation code to the
  290. specified phone number.
  291. @param phoneNumber The phone number to be verified.
  292. @param completion The callback to be invoked when the verification flow is finished.
  293. */
  294. - (void)internalVerifyPhoneNumber:(NSString *)phoneNumber
  295. completion:(nullable FIRVerificationResultCallback)completion {
  296. if (!phoneNumber.length) {
  297. completion(nil, [FIRAuthErrorUtils missingPhoneNumberErrorWithMessage:nil]);
  298. return;
  299. }
  300. [_auth.notificationManager checkNotificationForwardingWithCallback:
  301. ^(BOOL isNotificationBeingForwarded) {
  302. if (!isNotificationBeingForwarded) {
  303. completion(nil, [FIRAuthErrorUtils notificationNotForwardedError]);
  304. return;
  305. }
  306. FIRVerificationResultCallback callback = ^(NSString *_Nullable verificationID,
  307. NSError *_Nullable error) {
  308. if (completion) {
  309. completion(verificationID, error);
  310. }
  311. };
  312. [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
  313. retryOnInvalidAppCredential:YES
  314. callback:callback];
  315. }];
  316. }
  317. /** @fn verifyClientAndSendVerificationCodeToPhoneNumber:retryOnInvalidAppCredential:callback:
  318. @brief Starts the flow to verify the client via silent push notification.
  319. @param retryOnInvalidAppCredential Whether of not the flow should be retried if an
  320. FIRAuthErrorCodeInvalidAppCredential error is returned from the backend.
  321. @param phoneNumber The phone number to be verified.
  322. @param callback The callback to be invoked on the global work queue when the flow is
  323. finished.
  324. */
  325. - (void)verifyClientAndSendVerificationCodeToPhoneNumber:(NSString *)phoneNumber
  326. retryOnInvalidAppCredential:(BOOL)retryOnInvalidAppCredential
  327. callback:(FIRVerificationResultCallback)callback {
  328. if (_auth.settings.isAppVerificationDisabledForTesting) {
  329. FIRSendVerificationCodeRequest *request =
  330. [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
  331. appCredential:nil
  332. reCAPTCHAToken:nil
  333. requestConfiguration:
  334. _auth.requestConfiguration];
  335. [FIRAuthBackend sendVerificationCode:request
  336. callback:^(FIRSendVerificationCodeResponse *_Nullable response,
  337. NSError *_Nullable error) {
  338. callback(response.verificationID, error);
  339. }];
  340. return;
  341. }
  342. [self verifyClientWithCompletion:^(FIRAuthAppCredential *_Nullable appCredential,
  343. NSError *_Nullable error) {
  344. if (error) {
  345. callback(nil, error);
  346. return;
  347. }
  348. FIRSendVerificationCodeRequest *request =
  349. [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
  350. appCredential:appCredential
  351. reCAPTCHAToken:nil
  352. requestConfiguration:
  353. self->_auth.requestConfiguration];
  354. [FIRAuthBackend sendVerificationCode:request
  355. callback:^(FIRSendVerificationCodeResponse *_Nullable response,
  356. NSError *_Nullable error) {
  357. if (error) {
  358. if (error.code == FIRAuthErrorCodeInvalidAppCredential) {
  359. if (retryOnInvalidAppCredential) {
  360. [self->_auth.appCredentialManager clearCredential];
  361. [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
  362. retryOnInvalidAppCredential:NO
  363. callback:callback];
  364. return;
  365. }
  366. callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:nil
  367. underlyingError:error]);
  368. return;
  369. }
  370. callback(nil, error);
  371. return;
  372. }
  373. callback(response.verificationID, nil);
  374. }];
  375. }];
  376. }
  377. /** @fn verifyClientWithCompletion:completion:
  378. @brief Continues the flow to verify the client via silent push notification.
  379. @param completion The callback to be invoked when the client verification flow is finished.
  380. */
  381. - (void)verifyClientWithCompletion:(FIRVerifyClientCallback)completion {
  382. if (_auth.appCredentialManager.credential) {
  383. completion(_auth.appCredentialManager.credential, nil);
  384. return;
  385. }
  386. [_auth.tokenManager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token,
  387. NSError *_Nullable error) {
  388. if (!token) {
  389. completion(nil, [FIRAuthErrorUtils missingAppTokenErrorWithUnderlyingError:error]);
  390. return;
  391. }
  392. FIRVerifyClientRequest *request =
  393. [[FIRVerifyClientRequest alloc] initWithAppToken:token.string
  394. isSandbox:token.type == FIRAuthAPNSTokenTypeSandbox
  395. requestConfiguration:self->_auth.requestConfiguration];
  396. [FIRAuthBackend verifyClient:request callback:^(FIRVerifyClientResponse *_Nullable response,
  397. NSError *_Nullable error) {
  398. if (error) {
  399. completion(nil, error);
  400. return;
  401. }
  402. NSTimeInterval timeout = [response.suggestedTimeOutDate timeIntervalSinceNow];
  403. [self->_auth.appCredentialManager
  404. didStartVerificationWithReceipt:response.receipt
  405. timeout:timeout
  406. callback:^(FIRAuthAppCredential *credential) {
  407. if (!credential.secret) {
  408. FIRLogWarning(kFIRLoggerAuth, @"I-AUT000014",
  409. @"Failed to receive remote notification to verify app identity within "
  410. @"%.0f second(s)", timeout);
  411. }
  412. completion(credential, nil);
  413. }];
  414. }];
  415. }];
  416. }
  417. /** @fn reCAPTCHAURLWithEventID:completion:
  418. @brief Constructs a URL used for opening a reCAPTCHA app verification flow using a given event
  419. ID.
  420. @param eventID The event ID used for this purpose.
  421. @param completion The callback invoked after the URL has been constructed or an error
  422. has been encountered.
  423. */
  424. - (void)reCAPTCHAURLWithEventID:(NSString *)eventID completion:(FIRReCAPTCHAURLCallBack)completion {
  425. [self fetchAuthDomainWithCompletion:^(NSString *_Nullable authDomain,
  426. NSError *_Nullable error) {
  427. if (error) {
  428. completion(nil, error);
  429. return;
  430. }
  431. NSString *bundleID = [NSBundle mainBundle].bundleIdentifier;
  432. NSString *clientID = self->_auth.app.options.clientID;
  433. NSString *apiKey = self->_auth.requestConfiguration.APIKey;
  434. NSMutableArray<NSURLQueryItem *> *queryItems = [@[
  435. [NSURLQueryItem queryItemWithName:@"apiKey" value:apiKey],
  436. [NSURLQueryItem queryItemWithName:@"authType" value:kAuthTypeVerifyApp],
  437. [NSURLQueryItem queryItemWithName:@"ibi" value:bundleID ?: @""],
  438. [NSURLQueryItem queryItemWithName:@"clientId" value:clientID],
  439. [NSURLQueryItem queryItemWithName:@"v" value:[FIRAuthBackend authUserAgent]],
  440. [NSURLQueryItem queryItemWithName:@"eventId" value:eventID]
  441. ] mutableCopy
  442. ];
  443. if (self->_auth.requestConfiguration.languageCode) {
  444. [queryItems addObject:[NSURLQueryItem queryItemWithName:@"hl"value:
  445. self->_auth.requestConfiguration.languageCode]];
  446. }
  447. NSURLComponents *components = [[NSURLComponents alloc] initWithString:
  448. [NSString stringWithFormat:kReCAPTCHAURLStringFormat, authDomain]];
  449. [components setQueryItems:queryItems];
  450. completion([components URL], nil);
  451. }];
  452. }
  453. /** @fn fetchAuthDomainWithCompletion:completion:
  454. @brief Fetches the auth domain associated with the Firebase Project.
  455. @param completion The callback invoked after the auth domain has been constructed or an error
  456. has been encountered.
  457. */
  458. - (void)fetchAuthDomainWithCompletion:(FIRFetchAuthDomainCallback)completion {
  459. FIRGetProjectConfigRequest *request =
  460. [[FIRGetProjectConfigRequest alloc] initWithRequestConfiguration:_auth.requestConfiguration];
  461. [FIRAuthBackend getProjectConfig:request
  462. callback:^(FIRGetProjectConfigResponse *_Nullable response,
  463. NSError *_Nullable error) {
  464. if (error) {
  465. completion(nil, error);
  466. return;
  467. }
  468. NSString *authDomain;
  469. for (NSString *domain in response.authorizedDomains) {
  470. NSInteger index = domain.length - kAuthDomainSuffix.length;
  471. if (index >= 2) {
  472. if ([domain hasSuffix:kAuthDomainSuffix] && domain.length >= kAuthDomainSuffix.length + 2) {
  473. authDomain = domain;
  474. break;
  475. }
  476. }
  477. }
  478. if (!authDomain.length) {
  479. completion(nil, [FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:response]);
  480. return;
  481. }
  482. completion(authDomain, nil);
  483. }];
  484. }
  485. @end
  486. NS_ASSUME_NONNULL_END