From 2c6d2239df08ef7bb235a93952c9e7d169a7dd62 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 27 Oct 2023 12:09:29 -0700 Subject: [PATCH] [auth-swift] Custom Auth Domains (#12010) Co-authored-by: pragatimodi <110490169+pragatimodi@users.noreply.github.com> --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 7 ++ .../Swift/Utilities/AuthWebUtils.swift | 37 +++++++++++ .../Models/AuthMenu.swift | 15 ++++- .../ViewControllers/AuthViewController.swift | 16 +++++ .../AuthenticationExampleUITests.swift | 4 +- .../Tests/Unit/AuthWebUtilsTests.swift | 66 +++++++++++++++++++ FirebaseAuth/Tests/Unit/SwiftAPI.swift | 5 +- 7 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 FirebaseAuth/Tests/Unit/AuthWebUtilsTests.swift diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index cca35725a10..279875661dd 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -218,6 +218,13 @@ extension Auth: AuthInterop { */ @objc public var tenantID: String? + /** + * @property customAuthDomain + * @brief The custom authentication domain used to handle all sign-in redirects. End-users will see + * this domain when signing in. This domain must be allowlisted in the Firebase Console. + */ + @objc public var customAuthDomain: String? + /** @fn updateCurrentUser:completion: @brief Sets the `currentUser` on the receiver to the provided user object. @param user The user object to be set as the current user of the calling Auth instance. diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift index 9a1758f5915..dc6711904c4 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift @@ -77,6 +77,29 @@ import Foundation return false } + /** @fn extractDomain:urlString + @brief Strips url of scheme and path string to extract domain name + @param urlString URL string for domain + */ + static func extractDomain(urlString: String) -> String? { + var domain = urlString + + // Check for the presence of a scheme (e.g., http:// or https://) + if domain.prefix(7).caseInsensitiveCompare("http://") == .orderedSame { + domain = String(domain.dropFirst(7)) + } else if domain.prefix(8).caseInsensitiveCompare("https://") == .orderedSame { + domain = String(domain.dropFirst(8)) + } + + // Remove trailing slashes + domain = (domain as NSString).standardizingPath as String + + // Split the URL by "/". The domain is the first component after removing the scheme. + let urlComponents = domain.components(separatedBy: "/") + + return urlComponents.first + } + static func fetchAuthDomain(withRequestConfiguration requestConfiguration: AuthRequestConfiguration) async throws -> String { if let emulatorHostAndPort = requestConfiguration.emulatorHostAndPort { @@ -92,6 +115,20 @@ import Foundation // The sequence of supportedAuthDomains matters. ("firebaseapp.com", "web.app") // The searching ends once the first valid suportedAuthDomain 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 domain in response.authorizedDomains ?? [] { for supportedAuthDomain in Self.supportedAuthDomains { let index = domain.count - supportedAuthDomain.count diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift index d04603384fd..b0abf158c96 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift @@ -30,6 +30,7 @@ enum AuthMenu: String { case anonymous case custom case initRecaptcha + case customAuthDomain /// More intuitively named getter for `rawValue`. var id: String { rawValue } @@ -65,6 +66,8 @@ enum AuthMenu: String { return "Custom Auth System" case .initRecaptcha: return "Initialize reCAPTCHA Enterprise" + case .customAuthDomain: + return "Set Custom Auth Domain" } } @@ -100,6 +103,8 @@ enum AuthMenu: String { self = .custom case "Initialize reCAPTCHA Enterprise": self = .initRecaptcha + case "Set Custom Auth Domain": + self = .customAuthDomain default: return nil } } @@ -155,8 +160,16 @@ extension AuthMenu: DataSourceProvidable { return Section(headerDescription: header, items: [item]) } + static var customAuthDomainSection: Section { + let image = UIImage(named: "firebaseIcon") + let header = "Custom Auth Domain" + let item = Item(title: customAuthDomain.name, hasNestedContent: false, image: image) + return Section(headerDescription: header, items: [item]) + } + static var sections: [Section] { - [settingsSection, providerSection, emailPasswordSection, otherSection, recaptchaSection] + [settingsSection, providerSection, emailPasswordSection, otherSection, recaptchaSection, + customAuthDomainSection] } static var authLinkSections: [Section] { diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index 01dd83b9ff2..c02e03d56b4 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -90,6 +90,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { case .initRecaptcha: performInitRecaptcha() + + case .customAuthDomain: + performCustomAuthDomainFlow() } } @@ -262,6 +265,19 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { } } + private func performCustomAuthDomainFlow() { + let prompt = UIAlertController(title: nil, message: "Enter Custom Auth Domain For Auth:", + preferredStyle: .alert) + prompt.addTextField() + let okAction = UIAlertAction(title: "OK", style: .default) { action in + let domain = prompt.textFields?[0].text ?? "" + AppManager.shared.auth().customAuthDomain = domain + print("Successfully set auth domain to: \(domain)") + } + prompt.addAction(okAction) + present(prompt, animated: true) + } + // MARK: - Private Helpers private func configureDataSourceProvider() { diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift index 34ebfcfafd0..1dc9bddd718 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift @@ -37,8 +37,8 @@ class AuthenticationExampleUITests: XCTestCase { } func testAuthOptions() { - // There are 13 sign in methods, each with its own cell - XCTAssertEqual(app.tables.cells.count, 14) + // There are 15 sign in methods, each with its own cell + XCTAssertEqual(app.tables.cells.count, 15) } func testAuthAnonymously() { diff --git a/FirebaseAuth/Tests/Unit/AuthWebUtilsTests.swift b/FirebaseAuth/Tests/Unit/AuthWebUtilsTests.swift new file mode 100644 index 00000000000..3353e9e0032 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/AuthWebUtilsTests.swift @@ -0,0 +1,66 @@ +// 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 +import XCTest + +@testable import FirebaseAuth + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class AuthWebUtilsTests: XCTestCase { + /** @fn testExtractDomainWithHTTP + @brief Test case for extracting the domain from a URL with "http://" scheme. + */ + func testExtractDomainWithHTTP() { + let urlString = "http://www.example.com/path/to/resource" + let domain = AuthWebUtils.extractDomain(urlString: urlString) + XCTAssertEqual(domain, "www.example.com") + } + + /** @fn testExtractDomainWithHTTPS + @brief Test case for extracting the domain from a URL with "https://" scheme. + */ + func testExtractDomainWithHTTPS() { + let urlString = "https://www.example.com/path/to/resource/" + let domain = AuthWebUtils.extractDomain(urlString: urlString) + XCTAssertEqual(domain, "www.example.com") + } + + /** @fn testExtractDomainWithoutScheme + @brief Test case for extracting the domain from a URL without a scheme (assumes HTTP by default). + */ + func testExtractDomainWithoutScheme() { + let urlString = "www.example.com/path/to/resource" + let domain = AuthWebUtils.extractDomain(urlString: urlString) + XCTAssertEqual(domain, "www.example.com") + } + + /** @fn testExtractDomainWithTrailingSlashes + @brief Test case for extracting the domain from a URL with trailing slashes. + */ + func testExtractDomainWithTrailingSlashes() { + let urlString = "http://www.example.com//////" + let domain = AuthWebUtils.extractDomain(urlString: urlString) + XCTAssertEqual(domain, "www.example.com") + } + + /** @fn testExtractDomainWithStringDomain + @brief Test case for extracting the domain from a string that represents just the domain itself. + */ + func testExtractDomainWithStringDomain() { + let urlString = "example.com" + let domain = AuthWebUtils.extractDomain(urlString: urlString) + XCTAssertEqual(domain, "example.com") + } +} diff --git a/FirebaseAuth/Tests/Unit/SwiftAPI.swift b/FirebaseAuth/Tests/Unit/SwiftAPI.swift index 70e91c053c3..c462ddf2d44 100644 --- a/FirebaseAuth/Tests/Unit/SwiftAPI.swift +++ b/FirebaseAuth/Tests/Unit/SwiftAPI.swift @@ -82,9 +82,8 @@ class AuthAPI_hOnlyTests: XCTestCase { let _: String = auth.languageCode, let _: AuthSettings = auth.settings, let _: String = auth.userAccessGroup, - let _: String = auth.tenantID - // Future PR will add customAuthDomain - // let _ : String = auth.customAuthDomain + let _: String = auth.tenantID, + let _: String = auth.customAuthDomain {} #if os(iOS)