FIRAuthWebUtils.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. /*
  2. * Copyright 2018 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 "FirebaseAuth/Sources/Utilities/FIRAuthWebUtils.h"
  17. #import "FirebaseAuth/Sources/Backend/FIRAuthBackend.h"
  18. #import "FirebaseAuth/Sources/Backend/RPC/FIRGetProjectConfigRequest.h"
  19. #import "FirebaseAuth/Sources/Backend/RPC/FIRGetProjectConfigResponse.h"
  20. #import "FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h"
  21. NS_ASSUME_NONNULL_BEGIN
  22. @implementation FIRAuthWebUtils
  23. + (NSArray<NSString *> *)supportedAuthDomains {
  24. return @[ @"firebaseapp.com", @"web.app" ];
  25. }
  26. + (NSString *)randomStringWithLength:(NSUInteger)length {
  27. NSMutableString *randomString = [[NSMutableString alloc] init];
  28. for (int i = 0; i < length; i++) {
  29. [randomString
  30. appendString:[NSString stringWithFormat:@"%c", 'a' + arc4random_uniform('z' - 'a' + 1)]];
  31. }
  32. return randomString;
  33. }
  34. + (BOOL)isCallbackSchemeRegisteredForCustomURLScheme:(NSString *)URLScheme {
  35. NSString *expectedCustomScheme = [URLScheme lowercaseString];
  36. NSArray *urlTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"];
  37. for (NSDictionary *urlType in urlTypes) {
  38. NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"];
  39. for (NSString *urlTypeScheme in urlTypeSchemes) {
  40. if ([urlTypeScheme.lowercaseString isEqualToString:expectedCustomScheme]) {
  41. return YES;
  42. }
  43. }
  44. }
  45. return NO;
  46. }
  47. + (BOOL)isExpectedCallbackURL:(nullable NSURL *)URL
  48. eventID:(NSString *)eventID
  49. authType:(NSString *)authType
  50. callbackScheme:(NSString *)callbackScheme {
  51. if (!URL) {
  52. return NO;
  53. }
  54. NSURLComponents *actualURLComponents = [NSURLComponents componentsWithURL:URL
  55. resolvingAgainstBaseURL:NO];
  56. actualURLComponents.query = nil;
  57. actualURLComponents.fragment = nil;
  58. NSURLComponents *expectedURLComponents = [[NSURLComponents alloc] init];
  59. expectedURLComponents.scheme = callbackScheme;
  60. expectedURLComponents.host = @"firebaseauth";
  61. expectedURLComponents.path = @"/link";
  62. if (![expectedURLComponents.URL isEqual:actualURLComponents.URL]) {
  63. return NO;
  64. }
  65. NSDictionary<NSString *, NSString *> *URLQueryItems =
  66. [self dictionaryWithHttpArgumentsString:URL.query];
  67. NSURL *deeplinkURL = [NSURL URLWithString:URLQueryItems[@"deep_link_id"]];
  68. NSDictionary<NSString *, NSString *> *deeplinkQueryItems =
  69. [self dictionaryWithHttpArgumentsString:deeplinkURL.query];
  70. if ([deeplinkQueryItems[@"authType"] isEqualToString:authType] &&
  71. [deeplinkQueryItems[@"eventId"] isEqualToString:eventID]) {
  72. return YES;
  73. }
  74. return NO;
  75. }
  76. + (NSString *)extractDomain:(NSString *)urlString {
  77. // Remove trailing slashes
  78. urlString = [urlString
  79. stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]];
  80. // Check for the presence of a scheme (e.g., http:// or https://)
  81. NSRange range = [urlString rangeOfString:@"http://" options:NSCaseInsensitiveSearch];
  82. if (range.location != NSNotFound) {
  83. urlString = [urlString stringByReplacingCharactersInRange:range withString:@""];
  84. } else {
  85. range = [urlString rangeOfString:@"https://" options:NSCaseInsensitiveSearch];
  86. if (range.location != NSNotFound) {
  87. urlString = [urlString stringByReplacingCharactersInRange:range withString:@""];
  88. }
  89. }
  90. // Split the URL by "/"
  91. NSArray *urlComponents = [urlString componentsSeparatedByString:@"/"];
  92. // The domain is the first component after removing the scheme
  93. NSString *domain = urlComponents[0];
  94. return domain;
  95. }
  96. + (void)fetchAuthDomainWithRequestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration
  97. completion:(FIRFetchAuthDomainCallback)completion {
  98. if (requestConfiguration.emulatorHostAndPort) {
  99. // If we are using the auth emulator, we do not want to call the GetProjectConfig endpoint. The
  100. // widget is hosted on the emulator host and port, so we can return that directly.
  101. completion(requestConfiguration.emulatorHostAndPort, nil);
  102. return;
  103. }
  104. FIRGetProjectConfigRequest *request =
  105. [[FIRGetProjectConfigRequest alloc] initWithRequestConfiguration:requestConfiguration];
  106. [FIRAuthBackend
  107. getProjectConfig:request
  108. callback:^(FIRGetProjectConfigResponse *_Nullable response,
  109. NSError *_Nullable error) {
  110. if (error) {
  111. completion(nil, error);
  112. return;
  113. }
  114. // Look up an authorized domain ends with one of the supportedAuthDomains.
  115. // The searching ends once the first valid supportedAuthDomain is found.
  116. NSString *authDomain;
  117. NSString *customAuthDomain = requestConfiguration.auth.customAuthDomain;
  118. if (customAuthDomain) {
  119. customAuthDomain = [FIRAuthWebUtils extractDomain:customAuthDomain];
  120. BOOL isCustomAuthDomainAuthorized = NO;
  121. for (NSString *domain in response.authorizedDomains) {
  122. if ([customAuthDomain isEqualToString:domain]) {
  123. authDomain = customAuthDomain;
  124. isCustomAuthDomainAuthorized = YES;
  125. break;
  126. }
  127. }
  128. if (!isCustomAuthDomainAuthorized) {
  129. NSError *customDomainError =
  130. [FIRAuthErrorUtils unauthorizedDomainErrorWithMessage:
  131. @"Error while validating application identity: The "
  132. @"configured custom domain is not allowlisted."];
  133. completion(nil, customDomainError);
  134. return;
  135. }
  136. } else {
  137. for (NSString *domain in response.authorizedDomains) {
  138. for (NSString *supportedAuthDomain in [self supportedAuthDomains]) {
  139. NSInteger index = domain.length - supportedAuthDomain.length;
  140. if (index >= 2) {
  141. if ([domain hasSuffix:supportedAuthDomain] &&
  142. domain.length >= supportedAuthDomain.length + 2) {
  143. authDomain = domain;
  144. break;
  145. }
  146. }
  147. }
  148. if (authDomain != nil) {
  149. break;
  150. }
  151. }
  152. }
  153. if (!authDomain.length) {
  154. completion(nil, [FIRAuthErrorUtils
  155. unexpectedErrorResponseWithDeserializedResponse:response]);
  156. return;
  157. }
  158. completion(authDomain, nil);
  159. }];
  160. }
  161. /** @fn queryItemValue:from:
  162. @brief Utility function to get a value from a NSURLQueryItem array.
  163. @param name The key.
  164. @param queryList The NSURLQueryItem array.
  165. @return The value for the key.
  166. */
  167. + (nullable NSString *)queryItemValue:(NSString *)name from:(NSArray<NSURLQueryItem *> *)queryList {
  168. for (NSURLQueryItem *item in queryList) {
  169. if ([item.name isEqualToString:name]) {
  170. return item.value;
  171. }
  172. }
  173. return nil;
  174. }
  175. + (NSDictionary *)dictionaryWithHttpArgumentsString:(NSString *)argString {
  176. NSMutableDictionary *ret = [NSMutableDictionary dictionary];
  177. NSArray *components = [argString componentsSeparatedByString:@"&"];
  178. NSString *component;
  179. // Use reverse order so that the first occurrence of a key replaces
  180. // those subsequent.
  181. for (component in [components reverseObjectEnumerator]) {
  182. if (component.length == 0) continue;
  183. NSRange pos = [component rangeOfString:@"="];
  184. NSString *key;
  185. NSString *val;
  186. if (pos.location == NSNotFound) {
  187. key = [self stringByUnescapingFromURLArgument:component];
  188. val = @"";
  189. } else {
  190. key = [self stringByUnescapingFromURLArgument:[component substringToIndex:pos.location]];
  191. val = [self stringByUnescapingFromURLArgument:[component substringFromIndex:pos.location +
  192. pos.length]];
  193. }
  194. // returns nil on invalid UTF8 and NSMutableDictionary raises an exception when passed nil
  195. // values.
  196. if (!key) key = @"";
  197. if (!val) val = @"";
  198. [ret setObject:val forKey:key];
  199. }
  200. return ret;
  201. }
  202. + (NSString *)stringByUnescapingFromURLArgument:(NSString *)argument {
  203. NSMutableString *resultString = [NSMutableString stringWithString:argument];
  204. [resultString replaceOccurrencesOfString:@"+"
  205. withString:@" "
  206. options:NSLiteralSearch
  207. range:NSMakeRange(0, [resultString length])];
  208. return [resultString stringByRemovingPercentEncoding];
  209. }
  210. + (NSDictionary<NSString *, NSString *> *)parseURL:(NSString *)urlString {
  211. NSString *linkURL = [NSURLComponents componentsWithString:urlString].query;
  212. if (!linkURL) {
  213. return @{};
  214. }
  215. NSArray<NSString *> *URLComponents = [linkURL componentsSeparatedByString:@"&"];
  216. NSMutableDictionary<NSString *, NSString *> *queryItems =
  217. [[NSMutableDictionary alloc] initWithCapacity:URLComponents.count];
  218. for (NSString *component in URLComponents) {
  219. NSRange equalRange = [component rangeOfString:@"="];
  220. if (equalRange.location != NSNotFound) {
  221. NSString *queryItemKey =
  222. [[component substringToIndex:equalRange.location] stringByRemovingPercentEncoding];
  223. NSString *queryItemValue =
  224. [[component substringFromIndex:equalRange.location + 1] stringByRemovingPercentEncoding];
  225. if (queryItemKey && queryItemValue) {
  226. queryItems[queryItemKey] = queryItemValue;
  227. }
  228. }
  229. }
  230. return queryItems;
  231. }
  232. @end
  233. NS_ASSUME_NONNULL_END