mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
205 lines
7.3 KiB
Swift
205 lines
7.3 KiB
Swift
// Copyright 2023 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import Foundation
|
|
|
|
class AuthWebUtils {
|
|
static func randomString(withLength length: Int) -> String {
|
|
guard length > 0 else { return "" }
|
|
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
var randomString = ""
|
|
randomString.reserveCapacity(length)
|
|
for _ in 0 ..< length {
|
|
randomString.append(alphabet.randomElement()!)
|
|
}
|
|
return randomString
|
|
}
|
|
|
|
static func isCallbackSchemeRegistered(forCustomURLScheme scheme: String,
|
|
urlTypes: [[String: Any]]) -> Bool {
|
|
let expectedCustomScheme = scheme.lowercased()
|
|
for urlType in urlTypes {
|
|
guard let urlTypeSchemes = urlType["CFBundleURLSchemes"] else {
|
|
continue
|
|
}
|
|
if let schemes = urlTypeSchemes as? [String] {
|
|
for urlTypeScheme in schemes {
|
|
if urlTypeScheme.lowercased() == expectedCustomScheme {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
static func isExpectedCallbackURL(_ url: URL?, eventID: String, authType: String,
|
|
callbackScheme: String) -> Bool {
|
|
guard let url else {
|
|
return false
|
|
}
|
|
var actualURLComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
|
actualURLComponents?.query = nil
|
|
actualURLComponents?.fragment = nil
|
|
|
|
var expectedURLComponents = URLComponents()
|
|
expectedURLComponents.scheme = callbackScheme
|
|
expectedURLComponents.host = "firebaseauth"
|
|
expectedURLComponents.path = "/link"
|
|
|
|
guard let actualURL = actualURLComponents?.url,
|
|
let expectedURL = expectedURLComponents.url else {
|
|
return false
|
|
}
|
|
if expectedURL != actualURL {
|
|
return false
|
|
}
|
|
let urlQueryItems = dictionary(withHttpArgumentsString: url.query)
|
|
guard let deeplinkURLString = urlQueryItems["deep_link_id"],
|
|
let deeplinkURL = URL(string: deeplinkURLString) else {
|
|
return false
|
|
}
|
|
let deeplinkQueryItems = dictionary(withHttpArgumentsString: deeplinkURL.query)
|
|
if deeplinkQueryItems["authType"] == authType, deeplinkQueryItems["eventId"] == eventID {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Strips url of scheme and path string to extract domain name
|
|
/// - Parameter urlString: URL string for domain
|
|
static func extractDomain(urlString: String) -> String? {
|
|
let trimmedURLString = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let lowercasedURLString = trimmedURLString.lowercased()
|
|
let urlStringWithScheme = lowercasedURLString.hasPrefix("http://") ||
|
|
lowercasedURLString.hasPrefix("https://") ? trimmedURLString : "https://" + trimmedURLString
|
|
|
|
guard let url = URL(string: urlStringWithScheme), let host = url.host else {
|
|
return nil
|
|
}
|
|
return host
|
|
}
|
|
|
|
static func fetchAuthDomain(withRequestConfiguration requestConfiguration: AuthRequestConfiguration,
|
|
backend: AuthBackend)
|
|
async throws -> String {
|
|
if let emulatorHostAndPort = requestConfiguration.emulatorHostAndPort {
|
|
// If we are using the auth emulator, we do not want to call the GetProjectConfig endpoint.
|
|
// The widget is hosted on the emulator host and port, so we can return that directly.
|
|
return emulatorHostAndPort
|
|
}
|
|
|
|
let request = GetProjectConfigRequest(requestConfiguration: requestConfiguration)
|
|
let response = try await backend.call(with: request)
|
|
|
|
// Look up an authorized domain ends with one of the supportedAuthDomains.
|
|
// The sequence of supportedAuthDomains matters. ("firebaseapp.com", "web.app")
|
|
// The searching ends once the first valid supportedAuthDomain is found.
|
|
var authDomain: String?
|
|
|
|
if let customAuthDomain = requestConfiguration.auth?.customAuthDomain {
|
|
if let customDomain = AuthWebUtils.extractDomain(urlString: customAuthDomain) {
|
|
for domain in response.authorizedDomains ?? [] {
|
|
if domain == customDomain {
|
|
return domain
|
|
}
|
|
}
|
|
}
|
|
throw AuthErrorUtils.unauthorizedDomainError(
|
|
message: "Error while validating application identity: The " +
|
|
"configured custom domain is not allowlisted."
|
|
)
|
|
}
|
|
for supportedAuthDomain in Self.supportedAuthDomains {
|
|
for domain in response.authorizedDomains ?? [] {
|
|
if domain.hasSuffix(supportedAuthDomain), domain.count > supportedAuthDomain.count {
|
|
authDomain = domain
|
|
break
|
|
}
|
|
}
|
|
if authDomain != nil {
|
|
break
|
|
}
|
|
}
|
|
guard let authDomain = authDomain, !authDomain.isEmpty else {
|
|
throw AuthErrorUtils.unexpectedErrorResponse(deserializedResponse: response)
|
|
}
|
|
return authDomain
|
|
}
|
|
|
|
static func queryItemValue(name: String, from queryList: [URLQueryItem]) -> String? {
|
|
for item in queryList where item.name == name {
|
|
return item.value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
static func dictionary(withHttpArgumentsString argString: String?) -> [String: String] {
|
|
guard let argString else {
|
|
return [:]
|
|
}
|
|
var ret = [String: String]()
|
|
let components = argString.components(separatedBy: "&")
|
|
// Use reverse order so that the first occurrence of a key replaces
|
|
// those subsequent.
|
|
for component in components.reversed() {
|
|
if component.isEmpty { continue }
|
|
let pos = component.firstIndex(of: "=")
|
|
var key: String
|
|
var val: String
|
|
if pos == nil {
|
|
key = string(byUnescapingFromURLArgument: component)
|
|
val = ""
|
|
} else {
|
|
let index = component.index(after: pos!)
|
|
key = string(byUnescapingFromURLArgument: String(component[..<pos!]))
|
|
val = string(byUnescapingFromURLArgument: String(component[index...]))
|
|
}
|
|
if key.isEmpty { key = "" }
|
|
if val.isEmpty { val = "" }
|
|
ret[key] = val
|
|
}
|
|
return ret
|
|
}
|
|
|
|
static func string(byUnescapingFromURLArgument argument: String) -> String {
|
|
return argument
|
|
.replacingOccurrences(of: "+", with: " ")
|
|
.removingPercentEncoding ?? ""
|
|
}
|
|
|
|
static func parseURL(_ urlString: String) -> [String: String] {
|
|
let urlComponents = URLComponents(string: urlString)
|
|
guard let linkURL = urlComponents?.query else {
|
|
return [:]
|
|
}
|
|
let queryComponents = linkURL.components(separatedBy: "&")
|
|
var queryItems = [String: String]()
|
|
for component in queryComponents {
|
|
if let equalRange = component.range(of: "=") {
|
|
let queryItemKey = component[..<equalRange.lowerBound].removingPercentEncoding
|
|
let queryItemValue = component[equalRange.upperBound...].removingPercentEncoding
|
|
if let queryItemKey = queryItemKey, let queryItemValue = queryItemValue {
|
|
queryItems[queryItemKey] = queryItemValue
|
|
}
|
|
}
|
|
}
|
|
return queryItems
|
|
}
|
|
|
|
static var supportedAuthDomains: [String] {
|
|
return [".firebaseapp.com", ".web.app"]
|
|
}
|
|
}
|