AuthWebUtils.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // Copyright 2023 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  16. class AuthWebUtils: NSObject {
  17. static func randomString(withLength length: Int) -> String {
  18. var randomString = ""
  19. for _ in 0 ..< length {
  20. let randomValue = UInt32(arc4random_uniform(26) + 65)
  21. guard let randomCharacter = Unicode.Scalar(randomValue) else { continue }
  22. randomString += String(Character(randomCharacter))
  23. }
  24. return randomString
  25. }
  26. static func isCallbackSchemeRegistered(forCustomURLScheme scheme: String,
  27. urlTypes: [[String: Any]]) -> Bool {
  28. let expectedCustomScheme = scheme.lowercased()
  29. for urlType in urlTypes {
  30. guard let urlTypeSchemes = urlType["CFBundleURLSchemes"] else {
  31. continue
  32. }
  33. if let schemes = urlTypeSchemes as? [String] {
  34. for urlTypeScheme in schemes {
  35. if urlTypeScheme.lowercased() == expectedCustomScheme {
  36. return true
  37. }
  38. }
  39. }
  40. }
  41. return false
  42. }
  43. static func isExpectedCallbackURL(_ url: URL?, eventID: String, authType: String,
  44. callbackScheme: String) -> Bool {
  45. guard let url else {
  46. return false
  47. }
  48. var actualURLComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
  49. actualURLComponents?.query = nil
  50. actualURLComponents?.fragment = nil
  51. var expectedURLComponents = URLComponents()
  52. expectedURLComponents.scheme = callbackScheme
  53. expectedURLComponents.host = "firebaseauth"
  54. expectedURLComponents.path = "/link"
  55. guard let actualURL = actualURLComponents?.url,
  56. let expectedURL = expectedURLComponents.url else {
  57. return false
  58. }
  59. if expectedURL != actualURL {
  60. return false
  61. }
  62. let urlQueryItems = dictionary(withHttpArgumentsString: url.query)
  63. guard let deeplinkURLString = urlQueryItems["deep_link_id"],
  64. let deeplinkURL = URL(string: deeplinkURLString) else {
  65. return false
  66. }
  67. let deeplinkQueryItems = dictionary(withHttpArgumentsString: deeplinkURL.query)
  68. if deeplinkQueryItems["authType"] == authType, deeplinkQueryItems["eventId"] == eventID {
  69. return true
  70. }
  71. return false
  72. }
  73. /** @fn extractDomain:urlString
  74. @brief Strips url of scheme and path string to extract domain name
  75. @param urlString URL string for domain
  76. */
  77. static func extractDomain(urlString: String) -> String? {
  78. var domain = urlString
  79. // Check for the presence of a scheme (e.g., http:// or https://)
  80. if domain.prefix(7).caseInsensitiveCompare("http://") == .orderedSame {
  81. domain = String(domain.dropFirst(7))
  82. } else if domain.prefix(8).caseInsensitiveCompare("https://") == .orderedSame {
  83. domain = String(domain.dropFirst(8))
  84. }
  85. // Remove trailing slashes
  86. domain = (domain as NSString).standardizingPath as String
  87. // Split the URL by "/". The domain is the first component after removing the scheme.
  88. let urlComponents = domain.components(separatedBy: "/")
  89. return urlComponents.first
  90. }
  91. static func fetchAuthDomain(withRequestConfiguration requestConfiguration: AuthRequestConfiguration)
  92. async throws -> String {
  93. if let emulatorHostAndPort = requestConfiguration.emulatorHostAndPort {
  94. // If we are using the auth emulator, we do not want to call the GetProjectConfig endpoint.
  95. // The widget is hosted on the emulator host and port, so we can return that directly.
  96. return emulatorHostAndPort
  97. }
  98. let request = GetProjectConfigRequest(requestConfiguration: requestConfiguration)
  99. let response = try await AuthBackend.call(with: request)
  100. // Look up an authorized domain ends with one of the supportedAuthDomains.
  101. // The sequence of supportedAuthDomains matters. ("firebaseapp.com", "web.app")
  102. // The searching ends once the first valid suportedAuthDomain is found.
  103. var authDomain: String?
  104. if let customAuthDomain = requestConfiguration.auth?.customAuthDomain {
  105. if let customDomain = AuthWebUtils.extractDomain(urlString: customAuthDomain) {
  106. for domain in response.authorizedDomains ?? [] {
  107. if domain == customDomain {
  108. return domain
  109. }
  110. }
  111. }
  112. throw AuthErrorUtils.unauthorizedDomainError(
  113. message: "Error while validating application identity: The " +
  114. "configured custom domain is not allowlisted."
  115. )
  116. }
  117. for domain in response.authorizedDomains ?? [] {
  118. for supportedAuthDomain in Self.supportedAuthDomains {
  119. let index = domain.count - supportedAuthDomain.count
  120. if index >= 2, domain.hasSuffix(supportedAuthDomain),
  121. domain.count >= supportedAuthDomain.count + 2 {
  122. authDomain = domain
  123. break
  124. }
  125. }
  126. if authDomain != nil {
  127. break
  128. }
  129. }
  130. guard let authDomain = authDomain, !authDomain.isEmpty else {
  131. throw AuthErrorUtils.unexpectedErrorResponse(deserializedResponse: response)
  132. }
  133. return authDomain
  134. }
  135. static func queryItemValue(name: String, from queryList: [URLQueryItem]) -> String? {
  136. for item in queryList where item.name == name {
  137. return item.value
  138. }
  139. return nil
  140. }
  141. static func dictionary(withHttpArgumentsString argString: String?) -> [String: String] {
  142. guard let argString else {
  143. return [:]
  144. }
  145. var ret = [String: String]()
  146. let components = argString.components(separatedBy: "&")
  147. // Use reverse order so that the first occurrence of a key replaces
  148. // those subsequent.
  149. for component in components.reversed() {
  150. if component.isEmpty { continue }
  151. let pos = component.firstIndex(of: "=")
  152. var key: String
  153. var val: String
  154. if pos == nil {
  155. key = string(byUnescapingFromURLArgument: component)
  156. val = ""
  157. } else {
  158. let index = component.index(after: pos!)
  159. key = string(byUnescapingFromURLArgument: String(component[..<pos!]))
  160. val = string(byUnescapingFromURLArgument: String(component[index...]))
  161. }
  162. if key.isEmpty { key = "" }
  163. if val.isEmpty { val = "" }
  164. ret[key] = val
  165. }
  166. return ret
  167. }
  168. static func string(byUnescapingFromURLArgument argument: String) -> String {
  169. return argument
  170. .replacingOccurrences(of: "+", with: " ")
  171. .removingPercentEncoding ?? ""
  172. }
  173. static func parseURL(_ urlString: String) -> [String: String] {
  174. let urlComponents = URLComponents(string: urlString)
  175. guard let linkURL = urlComponents?.query else {
  176. return [:]
  177. }
  178. let queryComponents = linkURL.components(separatedBy: "&")
  179. var queryItems = [String: String]()
  180. for component in queryComponents {
  181. if let equalRange = component.range(of: "=") {
  182. let queryItemKey = component[..<equalRange.lowerBound].removingPercentEncoding
  183. let queryItemValue = component[equalRange.upperBound...].removingPercentEncoding
  184. if let queryItemKey = queryItemKey, let queryItemValue = queryItemValue {
  185. queryItems[queryItemKey] = queryItemValue
  186. }
  187. }
  188. }
  189. return queryItems
  190. }
  191. static var supportedAuthDomains: [String] {
  192. return ["firebaseapp.com", "web.app"]
  193. }
  194. }