Files
firebase-ios-sdk/FirebaseAuth/Sources/Swift/User/UserProfileUpdate.swift
T
Nick Cookeandgemini-code-assist[bot] dc18b7f5b9 refactor(global): cleanup redundant availability checks and update sample apps audit (#16143)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-30 18:48:30 -04:00

185 lines
7.7 KiB
Swift

// Copyright 2024 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
/// Actor to serialize the update profile calls.
actor UserProfileUpdate {
func link(user: User, with credential: AuthCredential) async throws -> AuthDataResult {
let accessToken = try await user.internalGetTokenAsync(backend: user.backend)
let request = VerifyAssertionRequest(providerID: credential.provider,
requestConfiguration: user.requestConfiguration)
credential.prepare(request)
request.accessToken = accessToken
do {
let response = try await user.backend.call(with: request)
guard let idToken = response.idToken,
let refreshToken = response.refreshToken,
let providerID = response.providerID else {
fatalError("Internal Auth Error: missing token in EmailLinkSignInResponse")
}
try await updateTokenAndRefreshUser(user: user,
idToken: idToken,
refreshToken: refreshToken,
expirationDate: response.approximateExpirationDate)
let updatedOAuthCredential = OAuthCredential(withVerifyAssertionResponse: response)
let additionalUserInfo = AdditionalUserInfo(providerID: providerID,
profile: response.profile,
username: response.username,
isNewUser: response.isNewUser)
return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo,
credential: updatedOAuthCredential)
} catch {
user.signOutIfTokenIsInvalid(withError: error)
throw error
}
}
func unlink(user: User, fromProvider provider: String) async throws -> User {
let accessToken = try await user.internalGetTokenAsync(backend: user.backend)
let request = SetAccountInfoRequest(
accessToken: accessToken, requestConfiguration: user.requestConfiguration
)
let providerExists = user.isProviderLinked(provider: provider)
if !providerExists {
throw AuthErrorUtils.noSuchProviderError()
}
request.deleteProviders = [provider]
do {
let response = try await user.backend.call(with: request)
user.unlinkProvider(provider: provider)
if let idToken = response.idToken,
let refreshToken = response.refreshToken {
let tokenService = SecureTokenService(
withRequestConfiguration: user.requestConfiguration,
accessToken: idToken,
accessTokenExpirationDate: response.approximateExpirationDate,
refreshToken: refreshToken
)
try await setTokenService(user: user, tokenService: tokenService)
return user
}
} catch {
user.signOutIfTokenIsInvalid(withError: error)
throw error
}
if let error = user.updateKeychain() {
throw error
}
return user
}
/// Performs a setAccountInfo request by mutating the results of a getAccountInfo response,
/// atomically in regards to other calls to this method.
/// - Parameter changeBlock: A block responsible for mutating a template `SetAccountInfoRequest`
func executeUserUpdateWithChanges(user: User,
changeBlock: @escaping (GetAccountInfoResponse.User,
SetAccountInfoRequest)
-> Void) async throws {
let userAccountInfo = try await getAccountInfoRefreshingCache(user)
let accessToken = try await user.internalGetTokenAsync(backend: user.backend)
// Mutate setAccountInfoRequest in block
let setAccountInfoRequest =
SetAccountInfoRequest(
accessToken: accessToken,
requestConfiguration: user.requestConfiguration
)
changeBlock(userAccountInfo, setAccountInfoRequest)
do {
let accountInfoResponse = try await user.backend.call(with: setAccountInfoRequest)
if let idToken = accountInfoResponse.idToken,
let refreshToken = accountInfoResponse.refreshToken {
let tokenService = SecureTokenService(
withRequestConfiguration: user.requestConfiguration,
accessToken: idToken,
accessTokenExpirationDate: accountInfoResponse.approximateExpirationDate,
refreshToken: refreshToken
)
try await setTokenService(user: user, tokenService: tokenService)
}
} catch {
user.signOutIfTokenIsInvalid(withError: error)
throw error
}
}
// Update the new token and refresh user info again.
func updateTokenAndRefreshUser(user: User,
idToken: String,
refreshToken: String,
expirationDate: Date?) async throws {
user.tokenService = SecureTokenService(
withRequestConfiguration: user.requestConfiguration,
accessToken: idToken,
accessTokenExpirationDate: expirationDate,
refreshToken: refreshToken
)
let accessToken = try await user.internalGetTokenAsync(backend: user.backend)
let getAccountInfoRequest = GetAccountInfoRequest(
accessToken: accessToken,
requestConfiguration: user.requestConfiguration
)
do {
let response = try await user.backend.call(with: getAccountInfoRequest)
user.isAnonymous = false
user.update(withGetAccountInfoResponse: response)
} catch {
user.signOutIfTokenIsInvalid(withError: error)
throw error
}
if let error = user.updateKeychain() {
throw error
}
}
/// Sets a new token service for the `User` instance.
///
/// The method makes sure the token service has access and refresh token and the new tokens
/// are saved in the keychain before calling back.
/// - Parameter tokenService: The new token service object.
/// - Parameter callback: The block to be called in the global auth working queue once finished.
func setTokenService(user: User, tokenService: SecureTokenService) async throws {
_ = try await tokenService.fetchAccessToken(forcingRefresh: false, backend: user.backend)
user.tokenService = tokenService
if let error = user.updateKeychain() {
throw error
}
}
/// Gets the users' account data from the server, updating our local values.
/// - Parameter callback: Invoked when the request to getAccountInfo has completed, or when an
/// error has been detected. Invoked asynchronously on the auth global work queue in the future.
func getAccountInfoRefreshingCache(_ user: User) async throws
-> GetAccountInfoResponse.User {
let token = try await user.internalGetTokenAsync(backend: user.backend)
let request = GetAccountInfoRequest(accessToken: token,
requestConfiguration: user.requestConfiguration)
do {
let accountInfoResponse = try await user.backend.call(with: request)
user.update(withGetAccountInfoResponse: accountInfoResponse)
if let error = user.updateKeychain() {
throw error
}
return (accountInfoResponse.users?.first)!
} catch {
user.signOutIfTokenIsInvalid(withError: error)
throw error
}
}
}