FIRAuthRecaptchaVerifier.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /*
  2. * Copyright 2023 Google LLC
  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 "FirebaseAuth/Sources/Utilities/FIRAuthRecaptchaVerifier.h"
  17. #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
  18. #import "FirebaseAuth/Sources/Auth/FIRAuth_Internal.h"
  19. #import "FirebaseAuth/Sources/Backend/FIRAuthBackend.h"
  20. #import "FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.h"
  21. #import "FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.h"
  22. #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyPasswordRequest.h"
  23. #import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuth.h"
  24. #import "FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h"
  25. #import <RecaptchaInterop/RCAActionProtocol.h>
  26. #import <RecaptchaInterop/RCARecaptchaProtocol.h>
  27. static const NSDictionary *providerToStringMap;
  28. static const NSDictionary *actionToStringMap;
  29. static NSString *const kClientType = @"CLIENT_TYPE_IOS";
  30. static NSString *const kRecaptchaVersion = @"RECAPTCHA_ENTERPRISE";
  31. static NSString *const kFakeToken = @"NO_RECAPTCHA";
  32. @implementation FIRAuthRecaptchaConfig
  33. @end
  34. @implementation FIRAuthRecaptchaVerifier
  35. + (id)sharedRecaptchaVerifier:(nullable FIRAuth *)auth {
  36. static FIRAuthRecaptchaVerifier *sharedRecaptchaVerifier = nil;
  37. static dispatch_once_t onceToken;
  38. dispatch_once(&onceToken, ^{
  39. sharedRecaptchaVerifier = [[self alloc] init];
  40. providerToStringMap = @{@(FIRAuthRecaptchaProviderPassword) : @"EMAIL_PASSWORD_PROVIDER"};
  41. actionToStringMap = @{
  42. @(FIRAuthRecaptchaActionSignInWithPassword) : @"signInWithPassword",
  43. @(FIRAuthRecaptchaActionGetOobCode) : @"getOobCode",
  44. @(FIRAuthRecaptchaActionSignUpPassword) : @"signUpPassword"
  45. };
  46. });
  47. if (sharedRecaptchaVerifier.auth != auth) {
  48. sharedRecaptchaVerifier.agentConfig = nil;
  49. sharedRecaptchaVerifier.tenantConfigs = nil;
  50. sharedRecaptchaVerifier.auth = auth;
  51. }
  52. return sharedRecaptchaVerifier;
  53. }
  54. - (NSString *)siteKey {
  55. if (self.auth.tenantID == nil) {
  56. return self->_agentConfig.siteKey;
  57. } else {
  58. FIRAuthRecaptchaConfig *config = self->_tenantConfigs[self.auth.tenantID];
  59. if (config) {
  60. return config.siteKey;
  61. } else {
  62. return nil;
  63. }
  64. }
  65. }
  66. - (BOOL)enablementStatusForProvider:(FIRAuthRecaptchaProvider)provider {
  67. if (self.auth.tenantID == nil) {
  68. return [self->_agentConfig.enablementStatus[providerToStringMap[@(provider)]] boolValue];
  69. } else {
  70. return
  71. [self->_tenantConfigs[self.auth.tenantID].enablementStatus[providerToStringMap[@(provider)]]
  72. boolValue];
  73. }
  74. }
  75. - (void)verifyForceRefresh:(BOOL)forceRefresh
  76. action:(FIRAuthRecaptchaAction)action
  77. completion:(nullable FIRAuthRecaptchaTokenCallback)completion {
  78. [self
  79. retrieveRecaptchaConfigForceRefresh:forceRefresh
  80. completion:^(NSError *_Nullable error) {
  81. if (error) {
  82. completion(nil, error);
  83. }
  84. if (!self.recaptchaClient) {
  85. NSString *siteKey = [self siteKey];
  86. Class RecaptchaClass = NSClassFromString(@"Recaptcha");
  87. if (RecaptchaClass) {
  88. SEL selector =
  89. NSSelectorFromString(@"getClientWithSiteKey:completion:");
  90. if ([RecaptchaClass respondsToSelector:selector]) {
  91. void (*funcWithoutTimeout)(
  92. id, SEL, NSString *,
  93. void (^)(
  94. id<RCARecaptchaClientProtocol> _Nullable recaptchaClient,
  95. NSError *_Nullable error)) =
  96. (void *)[RecaptchaClass methodForSelector:selector];
  97. funcWithoutTimeout(
  98. RecaptchaClass, selector, siteKey,
  99. ^(id<RCARecaptchaClientProtocol> _Nullable recaptchaClient,
  100. NSError *_Nullable error) {
  101. if (recaptchaClient) {
  102. self.recaptchaClient = recaptchaClient;
  103. [self retrieveRecaptchaTokenWithAction:action
  104. completion:completion];
  105. } else if (error) {
  106. completion(nil, error);
  107. }
  108. });
  109. } else {
  110. FIRLogError(kFIRLoggerAuth, @"I-AUT000026",
  111. @"reCAPTCHA verification failed because "
  112. @"reCAPTCHA SDK not linked.");
  113. completion(nil,
  114. [FIRAuthErrorUtils recaptchaSDKNotLinkedError]);
  115. }
  116. } else {
  117. FIRLogError(kFIRLoggerAuth, @"I-AUT000026",
  118. @"reCAPTCHA verification failed because reCAPTCHA "
  119. @"SDK not linked.");
  120. completion(nil,
  121. [FIRAuthErrorUtils recaptchaSDKNotLinkedError]);
  122. }
  123. } else {
  124. FIRLogInfo(kFIRLoggerAuth, @"I-AUT000027",
  125. @"reCAPTCHA verification succeeded.");
  126. [self retrieveRecaptchaTokenWithAction:action
  127. completion:completion];
  128. }
  129. }];
  130. }
  131. - (void)retrieveRecaptchaConfigForceRefresh:(BOOL)forceRefresh
  132. completion:(nullable FIRAuthRecaptchaConfigCallback)completion {
  133. if (!forceRefresh) {
  134. if (self.auth.tenantID == nil && _agentConfig != nil) {
  135. completion(nil);
  136. return;
  137. }
  138. if (self.auth.tenantID != nil && _tenantConfigs[self.auth.tenantID] != nil) {
  139. completion(nil);
  140. return;
  141. }
  142. }
  143. FIRGetRecaptchaConfigRequest *request = [[FIRGetRecaptchaConfigRequest alloc]
  144. initWithRequestConfiguration:self.auth.requestConfiguration];
  145. [FIRAuthBackend
  146. getRecaptchaConfig:request
  147. callback:^(FIRGetRecaptchaConfigResponse *_Nullable response,
  148. NSError *_Nullable error) {
  149. if (error) {
  150. FIRLogError(kFIRLoggerAuth, @"I-AUT000028",
  151. @"reCAPTCHA config retrieval failed.");
  152. completion(error);
  153. }
  154. FIRLogInfo(kFIRLoggerAuth, @"I-AUT000029",
  155. @"reCAPTCHA config retrieval succeeded.");
  156. FIRAuthRecaptchaConfig *config = [[FIRAuthRecaptchaConfig alloc] init];
  157. // Response's site key is of the format projects/<project-id>/keys/<site-key>'
  158. config.siteKey = [response.recaptchaKey componentsSeparatedByString:@"/"][3];
  159. NSMutableDictionary *tmpEnablementStatus = [NSMutableDictionary dictionary];
  160. for (NSDictionary *state in response.enforcementState) {
  161. if ([state[@"provider"]
  162. isEqualToString:providerToStringMap[
  163. @(FIRAuthRecaptchaProviderPassword)]]) {
  164. if ([state[@"enforcementState"] isEqualToString:@"ENFORCE"]) {
  165. tmpEnablementStatus[state[@"provider"]] = @YES;
  166. } else if ([state[@"enforcementState"] isEqualToString:@"AUDIT"]) {
  167. tmpEnablementStatus[state[@"provider"]] = @YES;
  168. } else if ([state[@"enforcementState"] isEqualToString:@"OFF"]) {
  169. tmpEnablementStatus[state[@"provider"]] = @NO;
  170. }
  171. }
  172. }
  173. config.enablementStatus = tmpEnablementStatus;
  174. if (self.auth.tenantID == nil) {
  175. self->_agentConfig = config;
  176. completion(nil);
  177. return;
  178. } else {
  179. if (!self->_tenantConfigs) {
  180. self->_tenantConfigs = [[NSMutableDictionary alloc] init];
  181. }
  182. self->_tenantConfigs[self.auth.tenantID] = config;
  183. completion(nil);
  184. return;
  185. }
  186. }];
  187. }
  188. - (void)retrieveRecaptchaTokenWithAction:(FIRAuthRecaptchaAction)action
  189. completion:(nullable FIRAuthRecaptchaTokenCallback)completion {
  190. Class RecaptchaActionClass = NSClassFromString(@"RecaptchaAction");
  191. if (RecaptchaActionClass) {
  192. SEL customActionSelector = NSSelectorFromString(@"initWithCustomAction:");
  193. if ([RecaptchaActionClass instancesRespondToSelector:customActionSelector]) {
  194. // Initialize with a custom action
  195. id (*funcWithCustomAction)(id, SEL, NSString *) =
  196. (id(*)(id, SEL,
  197. NSString *))[RecaptchaActionClass instanceMethodForSelector:customActionSelector];
  198. id<RCAActionProtocol> customAction = funcWithCustomAction(
  199. [[RecaptchaActionClass alloc] init], customActionSelector, actionToStringMap[@(action)]);
  200. if (customAction) {
  201. [self.recaptchaClient
  202. execute:customAction
  203. completion:^(NSString *_Nullable token, NSError *_Nullable error) {
  204. if (!error) {
  205. FIRLogInfo(kFIRLoggerAuth, @"I-AUT000030", @"reCAPTCHA token retrieval succeeded.");
  206. completion(token, nil);
  207. return;
  208. } else {
  209. FIRLogError(
  210. kFIRLoggerAuth, @"I-AUT000031",
  211. @"reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.");
  212. completion(kFakeToken, nil);
  213. }
  214. }];
  215. }
  216. } else {
  217. completion(nil, [FIRAuthErrorUtils recaptchaSDKNotLinkedError]);
  218. }
  219. } else {
  220. completion(nil, [FIRAuthErrorUtils recaptchaSDKNotLinkedError]);
  221. }
  222. }
  223. - (void)injectRecaptchaFields:(FIRIdentityToolkitRequest<FIRAuthRPCRequest> *)request
  224. provider:(FIRAuthRecaptchaProvider)provider
  225. action:(FIRAuthRecaptchaAction)action
  226. completion:(nullable FIRAuthInjectRequestCallback)completion {
  227. [self retrieveRecaptchaConfigForceRefresh:false
  228. completion:^(NSError *_Nullable error) {
  229. if ([self enablementStatusForProvider:provider]) {
  230. [self verifyForceRefresh:false
  231. action:action
  232. completion:^(NSString *_Nullable token,
  233. NSError *_Nullable error) {
  234. [request
  235. injectRecaptchaFields:token
  236. recaptchaVersion:kRecaptchaVersion];
  237. completion(request);
  238. }];
  239. } else {
  240. [request injectRecaptchaFields:nil
  241. recaptchaVersion:kRecaptchaVersion];
  242. completion(request);
  243. }
  244. }];
  245. }
  246. @end
  247. #endif