FIRPhoneAuthProvider.m 19 KB

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