No Description

FIRPhoneAuthProvider.m 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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. 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 occurred 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 associated 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 (![FIRAuthWebUtils isCallbackSchemeRegisteredForCustomURLScheme:_callbackScheme]) {
  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. NSString *eventID = [FIRAuthWebUtils randomStringWithLength:10];
  129. [self reCAPTCHAURLWithEventID:eventID completion:^(NSURL *_Nullable reCAPTCHAURL,
  130. NSError *_Nullable error) {
  131. if (error) {
  132. callBackOnMainThread(nil, error);
  133. return;
  134. }
  135. FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nullable callbackURL) {
  136. return [FIRAuthWebUtils isExpectedCallbackURL:callbackURL
  137. eventID:eventID
  138. authType:kAuthTypeVerifyApp
  139. callbackScheme:self->_callbackScheme];
  140. };
  141. [self->_auth.authURLPresenter presentURL:reCAPTCHAURL
  142. UIDelegate:UIDelegate
  143. callbackMatcher:callbackMatcher
  144. completion:^(NSURL *_Nullable callbackURL,
  145. NSError *_Nullable error) {
  146. if (error) {
  147. callBackOnMainThread(nil, error);
  148. return;
  149. }
  150. NSError *reCAPTCHAError;
  151. NSString *reCAPTCHAToken = [self reCAPTCHATokenForURL:callbackURL error:&reCAPTCHAError];
  152. if (!reCAPTCHAToken) {
  153. callBackOnMainThread(nil, reCAPTCHAError);
  154. return;
  155. }
  156. FIRSendVerificationCodeRequest *request =
  157. [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
  158. appCredential:nil
  159. reCAPTCHAToken:reCAPTCHAToken
  160. requestConfiguration:
  161. self->_auth.requestConfiguration];
  162. [FIRAuthBackend sendVerificationCode:request
  163. callback:^(FIRSendVerificationCodeResponse
  164. *_Nullable response, NSError *_Nullable error) {
  165. if (error) {
  166. callBackOnMainThread(nil, error);
  167. return;
  168. }
  169. callBackOnMainThread(response.verificationID, nil);
  170. }];
  171. }];
  172. }];
  173. }];
  174. });
  175. }
  176. - (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID
  177. verificationCode:(NSString *)verificationCode {
  178. return [[FIRPhoneAuthCredential alloc] initWithProviderID:FIRPhoneAuthProviderID
  179. verificationID:verificationID
  180. verificationCode:verificationCode];
  181. }
  182. + (instancetype)provider {
  183. return [[self alloc]initWithAuth:[FIRAuth auth]];
  184. }
  185. + (instancetype)providerWithAuth:(FIRAuth *)auth {
  186. return [[self alloc]initWithAuth:auth];
  187. }
  188. #pragma mark - Internal Methods
  189. /** @fn reCAPTCHATokenForURL:error:
  190. @brief Parses the reCAPTCHA URL and returns the reCAPTCHA token.
  191. @param URL The url to be parsed for a reCAPTCHA token.
  192. @param error The error that occurred if any.
  193. @return The reCAPTCHA token if successful.
  194. */
  195. - (NSString *)reCAPTCHATokenForURL:(NSURL *)URL error:(NSError **)error {
  196. NSURLComponents *actualURLComponents = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
  197. NSArray<NSURLQueryItem *> *queryItems = [actualURLComponents queryItems];
  198. NSString *deepLinkURL = [FIRAuthWebUtils queryItemValue:@"deep_link_id" from:queryItems];
  199. NSData *errorData;
  200. if (deepLinkURL) {
  201. actualURLComponents = [NSURLComponents componentsWithString:deepLinkURL];
  202. queryItems = [actualURLComponents queryItems];
  203. NSString *recaptchaToken = [FIRAuthWebUtils queryItemValue:@"recaptchaToken" from:queryItems];
  204. if (recaptchaToken) {
  205. return recaptchaToken;
  206. }
  207. NSString *firebaseError = [FIRAuthWebUtils queryItemValue:@"firebaseError" from:queryItems];
  208. errorData = [firebaseError dataUsingEncoding:NSUTF8StringEncoding];
  209. } else {
  210. errorData = nil;
  211. }
  212. NSError *jsonError;
  213. NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData:errorData
  214. options:0
  215. error:&jsonError];
  216. if (jsonError) {
  217. *error = [FIRAuthErrorUtils JSONSerializationErrorWithUnderlyingError:jsonError];
  218. return nil;
  219. }
  220. *error = [FIRAuthErrorUtils URLResponseErrorWithCode:errorDict[@"code"]
  221. message:errorDict[@"message"]];
  222. if (!*error) {
  223. NSString *reason;
  224. if(errorDict[@"code"] && errorDict[@"message"]) {
  225. reason = [NSString stringWithFormat:@"[%@] - %@",errorDict[@"code"], errorDict[@"message"]];
  226. } else {
  227. reason = [NSString stringWithFormat:@"An unknown error occurred with the following "
  228. "response: %@", deepLinkURL];
  229. }
  230. *error = [FIRAuthErrorUtils appVerificationUserInteractionFailureWithReason:reason];
  231. }
  232. return nil;
  233. }
  234. /** @fn internalVerifyPhoneNumber:completion:
  235. @brief Starts the phone number authentication flow by sending a verifcation code to the
  236. specified phone number.
  237. @param phoneNumber The phone number to be verified.
  238. @param completion The callback to be invoked when the verification flow is finished.
  239. */
  240. - (void)internalVerifyPhoneNumber:(NSString *)phoneNumber
  241. completion:(nullable FIRVerificationResultCallback)completion {
  242. if (!phoneNumber.length) {
  243. completion(nil, [FIRAuthErrorUtils missingPhoneNumberErrorWithMessage:nil]);
  244. return;
  245. }
  246. [_auth.notificationManager checkNotificationForwardingWithCallback:
  247. ^(BOOL isNotificationBeingForwarded) {
  248. if (!isNotificationBeingForwarded) {
  249. completion(nil, [FIRAuthErrorUtils notificationNotForwardedError]);
  250. return;
  251. }
  252. FIRVerificationResultCallback callback = ^(NSString *_Nullable verificationID,
  253. NSError *_Nullable error) {
  254. if (completion) {
  255. completion(verificationID, error);
  256. }
  257. };
  258. [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
  259. retryOnInvalidAppCredential:YES
  260. callback:callback];
  261. }];
  262. }
  263. /** @fn verifyClientAndSendVerificationCodeToPhoneNumber:retryOnInvalidAppCredential:callback:
  264. @brief Starts the flow to verify the client via silent push notification.
  265. @param retryOnInvalidAppCredential Whether of not the flow should be retried if an
  266. FIRAuthErrorCodeInvalidAppCredential error is returned from the backend.
  267. @param phoneNumber The phone number to be verified.
  268. @param callback The callback to be invoked on the global work queue when the flow is
  269. finished.
  270. */
  271. - (void)verifyClientAndSendVerificationCodeToPhoneNumber:(NSString *)phoneNumber
  272. retryOnInvalidAppCredential:(BOOL)retryOnInvalidAppCredential
  273. callback:(FIRVerificationResultCallback)callback {
  274. if (_auth.settings.isAppVerificationDisabledForTesting) {
  275. FIRSendVerificationCodeRequest *request =
  276. [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
  277. appCredential:nil
  278. reCAPTCHAToken:nil
  279. requestConfiguration:
  280. _auth.requestConfiguration];
  281. [FIRAuthBackend sendVerificationCode:request
  282. callback:^(FIRSendVerificationCodeResponse *_Nullable response,
  283. NSError *_Nullable error) {
  284. callback(response.verificationID, error);
  285. }];
  286. return;
  287. }
  288. [self verifyClientWithCompletion:^(FIRAuthAppCredential *_Nullable appCredential,
  289. NSError *_Nullable error) {
  290. if (error) {
  291. callback(nil, error);
  292. return;
  293. }
  294. FIRSendVerificationCodeRequest *request =
  295. [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
  296. appCredential:appCredential
  297. reCAPTCHAToken:nil
  298. requestConfiguration:
  299. self->_auth.requestConfiguration];
  300. [FIRAuthBackend sendVerificationCode:request
  301. callback:^(FIRSendVerificationCodeResponse *_Nullable response,
  302. NSError *_Nullable error) {
  303. if (error) {
  304. if (error.code == FIRAuthErrorCodeInvalidAppCredential) {
  305. if (retryOnInvalidAppCredential) {
  306. [self->_auth.appCredentialManager clearCredential];
  307. [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
  308. retryOnInvalidAppCredential:NO
  309. callback:callback];
  310. return;
  311. }
  312. callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:nil
  313. underlyingError:error]);
  314. return;
  315. }
  316. callback(nil, error);
  317. return;
  318. }
  319. callback(response.verificationID, nil);
  320. }];
  321. }];
  322. }
  323. /** @fn verifyClientWithCompletion:completion:
  324. @brief Continues the flow to verify the client via silent push notification.
  325. @param completion The callback to be invoked when the client verification flow is finished.
  326. */
  327. - (void)verifyClientWithCompletion:(FIRVerifyClientCallback)completion {
  328. if (_auth.appCredentialManager.credential) {
  329. completion(_auth.appCredentialManager.credential, nil);
  330. return;
  331. }
  332. [_auth.tokenManager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token,
  333. NSError *_Nullable error) {
  334. if (!token) {
  335. completion(nil, [FIRAuthErrorUtils missingAppTokenErrorWithUnderlyingError:error]);
  336. return;
  337. }
  338. FIRVerifyClientRequest *request =
  339. [[FIRVerifyClientRequest alloc] initWithAppToken:token.string
  340. isSandbox:token.type == FIRAuthAPNSTokenTypeSandbox
  341. requestConfiguration:self->_auth.requestConfiguration];
  342. [FIRAuthBackend verifyClient:request callback:^(FIRVerifyClientResponse *_Nullable response,
  343. NSError *_Nullable error) {
  344. if (error) {
  345. completion(nil, error);
  346. return;
  347. }
  348. NSTimeInterval timeout = [response.suggestedTimeOutDate timeIntervalSinceNow];
  349. [self->_auth.appCredentialManager
  350. didStartVerificationWithReceipt:response.receipt
  351. timeout:timeout
  352. callback:^(FIRAuthAppCredential *credential) {
  353. if (!credential.secret) {
  354. FIRLogWarning(kFIRLoggerAuth, @"I-AUT000014",
  355. @"Failed to receive remote notification to verify app identity within "
  356. @"%.0f second(s)", timeout);
  357. }
  358. completion(credential, nil);
  359. }];
  360. }];
  361. }];
  362. }
  363. /** @fn reCAPTCHAURLWithEventID:completion:
  364. @brief Constructs a URL used for opening a reCAPTCHA app verification flow using a given event
  365. ID.
  366. @param eventID The event ID used for this purpose.
  367. @param completion The callback invoked after the URL has been constructed or an error
  368. has been encountered.
  369. */
  370. - (void)reCAPTCHAURLWithEventID:(NSString *)eventID completion:(FIRReCAPTCHAURLCallBack)completion {
  371. [FIRAuthWebUtils fetchAuthDomainWithRequestConfiguration:_auth.requestConfiguration
  372. completion:^(NSString *_Nullable authDomain,
  373. NSError *_Nullable error) {
  374. if (error) {
  375. if (completion) {
  376. completion(nil, error);
  377. return;
  378. }
  379. }
  380. NSString *bundleID = [NSBundle mainBundle].bundleIdentifier;
  381. NSString *clientID = self->_auth.app.options.clientID;
  382. NSString *apiKey = self->_auth.requestConfiguration.APIKey;
  383. NSMutableArray<NSURLQueryItem *> *queryItems = [@[
  384. [NSURLQueryItem queryItemWithName:@"apiKey" value:apiKey],
  385. [NSURLQueryItem queryItemWithName:@"authType" value:kAuthTypeVerifyApp],
  386. [NSURLQueryItem queryItemWithName:@"ibi" value:bundleID ?: @""],
  387. [NSURLQueryItem queryItemWithName:@"clientId" value:clientID],
  388. [NSURLQueryItem queryItemWithName:@"v" value:[FIRAuthBackend authUserAgent]],
  389. [NSURLQueryItem queryItemWithName:@"eventId" value:eventID]
  390. ] mutableCopy
  391. ];
  392. if (self->_auth.requestConfiguration.languageCode) {
  393. [queryItems addObject:[NSURLQueryItem queryItemWithName:@"hl"value:
  394. self->_auth.requestConfiguration.languageCode]];
  395. }
  396. NSURLComponents *components = [[NSURLComponents alloc] initWithString:
  397. [NSString stringWithFormat:kReCAPTCHAURLStringFormat, authDomain]];
  398. [components setQueryItems:queryItems];
  399. completion([components URL], nil);
  400. }];
  401. }
  402. @end
  403. NS_ASSUME_NONNULL_END