Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 86 additions & 35 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import FirebaseAppCheckInterop
import FirebaseAuthInterop
import FirebaseCore
import FirebaseCoreExtension
import FirebaseCoreInternal
#if COCOAPODS
internal import GoogleUtilities
#else
Expand Down Expand Up @@ -83,48 +84,74 @@ extension Auth: AuthInterop {
public func getToken(forcingRefresh forceRefresh: Bool,
completion callback: @escaping (String?, Error?) -> Void) {
kAuthGlobalWorkQueue.async { [weak self] in
if let strongSelf = self {
// Enable token auto-refresh if not already enabled.
if !strongSelf.autoRefreshTokens {
AuthLog.logInfo(code: "I-AUT000002", message: "Token auto-refresh enabled.")
strongSelf.autoRefreshTokens = true
strongSelf.scheduleAutoTokenRefresh()

#if os(iOS) || os(tvOS) // TODO(ObjC): Is a similar mechanism needed on macOS?
strongSelf.applicationDidBecomeActiveObserver =
NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil, queue: nil
) { notification in
if let strongSelf = self {
strongSelf.isAppInBackground = false
if !strongSelf.autoRefreshScheduled {
strongSelf.scheduleAutoTokenRefresh()
}
}
}
strongSelf.applicationDidEnterBackgroundObserver =
NotificationCenter.default.addObserver(
forName: UIApplication.didEnterBackgroundNotification,
object: nil, queue: nil
) { notification in
if let strongSelf = self {
strongSelf.isAppInBackground = true
}
}
#endif
guard let self else {
DispatchQueue.main.async { callback(nil, nil) }
return
}
/// Before checking for a standard user, check if we are in a token-only session established
/// by a successful exchangeToken call.
let rGCIPToken = self.rGCIPFirebaseTokenLock.withLock { $0 }

if let token = rGCIPToken {
/// Logic for tokens obtained via exchangeToken (R-GCIP mode)
if token.expirationDate < Date() {
/// Token expired
let error = AuthErrorUtils
.userTokenExpiredError(
message: "The firebase access token obtained via exchangeToken() has expired."
)
Auth.wrapMainAsync(callback: callback, with: .failure(error))
} else if forceRefresh {
/// Token is not expired, but forceRefresh was requested which is currently unsupported
let error = AuthErrorUtils
.operationNotAllowedError(
message: "forceRefresh is not supported for firebase access tokens obtained via exchangeToken()."
)
Auth.wrapMainAsync(callback: callback, with: .failure(error))
} else {
/// The token is valid and not expired.
Auth.wrapMainAsync(callback: callback, with: .success(token.token))
}
/// Exit here as this path is for rGCIPFirebaseToken only.
return
}
/// Fallback to standard `currentUser` logic if not in token-only mode.
if !self.autoRefreshTokens {
AuthLog.logInfo(code: "I-AUT000002", message: "Token auto-refresh enabled.")
self.autoRefreshTokens = true
self.scheduleAutoTokenRefresh()

#if os(iOS) || os(tvOS)
self.applicationDidBecomeActiveObserver =
NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: nil
) { [weak self] _ in
guard let self = self, !self.isAppInBackground,
!self.autoRefreshScheduled else { return }
self.scheduleAutoTokenRefresh()
}
self.applicationDidEnterBackgroundObserver =
NotificationCenter.default.addObserver(
forName: UIApplication.didEnterBackgroundNotification,
object: nil,
queue: nil
) { [weak self] _ in
self?.isAppInBackground = true
}
#endif
}
// Call back with 'nil' if there is no current user.
guard let strongSelf = self, let currentUser = strongSelf._currentUser else {

guard let currentUser = self._currentUser else {
DispatchQueue.main.async {
callback(nil, nil)
}
return
}
// Call back with current user token.
currentUser
.internalGetToken(forceRefresh: forceRefresh, backend: strongSelf.backend) { token, error in
.internalGetToken(forceRefresh: forceRefresh, backend: self.backend) { token, error in
DispatchQueue.main.async {
callback(token, error)
}
Expand Down Expand Up @@ -2265,6 +2292,11 @@ extension Auth: AuthInterop {
return { result in
switch result {
case let .success(authResult):
/// When a standard user successfully signs in, any existing token-only session must be
/// invalidated to prevent a conflicting auth state.
/// Clear any R-GCIP session state when a standard user signs in. This ensures we exit
/// Token-Only Mode.
self.rGCIPFirebaseTokenLock.withLock { $0 = nil }
do {
try self.updateCurrentUser(authResult.user, byForce: false, savingToDisk: true)
Auth.wrapMainAsync(callback: callback, with: .success(authResult))
Expand Down Expand Up @@ -2428,9 +2460,20 @@ extension Auth: AuthInterop {
///
/// Mutations should occur within a @synchronized(self) context.
private var listenerHandles: NSMutableArray = []

// R-GCIP Token-Only Session State

/// The session token obtained from a successful `exchangeToken` call, protected by a lock.
///
/// This property is used to support a "token-only" authentication mode for Regionalized
/// GCIP, where no `User` object is created. It is mutually exclusive with `_currentUser`.
/// If the wrapped value is non-nil, the `AuthInterop` layer will use it for token generation
/// instead of relying on a `currentUser`.
private let rGCIPFirebaseTokenLock = FIRAllocatedUnfairLock<FirebaseToken?>(initialState: nil)
}

/// Regionalized auth
// MARK: - Regionalized auth

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public extension Auth {
/// Gets the Auth object for a `FirebaseApp` configured for a specific Regional Google Cloud
Expand Down Expand Up @@ -2511,10 +2554,18 @@ public extension Auth {
)
do {
let response = try await backend.call(with: request)
return FirebaseToken(
let newToken = FirebaseToken(
token: response.firebaseToken,
expirationDate: response.expirationDate
)
// Lock and update the token, signing out any current user.
rGCIPFirebaseTokenLock.withLock { token in
if self._currentUser != nil {
try? self.signOut()
}
token = newToken
}
return newToken
} catch {
throw error
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

import Foundation

private let regionalGCIPAPIHost = "identityplatform.googleapis.com"
private let regionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.googleapis.com"
private let kRegionalGCIPAPIHost = "identityplatform.googleapis.com"
private let kRegionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.googleapis.com"
Comment on lines +17 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Prefer the non-prefixed versions for constants. The k prefix is more common in ObjC

Suggested change
private let kRegionalGCIPAPIHost = "identityplatform.googleapis.com"
private let kRegionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.googleapis.com"
private let regionalGCIPAPIHost = "identityplatform.googleapis.com"
private let regionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.googleapis.com"


// MARK: - ExchangeTokenRequest

Expand Down Expand Up @@ -44,14 +44,6 @@ struct ExchangeTokenRequest: AuthRPCRequest {
/// Flag for whether to use the staging backend.
let useStaging: Bool

/// The base URL components for the request, will be used to construct the final URL.
private var baseURLComponents: URLComponents {
var components = URLComponents()
components.scheme = "https"
components.host = useStaging ? regionalGCIPStagingAPIHost : regionalGCIPAPIHost
return components
}

/// Initializes an `ExchangeTokenRequest`.
///
/// - Parameters:
Expand Down Expand Up @@ -89,16 +81,16 @@ struct ExchangeTokenRequest: AuthRPCRequest {
"Internal Error: ExchangeTokenRequest requires `location`, `tenantId`, and `projectID`."
)
}
var components = baseURLComponents
if location != "global" {
components.host = "\(location)-\(components.host ?? "")"
}
let locationPath = location == "global" ? "global" : location
components
.path =
"/v2beta/projects/\(project)/locations/\(locationPath)/tenants/\(tenant)/idpConfigs/\(idpConfigID):exchangeOidcToken"
components.queryItems = [URLQueryItem(name: "key", value: config.apiKey)]
guard let url = components.url else {
let baseHost = useStaging ? kRegionalGCIPStagingAPIHost : kRegionalGCIPAPIHost
let host = (location == "prod-global" || location == "global") ? baseHost :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there global and prod-global?

"\(location)-\(baseHost)"

let locationPath = (location == "prod-global") ? "global" : location

let path = "/v2beta/projects/\(project)/locations/\(locationPath)" +
"/tenants/\(tenant)/idpConfigs/\(idpConfigID):exchangeOidcToken"

guard let url = URL(string: "https://\(host)\(path)?key=\(config.apiKey)") else {
fatalError("Failed to create URL for ExchangeTokenRequest")
}
return url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,16 @@ struct ExchangeTokenResponse: AuthRPCResponse {
///
/// - Parameter dictionary: The dictionary representing the JSON response from server.
/// - Throws: `AuthErrorUtils.unexpectedResponse` if the required fields
/// (like "idToken", "expiresIn") are missing, have unexpected types
/// (like "accessToken", "expiresIn") are missing or have unexpected types.
init(dictionary: [String: AnyHashable]) throws {
guard let token = dictionary["idToken"] as? String else {
guard let token = dictionary["accessToken"] as? String else {
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
}
firebaseToken = token
guard let expiresInString = dictionary["expiresIn"] as? String,
let expiresInInterval = TimeInterval(expiresInString) else {
guard let expireIn = dictionary["expiresIn"] as? Int else {
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
}
expiresIn = expiresInInterval
expirationDate = Date().addingTimeInterval(expiresIn)
expiresIn = TimeInterval(expireIn)
expirationDate = Date().addingTimeInterval(TimeInterval(expiresIn))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ class AppManager {
private var otherApp: FirebaseApp
var app: FirebaseApp

// Initialise Auth with TenantConfig
let tenantConfig = Auth.TenantConfig(tenantId: "Foo-e2e-tenant-007", location: "global")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TenantConfig should be moved out of Auth namespace.

func auth() -> Auth {
return Auth.auth(app: app)
return Auth.auth(app: app, tenantConfig: tenantConfig)
}

private init() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ enum AuthMenu: String {
case phoneEnroll
case totpEnroll
case multifactorUnenroll
case exchangeToken

// More intuitively named getter for `rawValue`.
var id: String { rawValue }
Expand Down Expand Up @@ -139,6 +140,9 @@ enum AuthMenu: String {
return "TOTP Enroll"
case .multifactorUnenroll:
return "Multifactor unenroll"
// R-GCIP Exchange Token
case .exchangeToken:
return "Exchange Token"
}
}

Expand Down Expand Up @@ -220,6 +224,8 @@ enum AuthMenu: String {
self = .totpEnroll
case "Multifactor unenroll":
self = .multifactorUnenroll
case "Exchange Token":
self = .exchangeToken
default:
return nil
}
Expand Down Expand Up @@ -354,9 +360,16 @@ class AuthMenuData: DataSourceProvidable {
return Section(headerDescription: header, items: items)
}

static var exchangeTokenSection: Section {
let header = "Exchange Token [Regionalized Auth]"
let items: [Item] = [
Item(title: AuthMenu.exchangeToken.name),
]
return Section(headerDescription: header, items: items)
}

static let sections: [Section] =
[settingsSection, providerSection, emailPasswordSection, otherSection, recaptchaSection,
customAuthDomainSection, appSection, oobSection, multifactorSection]
[exchangeTokenSection]

static var authLinkSections: [Section] {
let allItems = [providerSection, emailPasswordSection, otherSection].flatMap { $0.items }
Expand Down
Loading
Loading