Skip to content

Commit

Permalink
[auth-swift] Custom Auth Domains (#12010)
Browse files Browse the repository at this point in the history
Co-authored-by: pragatimodi <[email protected]>
  • Loading branch information
paulb777 and pragatimodi authored Oct 27, 2023
1 parent 24eb687 commit 2c6d223
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 6 deletions.
7 changes: 7 additions & 0 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -65,6 +66,8 @@ enum AuthMenu: String {
return "Custom Auth System"
case .initRecaptcha:
return "Initialize reCAPTCHA Enterprise"
case .customAuthDomain:
return "Set Custom Auth Domain"
}
}

Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {

case .initRecaptcha:
performInitRecaptcha()

case .customAuthDomain:
performCustomAuthDomainFlow()
}
}

Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
66 changes: 66 additions & 0 deletions FirebaseAuth/Tests/Unit/AuthWebUtilsTests.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
5 changes: 2 additions & 3 deletions FirebaseAuth/Tests/Unit/SwiftAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 2c6d223

Please sign in to comment.