FIRAuthRecaptchaVerifier.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. NSLog(@"reCAPTCHA verification faled because reCAPTCHA SDK "
  111. @"not linked.");
  112. completion(nil,
  113. [FIRAuthErrorUtils recaptchaSDKNotLinkedError]);
  114. }
  115. } else {
  116. NSLog(@"reCAPTCHA verification faled because reCAPTCHA SDK "
  117. @"not linked.");
  118. completion(nil,
  119. [FIRAuthErrorUtils recaptchaSDKNotLinkedError]);
  120. }
  121. } else {
  122. NSLog(@"reCAPTCHA verification succeeded.");
  123. [self retrieveRecaptchaTokenWithAction:action
  124. completion:completion];
  125. }
  126. }];
  127. }
  128. - (void)retrieveRecaptchaConfigForceRefresh:(BOOL)forceRefresh
  129. completion:(nullable FIRAuthRecaptchaConfigCallback)completion {
  130. if (!forceRefresh) {
  131. if (self.auth.tenantID == nil && _agentConfig != nil) {
  132. completion(nil);
  133. return;
  134. }
  135. if (self.auth.tenantID != nil && _tenantConfigs[self.auth.tenantID] != nil) {
  136. completion(nil);
  137. return;
  138. }
  139. }
  140. FIRGetRecaptchaConfigRequest *request = [[FIRGetRecaptchaConfigRequest alloc]
  141. initWithRequestConfiguration:self.auth.requestConfiguration];
  142. [FIRAuthBackend
  143. getRecaptchaConfig:request
  144. callback:^(FIRGetRecaptchaConfigResponse *_Nullable response,
  145. NSError *_Nullable error) {
  146. if (error) {
  147. NSLog(@"reCAPTCHA config retrieval failed.");
  148. completion(error);
  149. }
  150. NSLog(@"reCAPTCHA config retrieval succeeded.");
  151. FIRAuthRecaptchaConfig *config = [[FIRAuthRecaptchaConfig alloc] init];
  152. // Response's site key is of the format projects/<project-id>/keys/<site-key>'
  153. config.siteKey = [response.recaptchaKey componentsSeparatedByString:@"/"][3];
  154. NSMutableDictionary *tmpEnablementStatus = [NSMutableDictionary dictionary];
  155. for (NSDictionary *state in response.enforcementState) {
  156. if ([state[@"provider"]
  157. isEqualToString:providerToStringMap[
  158. @(FIRAuthRecaptchaProviderPassword)]]) {
  159. if ([state[@"enforcementState"] isEqualToString:@"ENFORCE"]) {
  160. tmpEnablementStatus[state[@"provider"]] = @YES;
  161. } else if ([state[@"enforcementState"] isEqualToString:@"AUDIT"]) {
  162. tmpEnablementStatus[state[@"provider"]] = @YES;
  163. } else if ([state[@"enforcementState"] isEqualToString:@"OFF"]) {
  164. tmpEnablementStatus[state[@"provider"]] = @NO;
  165. }
  166. }
  167. }
  168. config.enablementStatus = tmpEnablementStatus;
  169. if (self.auth.tenantID == nil) {
  170. self->_agentConfig = config;
  171. completion(nil);
  172. return;
  173. } else {
  174. if (!self->_tenantConfigs) {
  175. self->_tenantConfigs = [[NSMutableDictionary alloc] init];
  176. }
  177. self->_tenantConfigs[self.auth.tenantID] = config;
  178. completion(nil);
  179. return;
  180. }
  181. }];
  182. }
  183. - (void)retrieveRecaptchaTokenWithAction:(FIRAuthRecaptchaAction)action
  184. completion:(nullable FIRAuthRecaptchaTokenCallback)completion {
  185. Class RecaptchaActionClass = NSClassFromString(@"RecaptchaAction");
  186. if (RecaptchaActionClass) {
  187. SEL customActionSelector = NSSelectorFromString(@"initWithCustomAction:");
  188. if ([RecaptchaActionClass instancesRespondToSelector:customActionSelector]) {
  189. // Initialize with a custom action
  190. id (*funcWithCustomAction)(id, SEL, NSString *) =
  191. (id(*)(id, SEL,
  192. NSString *))[RecaptchaActionClass instanceMethodForSelector:customActionSelector];
  193. id<RCAActionProtocol> customAction = funcWithCustomAction(
  194. [[RecaptchaActionClass alloc] init], customActionSelector, actionToStringMap[@(action)]);
  195. if (customAction) {
  196. [self.recaptchaClient
  197. execute:customAction
  198. completion:^(NSString *_Nullable token, NSError *_Nullable error) {
  199. if (!error) {
  200. NSLog(@"reCAPTCHA token retrieval succeeded.");
  201. completion(token, nil);
  202. return;
  203. } else {
  204. NSLog(@"reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.");
  205. completion(kFakeToken, nil);
  206. }
  207. }];
  208. }
  209. } else {
  210. completion(nil, [FIRAuthErrorUtils recaptchaSDKNotLinkedError]);
  211. }
  212. } else {
  213. completion(nil, [FIRAuthErrorUtils recaptchaSDKNotLinkedError]);
  214. }
  215. }
  216. - (void)injectRecaptchaFields:(FIRIdentityToolkitRequest<FIRAuthRPCRequest> *)request
  217. provider:(FIRAuthRecaptchaProvider)provider
  218. action:(FIRAuthRecaptchaAction)action
  219. completion:(nullable FIRAuthInjectRequestCallback)completion {
  220. [self retrieveRecaptchaConfigForceRefresh:false
  221. completion:^(NSError *_Nullable error) {
  222. if ([self enablementStatusForProvider:provider]) {
  223. [self verifyForceRefresh:false
  224. action:action
  225. completion:^(NSString *_Nullable token,
  226. NSError *_Nullable error) {
  227. [request
  228. injectRecaptchaFields:token
  229. recaptchaVersion:kRecaptchaVersion];
  230. completion(request);
  231. }];
  232. } else {
  233. [request injectRecaptchaFields:nil
  234. recaptchaVersion:kRecaptchaVersion];
  235. completion(request);
  236. }
  237. }];
  238. }
  239. @end
  240. #endif