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.

445 lines
19 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
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 occurred 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 occurred while verifying the app, if any.
  52. */
  53. typedef void (^FIRVerifyClientCallback)(FIRAuthAppCredential *_Nullable appCredential,
  54. NSString *_Nullable reCAPTCHAToken,
  55. NSError *_Nullable error);
  56. /** @typedef FIRFetchAuthDomainCallback
  57. @brief The callback invoked at the end of the flow to fetch the Auth domain.
  58. @param authDomain The Auth domain.
  59. @param error The error that occurred while fetching the auth domain, if any.
  60. */
  61. typedef void (^FIRFetchAuthDomainCallback)(NSString *_Nullable authDomain,
  62. NSError *_Nullable error);
  63. /** @var kauthTypeVerifyApp
  64. @brief The auth type to be specified in the app verification request.
  65. */
  66. static NSString *const kAuthTypeVerifyApp = @"verifyApp";
  67. /** @var kReCAPTCHAURLStringFormat
  68. @brief The format of the URL used to open the reCAPTCHA page during app verification.
  69. */
  70. NSString *const kReCAPTCHAURLStringFormat = @"https://%@/__/auth/handler?";
  71. @implementation FIRPhoneAuthProvider {
  72. /** @var _auth
  73. @brief The auth instance used for verifying the phone number.
  74. */
  75. FIRAuth *_auth;
  76. /** @var _callbackScheme
  77. @brief The callback URL scheme used for reCAPTCHA fallback.
  78. */
  79. NSString *_callbackScheme;
  80. }
  81. /** @fn initWithAuth:
  82. @brief returns an instance of @c FIRPhoneAuthProvider associated with the provided auth
  83. instance.
  84. @return An Instance of @c FIRPhoneAuthProvider.
  85. */
  86. - (nullable instancetype)initWithAuth:(FIRAuth *)auth {
  87. self = [super init];
  88. if (self) {
  89. _auth = auth;
  90. _callbackScheme = [[[_auth.app.options.clientID componentsSeparatedByString:@"."]
  91. reverseObjectEnumerator].allObjects componentsJoinedByString:@"."];
  92. }
  93. return self;
  94. }
  95. - (void)verifyPhoneNumber:(NSString *)phoneNumber
  96. UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
  97. completion:(nullable FIRVerificationResultCallback)completion {
  98. if (![FIRAuthWebUtils isCallbackSchemeRegisteredForCustomURLScheme:_callbackScheme]) {
  99. [NSException raise:NSInternalInconsistencyException
  100. format:@"Please register custom URL scheme '%@' in the app's Info.plist file.",
  101. _callbackScheme];
  102. }
  103. dispatch_async(FIRAuthGlobalWorkQueue(), ^{
  104. FIRVerificationResultCallback callBackOnMainThread = ^(NSString *_Nullable verificationID,
  105. NSError *_Nullable error) {
  106. if (completion) {
  107. dispatch_async(dispatch_get_main_queue(), ^{
  108. completion(verificationID, error);
  109. });
  110. }
  111. };
  112. [self internalVerifyPhoneNumber:phoneNumber
  113. UIDelegate:UIDelegate
  114. completion:^(NSString *_Nullable verificationID,
  115. NSError *_Nullable error) {
  116. if (!error) {
  117. callBackOnMainThread(verificationID, nil);
  118. return;
  119. } else {
  120. callBackOnMainThread(nil, error);
  121. return;
  122. }
  123. }];
  124. });
  125. }
  126. - (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID
  127. verificationCode:(NSString *)verificationCode {
  128. return [[FIRPhoneAuthCredential alloc] initWithProviderID:FIRPhoneAuthProviderID
  129. verificationID:verificationID
  130. verificationCode:verificationCode];
  131. }
  132. + (instancetype)provider {
  133. return [[self alloc]initWithAuth:[FIRAuth auth]];
  134. }
  135. + (instancetype)providerWithAuth:(FIRAuth *)auth {
  136. return [[self alloc]initWithAuth:auth];
  137. }
  138. #pragma mark - Internal Methods
  139. /** @fn reCAPTCHATokenForURL:error:
  140. @brief Parses the reCAPTCHA URL and returns the reCAPTCHA token.
  141. @param URL The url to be parsed for a reCAPTCHA token.
  142. @param error The error that occurred if any.
  143. @return The reCAPTCHA token if successful.
  144. */
  145. - (NSString *)reCAPTCHATokenForURL:(NSURL *)URL error:(NSError **)error {
  146. NSURLComponents *actualURLComponents = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
  147. NSArray<NSURLQueryItem *> *queryItems = [actualURLComponents queryItems];
  148. NSString *deepLinkURL = [FIRAuthWebUtils queryItemValue:@"deep_link_id" from:queryItems];
  149. NSData *errorData;
  150. if (deepLinkURL) {
  151. actualURLComponents = [NSURLComponents componentsWithString:deepLinkURL];
  152. queryItems = [actualURLComponents queryItems];
  153. NSString *recaptchaToken = [FIRAuthWebUtils queryItemValue:@"recaptchaToken" from:queryItems];
  154. if (recaptchaToken) {
  155. return recaptchaToken;
  156. }
  157. NSString *firebaseError = [FIRAuthWebUtils queryItemValue:@"firebaseError" from:queryItems];
  158. errorData = [firebaseError dataUsingEncoding:NSUTF8StringEncoding];
  159. } else {
  160. errorData = nil;
  161. }
  162. NSError *jsonError;
  163. NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData:errorData
  164. options:0
  165. error:&jsonError];
  166. if (jsonError) {
  167. *error = [FIRAuthErrorUtils JSONSerializationErrorWithUnderlyingError:jsonError];
  168. return nil;
  169. }
  170. *error = [FIRAuthErrorUtils URLResponseErrorWithCode:errorDict[@"code"]
  171. message:errorDict[@"message"]];
  172. if (!*error) {
  173. NSString *reason;
  174. if(errorDict[@"code"] && errorDict[@"message"]) {
  175. reason = [NSString stringWithFormat:@"[%@] - %@",errorDict[@"code"], errorDict[@"message"]];
  176. } else {
  177. reason = [NSString stringWithFormat:@"An unknown error occurred with the following "
  178. "response: %@", deepLinkURL];
  179. }
  180. *error = [FIRAuthErrorUtils appVerificationUserInteractionFailureWithReason:reason];
  181. }
  182. return nil;
  183. }
  184. /** @fn internalVerifyPhoneNumber:completion:
  185. @brief Starts the phone number authentication flow by sending a verifcation code to the
  186. specified phone number.
  187. @param phoneNumber The phone number to be verified.
  188. @param completion The callback to be invoked when the verification flow is finished.
  189. */
  190. - (void)internalVerifyPhoneNumber:(NSString *)phoneNumber
  191. UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
  192. completion:(nullable FIRVerificationResultCallback)completion {
  193. if (!phoneNumber.length) {
  194. completion(nil, [FIRAuthErrorUtils missingPhoneNumberErrorWithMessage:nil]);
  195. return;
  196. }
  197. [_auth.notificationManager checkNotificationForwardingWithCallback:
  198. ^(BOOL isNotificationBeingForwarded) {
  199. if (!isNotificationBeingForwarded) {
  200. completion(nil, [FIRAuthErrorUtils notificationNotForwardedError]);
  201. return;
  202. }
  203. FIRVerificationResultCallback callback = ^(NSString *_Nullable verificationID,
  204. NSError *_Nullable error) {
  205. if (completion) {
  206. completion(verificationID, error);
  207. }
  208. };
  209. [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
  210. retryOnInvalidAppCredential:YES
  211. UIDelegate:UIDelegate
  212. callback:callback];
  213. }];
  214. }
  215. /** @fn verifyClientAndSendVerificationCodeToPhoneNumber:retryOnInvalidAppCredential:callback:
  216. @brief Starts the flow to verify the client via silent push notification.
  217. @param retryOnInvalidAppCredential Whether of not the flow should be retried if an
  218. FIRAuthErrorCodeInvalidAppCredential error is returned from the backend.
  219. @param phoneNumber The phone number to be verified.
  220. @param callback The callback to be invoked on the global work queue when the flow is
  221. finished.
  222. */
  223. - (void)verifyClientAndSendVerificationCodeToPhoneNumber:(NSString *)phoneNumber
  224. retryOnInvalidAppCredential:(BOOL)retryOnInvalidAppCredential
  225. UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
  226. callback:(FIRVerificationResultCallback)callback {
  227. if (_auth.settings.isAppVerificationDisabledForTesting) {
  228. FIRSendVerificationCodeRequest *request =
  229. [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
  230. appCredential:nil
  231. reCAPTCHAToken:nil
  232. requestConfiguration:
  233. _auth.requestConfiguration];
  234. [FIRAuthBackend sendVerificationCode:request
  235. callback:^(FIRSendVerificationCodeResponse *_Nullable response,
  236. NSError *_Nullable error) {
  237. callback(response.verificationID, error);
  238. }];
  239. return;
  240. }
  241. [self verifyClientWithUIDelegate:UIDelegate
  242. completion:^(FIRAuthAppCredential *_Nullable appCredential,
  243. NSString *_Nullable reCAPTCHAToken,
  244. NSError *_Nullable error) {
  245. if (error) {
  246. callback(nil, error);
  247. return;
  248. }
  249. FIRSendVerificationCodeRequest * _Nullable request;
  250. if (appCredential) {
  251. request =
  252. [[FIRSendVerificationCodeRequest alloc]
  253. initWithPhoneNumber:phoneNumber
  254. appCredential:appCredential
  255. reCAPTCHAToken:nil
  256. requestConfiguration:self->_auth.requestConfiguration];
  257. } else if (reCAPTCHAToken) {
  258. request =
  259. [[FIRSendVerificationCodeRequest alloc]
  260. initWithPhoneNumber:phoneNumber
  261. appCredential:nil
  262. reCAPTCHAToken:reCAPTCHAToken
  263. requestConfiguration:self->_auth.requestConfiguration];
  264. }
  265. if (request) {
  266. [FIRAuthBackend sendVerificationCode:request
  267. callback:^(FIRSendVerificationCodeResponse *_Nullable response,
  268. NSError *_Nullable error) {
  269. if (error) {
  270. if (error.code == FIRAuthErrorCodeInvalidAppCredential) {
  271. if (retryOnInvalidAppCredential) {
  272. [self->_auth.appCredentialManager clearCredential];
  273. [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
  274. retryOnInvalidAppCredential:NO
  275. UIDelegate:UIDelegate
  276. callback:callback];
  277. return;
  278. }
  279. callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:nil
  280. underlyingError:error]);
  281. return;
  282. }
  283. callback(nil, error);
  284. return;
  285. }
  286. callback(response.verificationID, nil);
  287. }];
  288. }
  289. }];
  290. }
  291. /** @fn verifyClientWithCompletion:completion:
  292. @brief Continues the flow to verify the client via silent push notification.
  293. @param completion The callback to be invoked when the client verification flow is finished.
  294. */
  295. - (void)verifyClientWithUIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
  296. completion:(FIRVerifyClientCallback)completion {
  297. if (_auth.appCredentialManager.credential) {
  298. completion(_auth.appCredentialManager.credential, nil, nil);
  299. return;
  300. }
  301. [_auth.tokenManager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token,
  302. NSError *_Nullable error) {
  303. if (!token) {
  304. [self reCAPTCHAFlowWithUIDelegate:UIDelegate completion:completion];
  305. return;
  306. }
  307. FIRVerifyClientRequest *request =
  308. [[FIRVerifyClientRequest alloc] initWithAppToken:token.string
  309. isSandbox:token.type == FIRAuthAPNSTokenTypeSandbox
  310. requestConfiguration:self->_auth.requestConfiguration];
  311. [FIRAuthBackend verifyClient:request callback:^(FIRVerifyClientResponse *_Nullable response,
  312. NSError *_Nullable error) {
  313. if (error) {
  314. NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
  315. BOOL isInvalidAppCredential = error.code == FIRAuthErrorCodeInternalError &&
  316. underlyingError.code == FIRAuthErrorCodeInvalidAppCredential;
  317. if (error.code != FIRAuthErrorCodeMissingAppToken && !isInvalidAppCredential) {
  318. completion(nil, nil, error);
  319. return;
  320. } else {
  321. [self reCAPTCHAFlowWithUIDelegate:UIDelegate completion:completion];
  322. return;
  323. }
  324. }
  325. NSTimeInterval timeout = [response.suggestedTimeOutDate timeIntervalSinceNow];
  326. [self->_auth.appCredentialManager
  327. didStartVerificationWithReceipt:response.receipt
  328. timeout:timeout
  329. callback:^(FIRAuthAppCredential *credential) {
  330. if (!credential.secret) {
  331. FIRLogWarning(kFIRLoggerAuth, @"I-AUT000014",
  332. @"Failed to receive remote notification to verify app identity within "
  333. @"%.0f second(s)", timeout);
  334. }
  335. completion(credential, nil, nil);
  336. }];
  337. }];
  338. }];
  339. }
  340. - (void)reCAPTCHAFlowWithUIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
  341. completion:(FIRVerifyClientCallback)completion {
  342. NSString *eventID = [FIRAuthWebUtils randomStringWithLength:10];
  343. [self reCAPTCHAURLWithEventID:eventID completion:^(NSURL *_Nullable reCAPTCHAURL,
  344. NSError *_Nullable error) {
  345. if (error) {
  346. completion(nil, nil, error);
  347. return;
  348. }
  349. FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nullable callbackURL) {
  350. return [FIRAuthWebUtils isExpectedCallbackURL:callbackURL
  351. eventID:eventID
  352. authType:kAuthTypeVerifyApp
  353. callbackScheme:self->_callbackScheme];
  354. };
  355. [self->_auth.authURLPresenter presentURL:reCAPTCHAURL
  356. UIDelegate:UIDelegate
  357. callbackMatcher:callbackMatcher
  358. completion:^(NSURL *_Nullable callbackURL,
  359. NSError *_Nullable error) {
  360. if (error) {
  361. completion(nil, nil, error);
  362. return;
  363. }
  364. NSError *reCAPTCHAError;
  365. NSString *reCAPTCHAToken = [self reCAPTCHATokenForURL:callbackURL error:&reCAPTCHAError];
  366. if (!reCAPTCHAToken) {
  367. completion(nil, nil, reCAPTCHAError);
  368. return;
  369. } else {
  370. completion(nil, reCAPTCHAToken, nil);
  371. return;
  372. }
  373. }];
  374. }];
  375. }
  376. /** @fn reCAPTCHAURLWithEventID:completion:
  377. @brief Constructs a URL used for opening a reCAPTCHA app verification flow using a given event
  378. ID.
  379. @param eventID The event ID used for this purpose.
  380. @param completion The callback invoked after the URL has been constructed or an error
  381. has been encountered.
  382. */
  383. - (void)reCAPTCHAURLWithEventID:(NSString *)eventID completion:(FIRReCAPTCHAURLCallBack)completion {
  384. [FIRAuthWebUtils fetchAuthDomainWithRequestConfiguration:_auth.requestConfiguration
  385. completion:^(NSString *_Nullable authDomain,
  386. NSError *_Nullable error) {
  387. if (error) {
  388. if (completion) {
  389. completion(nil, error);
  390. return;
  391. }
  392. }
  393. NSString *bundleID = [NSBundle mainBundle].bundleIdentifier;
  394. NSString *clientID = self->_auth.app.options.clientID;
  395. NSString *apiKey = self->_auth.requestConfiguration.APIKey;
  396. NSMutableArray<NSURLQueryItem *> *queryItems = [@[
  397. [NSURLQueryItem queryItemWithName:@"apiKey" value:apiKey],
  398. [NSURLQueryItem queryItemWithName:@"authType" value:kAuthTypeVerifyApp],
  399. [NSURLQueryItem queryItemWithName:@"ibi" value:bundleID ?: @""],
  400. [NSURLQueryItem queryItemWithName:@"clientId" value:clientID],
  401. [NSURLQueryItem queryItemWithName:@"v" value:[FIRAuthBackend authUserAgent]],
  402. [NSURLQueryItem queryItemWithName:@"eventId" value:eventID]
  403. ] mutableCopy
  404. ];
  405. if (self->_auth.requestConfiguration.languageCode) {
  406. [queryItems addObject:[NSURLQueryItem queryItemWithName:@"hl"value:
  407. self->_auth.requestConfiguration.languageCode]];
  408. }
  409. NSURLComponents *components = [[NSURLComponents alloc] initWithString:
  410. [NSString stringWithFormat:kReCAPTCHAURLStringFormat, authDomain]];
  411. [components setQueryItems:queryItems];
  412. if (completion) {
  413. completion([components URL], nil);
  414. }
  415. }];
  416. }
  417. @end
  418. NS_ASSUME_NONNULL_END