AuthWebUtils.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. /** @typedef FIRFetchAuthDomainCallback
  16. @brief The callback invoked at the end of the flow to fetch the Auth domain.
  17. @param authDomain The Auth domain.
  18. @param error The error that occurred while fetching the auth domain, if any.
  19. */
  20. typealias FIRFetchAuthDomainCallback = (String?, Error?) -> Void
  21. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  22. @objc(FIRAuthWebUtils) public class AuthWebUtils: NSObject {
  23. static func randomString(withLength length: Int) -> String {
  24. var randomString = ""
  25. for _ in 0 ..< length {
  26. let randomValue = UInt32(arc4random_uniform(26) + 65)
  27. guard let randomCharacter = Unicode.Scalar(randomValue) else { continue }
  28. randomString += String(Character(randomCharacter))
  29. }
  30. return randomString
  31. }
  32. @objc public static func isCallbackSchemeRegistered(forCustomURLScheme scheme: String,
  33. urlTypes: [[String: Any]]) -> Bool {
  34. let expectedCustomScheme = scheme.lowercased()
  35. for urlType in urlTypes {
  36. guard let urlTypeSchemes = urlType["CFBundleURLSchemes"] else {
  37. continue
  38. }
  39. if let schemes = urlTypeSchemes as? [String] {
  40. for urlTypeScheme in schemes {
  41. if urlTypeScheme.lowercased() == expectedCustomScheme {
  42. return true
  43. }
  44. }
  45. }
  46. }
  47. return false
  48. }
  49. static func isExpectedCallbackURL(_ url: URL?, eventID: String, authType: String,
  50. callbackScheme: String) -> Bool {
  51. guard let url else {
  52. return false
  53. }
  54. var actualURLComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
  55. actualURLComponents?.query = nil
  56. actualURLComponents?.fragment = nil
  57. var expectedURLComponents = URLComponents()
  58. expectedURLComponents.scheme = callbackScheme
  59. expectedURLComponents.host = "firebaseauth"
  60. expectedURLComponents.path = "/link"
  61. guard let actualURL = actualURLComponents?.url,
  62. let expectedURL = expectedURLComponents.url else {
  63. return false
  64. }
  65. if expectedURL != actualURL {
  66. return false
  67. }
  68. let urlQueryItems = dictionary(withHttpArgumentsString: url.query)
  69. guard let deeplinkURLString = urlQueryItems["deep_link_id"],
  70. let deeplinkURL = URL(string: deeplinkURLString) else {
  71. return false
  72. }
  73. let deeplinkQueryItems = dictionary(withHttpArgumentsString: deeplinkURL.query)
  74. if deeplinkQueryItems["authType"] == authType, deeplinkQueryItems["eventId"] == eventID {
  75. return true
  76. }
  77. return false
  78. }
  79. static func fetchAuthDomain(withRequestConfiguration requestConfiguration: AuthRequestConfiguration,
  80. completion: @escaping FIRFetchAuthDomainCallback) {
  81. if let emulatorHostAndPort = requestConfiguration.emulatorHostAndPort {
  82. // If we are using the auth emulator, we do not want to call the GetProjectConfig endpoint.
  83. // The widget is hosted on the emulator host and port, so we can return that directly.
  84. completion(emulatorHostAndPort, nil)
  85. return
  86. }
  87. let request = GetProjectConfigRequest(requestConfiguration: requestConfiguration)
  88. AuthBackend.post(with: request) { response, error in
  89. if let error = error {
  90. completion(nil, error)
  91. return
  92. }
  93. // Look up an authorized domain ends with one of the supportedAuthDomains.
  94. // The sequence of supportedAuthDomains matters. ("firebaseapp.com", "web.app")
  95. // The searching ends once the first valid suportedAuthDomain is found.
  96. var authDomain: String?
  97. if let response = response {
  98. for domain in response.authorizedDomains ?? [] {
  99. for supportedAuthDomain in Self.supportedAuthDomains {
  100. let index = domain.count - supportedAuthDomain.count
  101. if index >= 2, domain.hasSuffix(supportedAuthDomain),
  102. domain.count >= supportedAuthDomain.count + 2 {
  103. authDomain = domain
  104. break
  105. }
  106. }
  107. if authDomain != nil {
  108. break
  109. }
  110. }
  111. }
  112. if authDomain == nil || authDomain!.isEmpty {
  113. completion(
  114. nil,
  115. AuthErrorUtils.unexpectedErrorResponse(deserializedResponse: response)
  116. )
  117. return
  118. }
  119. completion(authDomain, nil)
  120. }
  121. }
  122. static func queryItemValue(name: String, from queryList: [URLQueryItem]) -> String? {
  123. for item in queryList where item.name == name {
  124. return item.value
  125. }
  126. return nil
  127. }
  128. @objc public static func dictionary(withHttpArgumentsString argString: String?)
  129. -> [String: String] {
  130. guard let argString else {
  131. return [:]
  132. }
  133. var ret = [String: String]()
  134. let components = argString.components(separatedBy: "&")
  135. // Use reverse order so that the first occurrence of a key replaces
  136. // those subsequent.
  137. for component in components.reversed() {
  138. if component.isEmpty { continue }
  139. let pos = component.firstIndex(of: "=")
  140. var key: String
  141. var val: String
  142. if pos == nil {
  143. key = string(byUnescapingFromURLArgument: component)
  144. val = ""
  145. } else {
  146. let index = component.index(after: pos!)
  147. key = string(byUnescapingFromURLArgument: String(component[..<pos!]))
  148. val = string(byUnescapingFromURLArgument: String(component[index...]))
  149. }
  150. if key.isEmpty { key = "" }
  151. if val.isEmpty { val = "" }
  152. ret[key] = val
  153. }
  154. return ret
  155. }
  156. static func string(byUnescapingFromURLArgument argument: String) -> String {
  157. return argument
  158. .replacingOccurrences(of: "+", with: " ")
  159. .removingPercentEncoding ?? ""
  160. }
  161. @objc public static func parseURL(_ urlString: String) -> [String: String] {
  162. let urlComponents = URLComponents(string: urlString)
  163. guard let linkURL = urlComponents?.query else {
  164. return [:]
  165. }
  166. let queryComponents = linkURL.components(separatedBy: "&")
  167. var queryItems = [String: String]()
  168. for component in queryComponents {
  169. if let equalRange = component.range(of: "=") {
  170. let queryItemKey = component[..<equalRange.lowerBound].removingPercentEncoding
  171. let queryItemValue = component[equalRange.upperBound...].removingPercentEncoding
  172. if let queryItemKey = queryItemKey, let queryItemValue = queryItemValue {
  173. queryItems[queryItemKey] = queryItemValue
  174. }
  175. }
  176. }
  177. return queryItems
  178. }
  179. static var supportedAuthDomains: [String] {
  180. return ["firebaseapp.com", "web.app"]
  181. }
  182. }