| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787 |
- // 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
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- extension User: NSSecureCoding {}
- /// Represents a user.
- ///
- /// Firebase Auth does not attempt to validate users
- /// when loading them from the keychain. Invalidated users (such as those
- /// whose passwords have been changed on another client) are automatically
- /// logged out when an auth-dependent operation is attempted or when the
- /// ID token is automatically refreshed.
- ///
- /// This class is thread-safe.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- @objc(FIRUser) open class User: NSObject, UserInfo {
- /// Indicates the user represents an anonymous user.
- @objc public internal(set) var isAnonymous: Bool
- /// Indicates the user represents an anonymous user.
- @objc open func anonymous() -> Bool { return isAnonymous }
- /// Indicates the email address associated with this user has been verified.
- @objc public private(set) var isEmailVerified: Bool
- /// Indicates the email address associated with this user has been verified.
- @objc open func emailVerified() -> Bool { return isEmailVerified }
- /// Profile data for each identity provider, if any.
- ///
- /// This data is cached on sign-in and updated when linking or unlinking.
- @objc open var providerData: [UserInfo] {
- return Array(providerDataRaw.values)
- }
- var providerDataRaw: [String: UserInfoImpl]
- /// The backend service for the given instance.
- private(set) var backend: AuthBackend
- /// Metadata associated with the Firebase user in question.
- @objc public private(set) var metadata: UserMetadata
- /// The tenant ID of the current user. `nil` if none is available.
- @objc public private(set) var tenantID: String?
- #if os(iOS)
- /// Multi factor object associated with the user.
- ///
- /// This property is available on iOS only.
- @objc public private(set) var multiFactor: MultiFactor
- #endif
- /// [Deprecated] Updates the email address for the user.
- ///
- /// On success, the cached user profile data is updated. Returns an error when
- /// [Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection)
- /// is enabled.
- ///
- /// May fail if there is already an account with this email address that was created using
- /// email and password authentication.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was
- /// sent in the request.
- /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in
- /// the console for this action.
- /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for
- /// sending update email.
- /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email is already in use by another
- /// account.
- /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed.
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s email is a security
- /// sensitive operation that requires a recent login from the user. This error indicates
- /// the user has not signed in recently enough. To resolve, reauthenticate the user by
- /// calling `reauthenticate(with:)`.
- /// - Parameter email: The email address for the user.
- /// - Parameter completion: Optionally; the block invoked when the user profile change has
- /// finished.
- #if !FIREBASE_CI
- @available(
- *,
- deprecated,
- message: "`updateEmail` is deprecated and will be removed in a future release. Use sendEmailVerification(beforeUpdatingEmail:) instead."
- )
- #endif // !FIREBASE_CI
- @objc(updateEmail:completion:)
- open func updateEmail(to email: String, completion: ((Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- self.updateEmail(email: email, password: nil) { error in
- User.callInMainThreadWithError(callback: completion, error: error)
- }
- }
- }
- /// [Deprecated] Updates the email address for the user.
- ///
- /// On success, the cached user profile data is updated. Throws when
- /// [Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection)
- /// is enabled.
- ///
- /// May fail if there is already an account with this email address that was created using
- /// email and password authentication.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was
- /// sent in the request.
- /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in
- /// the console for this action.
- /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for
- /// sending update email.
- /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email is already in use by another
- /// account.
- /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed.
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s email is a security
- /// sensitive operation that requires a recent login from the user. This error indicates
- /// the user has not signed in recently enough. To resolve, reauthenticate the user by
- /// calling `reauthenticate(with:)`.
- /// - Parameter email: The email address for the user.
- #if !FIREBASE_CI
- @available(
- *,
- deprecated,
- message: "`updateEmail` is deprecated and will be removed in a future release. Use sendEmailVerification(beforeUpdatingEmail:) instead."
- )
- #endif // !FIREBASE_CI
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func updateEmail(to email: String) async throws {
- return try await withCheckedThrowingContinuation { continuation in
- self.updateEmail(to: email) { error in
- if let error {
- continuation.resume(throwing: error)
- } else {
- continuation.resume()
- }
- }
- }
- }
- /// Updates the password for the user. On success, the cached user profile data is updated.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled
- /// sign in with the specified identity provider.
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s password is a security
- /// sensitive operation that requires a recent login from the user. This error indicates
- /// the user has not signed in recently enough. To resolve, reauthenticate the user by
- /// calling `reauthenticate(with:)`.
- /// * `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is
- /// considered too weak. The `NSLocalizedFailureReasonErrorKey` field in the `userInfo`
- /// dictionary object will contain more detailed explanation that can be shown to the user.
- /// - Parameter password: The new password for the user.
- /// - Parameter completion: Optionally; the block invoked when the user profile change has
- /// finished.
- @objc(updatePassword:completion:)
- open func updatePassword(to password: String, completion: ((Error?) -> Void)? = nil) {
- guard password.count > 0 else {
- if let completion {
- completion(AuthErrorUtils.weakPasswordError(serverResponseReason: "Missing Password"))
- }
- return
- }
- kAuthGlobalWorkQueue.async {
- self.updateEmail(email: nil, password: password) { error in
- User.callInMainThreadWithError(callback: completion, error: error)
- }
- }
- }
- /// Updates the password for the user. On success, the cached user profile data is updated.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled
- /// sign in with the specified identity provider.
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s password is a security
- /// sensitive operation that requires a recent login from the user. This error indicates
- /// the user has not signed in recently enough. To resolve, reauthenticate the user by
- /// calling `reauthenticate(with:)`.
- /// * `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is
- /// considered too weak. The `NSLocalizedFailureReasonErrorKey` field in the `userInfo`
- /// dictionary object will contain more detailed explanation that can be shown to the user.
- /// - Parameter password: The new password for the user.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func updatePassword(to password: String) async throws {
- return try await withCheckedThrowingContinuation { continuation in
- self.updatePassword(to: password) { error in
- if let error {
- continuation.resume(throwing: error)
- } else {
- continuation.resume()
- }
- }
- }
- }
- #if os(iOS)
- /// Updates the phone number for the user. On success, the cached user profile data is updated.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// This method is available on iOS only.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s phone number is a security
- /// sensitive operation that requires a recent login from the user. This error indicates
- /// the user has not signed in recently enough. To resolve, reauthenticate the user by
- /// calling `reauthenticate(with:)`.
- /// - Parameter credential: The new phone number credential corresponding to the
- /// phone number to be added to the Firebase account, if a phone number is already linked to the
- /// account this new phone number will replace it.
- /// - Parameter completion: Optionally; the block invoked when the user profile change has
- /// finished.
- @objc(updatePhoneNumberCredential:completion:)
- open func updatePhoneNumber(_ credential: PhoneAuthCredential,
- completion: ((Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- self.internalUpdateOrLinkPhoneNumber(credential: credential,
- isLinkOperation: false) { error in
- User.callInMainThreadWithError(callback: completion, error: error)
- }
- }
- }
- /// Updates the phone number for the user. On success, the cached user profile data is updated.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// This method is available on iOS only.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s phone number is a security
- /// sensitive operation that requires a recent login from the user. This error indicates
- /// the user has not signed in recently enough. To resolve, reauthenticate the user by
- /// calling `reauthenticate(with:)`.
- /// - Parameter credential: The new phone number credential corresponding to the
- /// phone number to be added to the Firebase account, if a phone number is already linked to the
- /// account this new phone number will replace it.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func updatePhoneNumber(_ credential: PhoneAuthCredential) async throws {
- return try await withCheckedThrowingContinuation { continuation in
- self.updatePhoneNumber(credential) { error in
- if let error {
- continuation.resume(throwing: error)
- } else {
- continuation.resume()
- }
- }
- }
- }
- #endif
- /// Creates an object which may be used to change the user's profile data.
- ///
- /// Set the properties of the returned object, then call
- /// `UserProfileChangeRequest.commitChanges()` to perform the updates atomically.
- /// - Returns: An object which may be used to change the user's profile data atomically.
- @objc(profileChangeRequest)
- open func createProfileChangeRequest() -> UserProfileChangeRequest {
- var result: UserProfileChangeRequest!
- kAuthGlobalWorkQueue.sync {
- result = UserProfileChangeRequest(self)
- }
- return result
- }
- /// A refresh token; useful for obtaining new access tokens independently.
- ///
- /// This property should only be used for advanced scenarios, and is not typically needed.
- @objc open var refreshToken: String? {
- var result: String?
- kAuthGlobalWorkQueue.sync {
- result = self.tokenService.refreshToken
- }
- return result
- }
- /// Reloads the user's profile data from the server.
- ///
- /// May fail with an `AuthErrorCodeRequiresRecentLogin` error code. In this case
- /// you should call `reauthenticate(with:)` before re-invoking
- /// `updateEmail(to:)`.
- /// - Parameter completion: Optionally; the block invoked when the reload has finished. Invoked
- /// asynchronously on the main thread in the future.
- @objc open func reload(completion: ((Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- self.getAccountInfoRefreshingCache { user, error in
- User.callInMainThreadWithError(callback: completion, error: error)
- }
- }
- }
- /// Reloads the user's profile data from the server.
- ///
- /// May fail with an `AuthErrorCodeRequiresRecentLogin` error code. In this case
- /// you should call `reauthenticate(with:)` before re-invoking
- /// `updateEmail(to:)`.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func reload() async throws {
- return try await withCheckedThrowingContinuation { continuation in
- self.reload { error in
- if let error {
- continuation.resume(throwing: error)
- } else {
- continuation.resume()
- }
- }
- }
- }
- /// Renews the user's authentication tokens by validating a fresh set of credentials supplied
- /// by the user and returns additional identity provider data.
- ///
- /// If the user associated with the supplied credential is different from the current user,
- /// or if the validation of the supplied credentials fails; an error is returned and the current
- /// user remains signed in.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid.
- /// This could happen if it has expired or it is malformed.
- /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the
- /// identity provider represented by the credential are not enabled. Enable them in the
- /// Auth section of the Firebase console.
- /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email asserted by the credential
- /// (e.g. the email in a Facebook access token) is already in use by an existing account,
- /// that cannot be authenticated with this method. This error will only be thrown if the
- /// "One account per email address" setting is enabled in the Firebase console, under Auth
- /// settings. Please note that the error code raised in this specific situation may not be
- /// the same on Web and Android.
- /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled.
- /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted reauthentication with
- /// an incorrect password, if credential is of the type `EmailPasswordAuthCredential`.
- /// * `AuthErrorCodeUserMismatch` - Indicates that an attempt was made to
- /// reauthenticate with a user which is not the current user.
- /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed.
- /// - Parameter credential: A user-supplied credential, which will be validated by the server.
- /// This can be a successful third-party identity provider sign-in, or an email address and
- /// password.
- /// - Parameter completion: Optionally; the block invoked when the re-authentication operation has
- /// finished. Invoked asynchronously on the main thread in the future.
- @objc(reauthenticateWithCredential:completion:)
- open func reauthenticate(with credential: AuthCredential,
- completion: ((AuthDataResult?, Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- Task {
- do {
- let authResult = try await self.auth?.internalSignInAndRetrieveData(
- withCredential: credential,
- isReauthentication: true
- )
- guard let user = authResult?.user,
- user.uid == self.auth?.getUserID() else {
- User.callInMainThreadWithAuthDataResultAndError(
- callback: completion,
- result: authResult,
- error: AuthErrorUtils.userMismatchError()
- )
- return
- }
- // Successful reauthenticate
- do {
- try await self.userProfileUpdate.setTokenService(user: self,
- tokenService: user.tokenService)
- User.callInMainThreadWithAuthDataResultAndError(callback: completion,
- result: authResult,
- error: nil)
- } catch {
- User.callInMainThreadWithAuthDataResultAndError(callback: completion,
- result: authResult,
- error: error)
- }
- } catch {
- // If "user not found" error returned by backend,
- // translate to user mismatch error which is more
- // accurate.
- var reportError: Error = error
- if (error as NSError).code == AuthErrorCode.userNotFound.rawValue {
- reportError = AuthErrorUtils.userMismatchError()
- }
- User.callInMainThreadWithAuthDataResultAndError(callback: completion,
- result: nil,
- error: reportError)
- }
- }
- }
- }
- /// Renews the user's authentication tokens by validating a fresh set of credentials supplied
- /// by the user and returns additional identity provider data.
- ///
- /// If the user associated with the supplied credential is different from the current user,
- /// or if the validation of the supplied credentials fails; an error is returned and the current
- /// user remains signed in.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid.
- /// This could happen if it has expired or it is malformed.
- /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the
- /// identity provider represented by the credential are not enabled. Enable them in the
- /// Auth section of the Firebase console.
- /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email asserted by the credential
- /// (e.g. the email in a Facebook access token) is already in use by an existing account,
- /// that cannot be authenticated with this method. This error will only be thrown if the
- /// "One account per email address" setting is enabled in the Firebase console, under Auth
- /// settings. Please note that the error code raised in this specific situation may not be
- /// the same on Web and Android.
- /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled.
- /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted reauthentication with
- /// an incorrect password, if credential is of the type `EmailPasswordAuthCredential`.
- /// * `AuthErrorCodeUserMismatch` - Indicates that an attempt was made to
- /// reauthenticate with a user which is not the current user.
- /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed.
- /// - Parameter credential: A user-supplied credential, which will be validated by the server.
- /// This can be a successful third-party identity provider sign-in, or an email address and
- /// password.
- /// - Returns: The `AuthDataResult` after the reauthentication.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- @discardableResult
- open func reauthenticate(with credential: AuthCredential) async throws -> AuthDataResult {
- return try await withCheckedThrowingContinuation { continuation in
- self.reauthenticate(with: credential) { result, error in
- if let result {
- continuation.resume(returning: result)
- } else if let error {
- continuation.resume(throwing: error)
- }
- }
- }
- }
- #if os(iOS)
- /// Renews the user's authentication using the provided auth provider instance.
- ///
- /// This method is available on iOS only.
- /// - Parameter provider: An instance of an auth provider used to initiate the reauthenticate
- /// flow.
- /// - Parameter uiDelegate: Optionally an instance of a class conforming to the `AuthUIDelegate`
- /// protocol, used for presenting the web context. If nil, a default `AuthUIDelegate`
- /// will be used.
- /// - Parameter completion: Optionally; a block which is invoked when the reauthenticate flow
- /// finishes, or is canceled. Invoked asynchronously on the main thread in the future.
- @objc(reauthenticateWithProvider:UIDelegate:completion:)
- open func reauthenticate(with provider: FederatedAuthProvider,
- uiDelegate: AuthUIDelegate?,
- completion: ((AuthDataResult?, Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- Task {
- do {
- let credential = try await provider.credential(with: uiDelegate)
- self.reauthenticate(with: credential, completion: completion)
- } catch {
- if let completion {
- completion(nil, error)
- }
- }
- }
- }
- }
- /// Renews the user's authentication using the provided auth provider instance.
- ///
- /// This method is available on iOS only.
- /// - Parameter provider: An instance of an auth provider used to initiate the reauthenticate
- /// flow.
- /// - Parameter uiDelegate: Optionally an instance of a class conforming to the `AuthUIDelegate`
- /// protocol, used for presenting the web context. If nil, a default `AuthUIDelegate`
- /// will be used.
- /// - Returns: The `AuthDataResult` after the reauthentication.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- @discardableResult
- open func reauthenticate(with provider: FederatedAuthProvider,
- uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult {
- return try await withCheckedThrowingContinuation { continuation in
- self.reauthenticate(with: provider, uiDelegate: uiDelegate) { result, error in
- if let result {
- continuation.resume(returning: result)
- } else if let error {
- continuation.resume(throwing: error)
- }
- }
- }
- }
- #endif
- /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
- /// - Parameter completion: Optionally; the block invoked when the token is available. Invoked
- /// asynchronously on the main thread in the future.
- @objc(getIDTokenWithCompletion:)
- open func getIDToken(completion: ((String?, Error?) -> Void)?) {
- // |getIDTokenForcingRefresh:completion:| is also a public API so there is no need to dispatch to
- // global work queue here.
- getIDTokenForcingRefresh(false, completion: completion)
- }
- /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
- ///
- /// The authentication token will be refreshed (by making a network request) if it has
- /// expired, or if `forceRefresh` is `true`.
- /// - Parameter forceRefresh: Forces a token refresh. Useful if the token becomes invalid for some
- /// reason other than an expiration.
- /// - Parameter completion: Optionally; the block invoked when the token is available. Invoked
- /// asynchronously on the main thread in the future.
- @objc(getIDTokenForcingRefresh:completion:)
- open func getIDTokenForcingRefresh(_ forceRefresh: Bool,
- completion: ((String?, Error?) -> Void)?) {
- getIDTokenResult(forcingRefresh: forceRefresh) { tokenResult, error in
- if let completion {
- DispatchQueue.main.async {
- completion(tokenResult?.token, error)
- }
- }
- }
- }
- /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
- ///
- /// The authentication token will be refreshed (by making a network request) if it has
- /// expired, or if `forceRefresh` is `true`.
- /// - Parameter forceRefresh: Forces a token refresh. Useful if the token becomes invalid for some
- /// reason other than an expiration.
- /// - Returns: The Firebase authentication token.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func getIDToken(forcingRefresh forceRefresh: Bool = false) async throws -> String {
- return try await withCheckedThrowingContinuation { continuation in
- self.getIDTokenForcingRefresh(forceRefresh) { tokenResult, error in
- if let tokenResult {
- continuation.resume(returning: tokenResult)
- } else if let error {
- continuation.resume(throwing: error)
- }
- }
- }
- }
- /// API included for compatibility with a mis-named Firebase 10 API.
- /// Use `getIDToken(forcingRefresh forceRefresh: Bool = false)` instead.
- open func idTokenForcingRefresh(_ forceRefresh: Bool) async throws -> String {
- return try await getIDToken(forcingRefresh: forceRefresh)
- }
- /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
- /// - Parameter completion: Optionally; the block invoked when the token is available. Invoked
- /// asynchronously on the main thread in the future.
- @objc(getIDTokenResultWithCompletion:)
- open func getIDTokenResult(completion: ((AuthTokenResult?, Error?) -> Void)?) {
- getIDTokenResult(forcingRefresh: false) { tokenResult, error in
- if let completion {
- DispatchQueue.main.async {
- completion(tokenResult, error)
- }
- }
- }
- }
- /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
- ///
- /// The authentication token will be refreshed (by making a network request) if it has
- /// expired, or if `forcingRefresh` is `true`.
- /// - Parameter forcingRefresh: Forces a token refresh. Useful if the token becomes invalid for
- /// some
- /// reason other than an expiration.
- /// - Parameter completion: Optionally; the block invoked when the token is available. Invoked
- /// asynchronously on the main thread in the future.
- @objc(getIDTokenResultForcingRefresh:completion:)
- open func getIDTokenResult(forcingRefresh: Bool,
- completion: ((AuthTokenResult?, Error?) -> Void)?) {
- kAuthGlobalWorkQueue.async {
- self.internalGetToken(forceRefresh: forcingRefresh, backend: self.backend) { token, error in
- var tokenResult: AuthTokenResult?
- if let token {
- do {
- tokenResult = try AuthTokenResult.tokenResult(token: token)
- AuthLog.logDebug(code: "I-AUT000017", message: "Actual token expiration date: " +
- "\(String(describing: tokenResult?.expirationDate))," +
- "current date: \(Date())")
- if let completion {
- DispatchQueue.main.async {
- completion(tokenResult, error)
- }
- }
- return
- } catch {
- if let completion {
- DispatchQueue.main.async {
- completion(tokenResult, error)
- }
- }
- return
- }
- }
- if let completion {
- DispatchQueue.main.async {
- completion(nil, error)
- }
- }
- }
- }
- }
- /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
- ///
- /// The authentication token will be refreshed (by making a network request) if it has
- /// expired, or if `forceRefresh` is `true`.
- /// - Parameter forceRefresh: Forces a token refresh. Useful if the token becomes invalid for some
- /// reason other than an expiration.
- /// - Returns: The Firebase authentication token.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func getIDTokenResult(forcingRefresh forceRefresh: Bool = false) async throws
- -> AuthTokenResult {
- return try await withCheckedThrowingContinuation { continuation in
- self.getIDTokenResult(forcingRefresh: forceRefresh) { tokenResult, error in
- if let tokenResult {
- continuation.resume(returning: tokenResult)
- } else if let error {
- continuation.resume(throwing: error)
- }
- }
- }
- }
- /// Associates a user account from a third-party identity provider with this user and
- /// returns additional identity provider data.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeProviderAlreadyLinked` - Indicates an attempt to link a provider of a
- /// type already linked to this account.
- /// * `AuthErrorCodeCredentialAlreadyInUse` - Indicates an attempt to link with a
- /// credential that has already been linked with a different Firebase account.
- /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the identity
- /// provider represented by the credential are not enabled. Enable them in the Auth section
- /// of the Firebase console.
- ///
- /// This method may also return error codes associated with `updateEmail(to:)` and
- /// `updatePassword(to:)` on `User`.
- /// - Parameter credential: The credential for the identity provider.
- /// - Parameter completion: Optionally; the block invoked when the unlinking is complete, or
- /// fails.
- @objc(linkWithCredential:completion:)
- open func link(with credential: AuthCredential,
- completion: ((AuthDataResult?, Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- if self.providerDataRaw[credential.provider] != nil {
- User.callInMainThreadWithAuthDataResultAndError(
- callback: completion,
- result: nil,
- error: AuthErrorUtils.providerAlreadyLinkedError()
- )
- return
- }
- if let emailCredential = credential as? EmailAuthCredential {
- self.link(withEmailCredential: emailCredential, completion: completion)
- return
- }
- #if !os(watchOS)
- if let gameCenterCredential = credential as? GameCenterAuthCredential {
- self.link(withGameCenterCredential: gameCenterCredential, completion: completion)
- return
- }
- #endif
- #if os(iOS)
- if let phoneCredential = credential as? PhoneAuthCredential {
- self.link(withPhoneCredential: phoneCredential, completion: completion)
- return
- }
- #endif
- Task {
- do {
- let authDataResult = try await self.userProfileUpdate.link(user: self, with: credential)
- await MainActor.run {
- completion?(authDataResult, nil)
- }
- } catch {
- await MainActor.run {
- completion?(nil, error)
- }
- }
- }
- }
- }
- /// Associates a user account from a third-party identity provider with this user and
- /// returns additional identity provider data.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeProviderAlreadyLinked` - Indicates an attempt to link a provider of a
- /// type already linked to this account.
- /// * `AuthErrorCodeCredentialAlreadyInUse` - Indicates an attempt to link with a
- /// credential that has already been linked with a different Firebase account.
- /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the identity
- /// provider represented by the credential are not enabled. Enable them in the Auth section
- /// of the Firebase console.
- ///
- /// This method may also return error codes associated with `updateEmail(to:)` and
- /// `updatePassword(to:)` on `User`.
- /// - Parameter credential: The credential for the identity provider.
- /// - Returns: An `AuthDataResult`.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- @discardableResult
- open func link(with credential: AuthCredential) async throws -> AuthDataResult {
- return try await withCheckedThrowingContinuation { continuation in
- self.link(with: credential) { result, error in
- if let result {
- continuation.resume(returning: result)
- } else if let error {
- continuation.resume(throwing: error)
- }
- }
- }
- }
- #if os(iOS)
- /// Link the user with the provided auth provider instance.
- ///
- /// This method is available on iOSonly.
- /// - Parameter provider: An instance of an auth provider used to initiate the link flow.
- /// - Parameter uiDelegate: Optionally an instance of a class conforming to the `AuthUIDelegate`
- /// protocol used for presenting the web context. If nil, a default `AuthUIDelegate` will be
- /// used.
- /// - Parameter completion: Optionally; a block which is invoked when the link flow finishes, or
- /// is canceled. Invoked asynchronously on the main thread in the future.
- @objc(linkWithProvider:UIDelegate:completion:)
- open func link(with provider: FederatedAuthProvider,
- uiDelegate: AuthUIDelegate?,
- completion: ((AuthDataResult?, Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- Task {
- do {
- let credential = try await provider.credential(with: uiDelegate)
- self.link(with: credential, completion: completion)
- } catch {
- if let completion {
- completion(nil, error)
- }
- }
- }
- }
- }
- /// Link the user with the provided auth provider instance.
- ///
- /// This method is available on iOSonly.
- /// - Parameter provider: An instance of an auth provider used to initiate the link flow.
- /// - Parameter uiDelegate: Optionally an instance of a class conforming to the `AuthUIDelegate`
- /// protocol used for presenting the web context. If nil, a default `AuthUIDelegate`
- /// will be used.
- /// - Returns: An AuthDataResult.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- @discardableResult
- open func link(with provider: FederatedAuthProvider,
- uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult {
- return try await withCheckedThrowingContinuation { continuation in
- self.link(with: provider, uiDelegate: uiDelegate) { result, error in
- if let result {
- continuation.resume(returning: result)
- } else if let error {
- continuation.resume(throwing: error)
- }
- }
- }
- }
- #endif
- /// Disassociates a user account from a third-party identity provider with this user.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeNoSuchProvider` - Indicates an attempt to unlink a provider
- /// that is not linked to the account.
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive
- /// operation that requires a recent login from the user. This error indicates the user
- /// has not signed in recently enough. To resolve, reauthenticate the user by calling
- /// `reauthenticate(with:)`.
- /// - Parameter provider: The provider ID of the provider to unlink.
- /// - Parameter completion: Optionally; the block invoked when the unlinking is complete, or
- /// fails.
- @objc open func unlink(fromProvider provider: String,
- completion: ((User?, Error?) -> Void)? = nil) {
- Task {
- do {
- let user = try await unlink(fromProvider: provider)
- await MainActor.run {
- completion?(user, nil)
- }
- } catch {
- await MainActor.run {
- completion?(nil, error)
- }
- }
- }
- }
- /// Disassociates a user account from a third-party identity provider with this user.
- ///
- /// Invoked asynchronously on the main thread in the future.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeNoSuchProvider` - Indicates an attempt to unlink a provider
- /// that is not linked to the account.
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive
- /// operation that requires a recent login from the user. This error indicates the user
- /// has not signed in recently enough. To resolve, reauthenticate the user by calling
- /// `reauthenticate(with:)`.
- /// - Parameter provider: The provider ID of the provider to unlink.
- /// - Returns: The user.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func unlink(fromProvider provider: String) async throws -> User {
- return try await userProfileUpdate.unlink(user: self, fromProvider: provider)
- }
- /// Initiates email verification for the user.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was
- /// sent in the request.
- /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in
- /// the console for this action.
- /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for
- /// sending update email.
- /// * `AuthErrorCodeUserNotFound` - Indicates the user account was not found.
- /// - Parameter completion: Optionally; the block invoked when the request to send an email
- /// verification is complete, or fails. Invoked asynchronously on the main thread in the future.
- @objc(sendEmailVerificationWithCompletion:)
- open func __sendEmailVerification(withCompletion completion: ((Error?) -> Void)?) {
- sendEmailVerification(completion: completion)
- }
- /// Initiates email verification for the user.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was
- /// sent in the request.
- /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in
- /// the console for this action.
- /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for
- /// sending update email.
- /// * `AuthErrorCodeUserNotFound` - Indicates the user account was not found.
- /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to
- /// handling action codes.
- /// - Parameter completion: Optionally; the block invoked when the request to send an email
- /// verification is complete, or fails. Invoked asynchronously on the main thread in the future.
- @objc(sendEmailVerificationWithActionCodeSettings:completion:)
- open func sendEmailVerification(with actionCodeSettings: ActionCodeSettings? = nil,
- completion: ((Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- self.internalGetToken(backend: self.backend) { accessToken, error in
- if let error {
- User.callInMainThreadWithError(callback: completion, error: error)
- return
- }
- guard let accessToken else {
- fatalError("Internal Error: Both error and accessToken are nil.")
- }
- guard let requestConfiguration = self.auth?.requestConfiguration else {
- fatalError("Internal Error: Unexpected nil requestConfiguration.")
- }
- let request = GetOOBConfirmationCodeRequest.verifyEmailRequest(
- accessToken: accessToken,
- actionCodeSettings: actionCodeSettings,
- requestConfiguration: requestConfiguration
- )
- Task {
- do {
- let _ = try await self.backend.call(with: request)
- User.callInMainThreadWithError(callback: completion, error: nil)
- } catch {
- self.signOutIfTokenIsInvalid(withError: error)
- User.callInMainThreadWithError(callback: completion, error: error)
- }
- }
- }
- }
- }
- /// Initiates email verification for the user.
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was
- /// sent in the request.
- /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in
- /// the console for this action.
- /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for
- /// sending update email.
- /// * `AuthErrorCodeUserNotFound` - Indicates the user account was not found.
- /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to
- /// handling action codes. The default value is `nil`.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func sendEmailVerification(with actionCodeSettings: ActionCodeSettings? = nil) async throws {
- return try await withCheckedThrowingContinuation { continuation in
- self.sendEmailVerification(with: actionCodeSettings) { error in
- if let error {
- continuation.resume(throwing: error)
- } else {
- continuation.resume()
- }
- }
- }
- }
- /// Deletes the user account (also signs out the user, if this was the current user).
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive
- /// operation that requires a recent login from the user. This error indicates the user
- /// has not signed in recently enough. To resolve, reauthenticate the user by calling
- /// `reauthenticate(with:)`.
- /// - Parameter completion: Optionally; the block invoked when the request to delete the account
- /// is complete, or fails. Invoked asynchronously on the main thread in the future.
- @objc open func delete(completion: ((Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- self.internalGetToken(backend: self.backend) { accessToken, error in
- if let error {
- User.callInMainThreadWithError(callback: completion, error: error)
- return
- }
- guard let accessToken else {
- fatalError("Auth Internal Error: Both error and accessToken are nil.")
- }
- guard let requestConfiguration = self.auth?.requestConfiguration else {
- fatalError("Auth Internal Error: Unexpected nil requestConfiguration.")
- }
- let request = DeleteAccountRequest(localID: self.uid, accessToken: accessToken,
- requestConfiguration: requestConfiguration)
- Task {
- do {
- let _ = try await self.backend.call(with: request)
- try self.auth?.signOutByForce(withUserID: self.uid)
- User.callInMainThreadWithError(callback: completion, error: nil)
- } catch {
- User.callInMainThreadWithError(callback: completion, error: error)
- }
- }
- }
- }
- }
- /// Deletes the user account (also signs out the user, if this was the current user).
- ///
- /// Possible error codes:
- /// * `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive
- /// operation that requires a recent login from the user. This error indicates the user
- /// has not signed in recently enough. To resolve, reauthenticate the user by calling
- /// `reauthenticate(with:)`.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func delete() async throws {
- return try await withCheckedThrowingContinuation { continuation in
- self.delete { error in
- if let error {
- continuation.resume(throwing: error)
- } else {
- continuation.resume()
- }
- }
- }
- }
- /// Send an email to verify the ownership of the account then update to the new email.
- /// - Parameter email: The email to be updated to.
- /// - Parameter completion: Optionally; the block invoked when the request to send the
- /// verification email is complete, or fails.
- @objc(sendEmailVerificationBeforeUpdatingEmail:completion:)
- open func __sendEmailVerificationBeforeUpdating(email: String, completion: ((Error?) -> Void)?) {
- sendEmailVerification(beforeUpdatingEmail: email, completion: completion)
- }
- /// Send an email to verify the ownership of the account then update to the new email.
- /// - Parameter email: The email to be updated to.
- /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to
- /// handling action codes.
- /// - Parameter completion: Optionally; the block invoked when the request to send the
- /// verification email is complete, or fails.
- @objc open func sendEmailVerification(beforeUpdatingEmail email: String,
- actionCodeSettings: ActionCodeSettings? = nil,
- completion: ((Error?) -> Void)? = nil) {
- kAuthGlobalWorkQueue.async {
- self.internalGetToken(backend: self.backend) { accessToken, error in
- if let error {
- User.callInMainThreadWithError(callback: completion, error: error)
- return
- }
- guard let accessToken else {
- fatalError("Internal Error: Both error and accessToken are nil.")
- }
- guard let requestConfiguration = self.auth?.requestConfiguration else {
- fatalError("Internal Error: Unexpected nil requestConfiguration.")
- }
- let request = GetOOBConfirmationCodeRequest.verifyBeforeUpdateEmail(
- accessToken: accessToken,
- newEmail: email,
- actionCodeSettings: actionCodeSettings,
- requestConfiguration: requestConfiguration
- )
- Task {
- do {
- let _ = try await self.backend.call(with: request)
- User.callInMainThreadWithError(callback: completion, error: nil)
- } catch {
- User.callInMainThreadWithError(callback: completion, error: error)
- }
- }
- }
- }
- }
- /// Send an email to verify the ownership of the account then update to the new email.
- /// - Parameter newEmail: The email to be updated to.
- /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to
- /// handling action codes.
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- open func sendEmailVerification(beforeUpdatingEmail newEmail: String,
- actionCodeSettings: ActionCodeSettings? = nil) async throws {
- return try await withCheckedThrowingContinuation { continuation in
- self.sendEmailVerification(beforeUpdatingEmail: newEmail,
- actionCodeSettings: actionCodeSettings) { error in
- if let error {
- continuation.resume(throwing: error)
- } else {
- continuation.resume()
- }
- }
- }
- }
- // MARK: Internal implementations below
- func rawAccessToken() -> String {
- return tokenService.accessToken
- }
- func accessTokenExpirationDate() -> Date? {
- return tokenService.accessTokenExpirationDate
- }
- init(withTokenService tokenService: SecureTokenService, backend: AuthBackend) {
- self.backend = backend
- providerDataRaw = [:]
- userProfileUpdate = UserProfileUpdate()
- self.tokenService = tokenService
- isAnonymous = false
- isEmailVerified = false
- metadata = UserMetadata(withCreationDate: nil, lastSignInDate: nil)
- tenantID = nil
- #if os(iOS)
- multiFactor = MultiFactor(withMFAEnrollments: [])
- #endif
- uid = ""
- hasEmailPasswordCredential = false
- requestConfiguration = AuthRequestConfiguration(apiKey: "", appID: "")
- }
- class func retrieveUser(withAuth auth: Auth,
- accessToken: String?,
- accessTokenExpirationDate: Date?,
- refreshToken: String?,
- anonymous: Bool) async throws -> User {
- guard let accessToken = accessToken,
- let refreshToken = refreshToken else {
- throw AuthErrorUtils
- .invalidUserTokenError(message: "Invalid user token: accessToken or refreshToken is nil")
- }
- let tokenService = SecureTokenService(withRequestConfiguration: auth.requestConfiguration,
- accessToken: accessToken,
- accessTokenExpirationDate: accessTokenExpirationDate,
- refreshToken: refreshToken)
- let user = User(withTokenService: tokenService, backend: auth.backend)
- user.auth = auth
- user.tenantID = auth.tenantID
- user.requestConfiguration = auth.requestConfiguration
- let accessToken2 = try await user.internalGetTokenAsync(backend: user.backend)
- let getAccountInfoRequest = GetAccountInfoRequest(
- accessToken: accessToken2,
- requestConfiguration: user.requestConfiguration
- )
- let response = try await auth.backend.call(with: getAccountInfoRequest)
- user.isAnonymous = anonymous
- user.update(withGetAccountInfoResponse: response)
- return user
- }
- @objc open var providerID: String {
- return "Firebase"
- }
- /// The provider's user ID for the user.
- @objc open var uid: String
- /// The name of the user.
- @objc open var displayName: String?
- /// The URL of the user's profile photo.
- @objc open var photoURL: URL?
- /// The user's email address.
- @objc open var email: String?
- /// A phone number associated with the user.
- ///
- /// This property is only available for users authenticated via phone number auth.
- @objc open var phoneNumber: String?
- /// Whether or not the user can be authenticated by using Firebase email and password.
- var hasEmailPasswordCredential: Bool
- /// Used to serialize the update profile calls.
- private let userProfileUpdate: UserProfileUpdate
- /// A strong reference to a requestConfiguration instance associated with this user instance.
- var requestConfiguration: AuthRequestConfiguration
- /// A secure token service associated with this user. For performing token exchanges and
- /// refreshing access tokens.
- var tokenService: SecureTokenService
- private weak var _auth: Auth?
- /// A weak reference to an `Auth` instance associated with this instance.
- weak var auth: Auth? {
- set {
- guard let newValue else {
- fatalError("Firebase Auth Internal Error: Set user's auth property with non-nil instance.")
- }
- _auth = newValue
- requestConfiguration = newValue.requestConfiguration
- tokenService.requestConfiguration = requestConfiguration
- backend = newValue.backend
- }
- get { return _auth }
- }
- // MARK: Private functions
- private func updateEmail(email: String?,
- password: String?,
- callback: @escaping (Error?) -> Void) {
- let hadEmailPasswordCredential = hasEmailPasswordCredential
- executeUserUpdateWithChanges(changeBlock: { user, request in
- if let email {
- request.email = email
- }
- if let password {
- request.password = password
- }
- }) { error in
- if let error {
- callback(error)
- return
- }
- if let email {
- self.email = email
- }
- if self.email != nil {
- if !hadEmailPasswordCredential {
- // The list of providers need to be updated for the newly added email-password provider.
- Task {
- do {
- let accessToken = try await self.internalGetTokenAsync(backend: self.backend)
- if let requestConfiguration = self.auth?.requestConfiguration {
- let getAccountInfoRequest = GetAccountInfoRequest(accessToken: accessToken,
- requestConfiguration: requestConfiguration)
- do {
- let accountInfoResponse = try await self.backend.call(with: getAccountInfoRequest)
- if let users = accountInfoResponse.users {
- for userAccountInfo in users {
- // Set the account to non-anonymous if there are any providers, even if
- // they're not email/password ones.
- if let providerUsers = userAccountInfo.providerUserInfo {
- if providerUsers.count > 0 {
- self.isAnonymous = false
- for providerUserInfo in providerUsers {
- if providerUserInfo.providerID == EmailAuthProvider.id {
- self.hasEmailPasswordCredential = true
- break
- }
- }
- }
- }
- }
- }
- self.update(withGetAccountInfoResponse: accountInfoResponse)
- if let error = self.updateKeychain() {
- callback(error)
- return
- }
- callback(nil)
- } catch {
- self.signOutIfTokenIsInvalid(withError: error)
- callback(error)
- }
- }
- } catch {
- callback(error)
- }
- }
- return
- }
- }
- if let error = self.updateKeychain() {
- callback(error)
- return
- }
- callback(nil)
- }
- }
- /// 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`
- /// - Parameter callback: A block to invoke when the change is complete. Invoked asynchronously on
- /// the auth global work queue in the future.
- func executeUserUpdateWithChanges(changeBlock: @escaping (GetAccountInfoResponse.User,
- SetAccountInfoRequest) -> Void,
- callback: @escaping (Error?) -> Void) {
- Task {
- do {
- try await userProfileUpdate.executeUserUpdateWithChanges(user: self,
- changeBlock: changeBlock)
- await MainActor.run {
- callback(nil)
- }
- } catch {
- await MainActor.run {
- callback(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(callback: @escaping (GetAccountInfoResponse.User?,
- Error?) -> Void) {
- Task {
- do {
- let responseUser = try await userProfileUpdate.getAccountInfoRefreshingCache(self)
- await MainActor.run {
- callback(responseUser, nil)
- }
- } catch {
- await MainActor.run {
- callback(nil, error)
- }
- }
- }
- }
- func update(withGetAccountInfoResponse response: GetAccountInfoResponse) {
- guard let user = response.users?.first else {
- // Silent fallthrough in ObjC code.
- AuthLog.logWarning(code: "I-AUT000016", message: "Missing user in GetAccountInfoResponse")
- return
- }
- uid = user.localID ?? ""
- email = user.email
- isEmailVerified = user.emailVerified
- displayName = user.displayName
- photoURL = user.photoURL
- phoneNumber = user.phoneNumber
- hasEmailPasswordCredential = user.passwordHash != nil && user.passwordHash!.count > 0
- metadata = UserMetadata(withCreationDate: user.creationDate,
- lastSignInDate: user.lastLoginDate)
- var providerData: [String: UserInfoImpl] = [:]
- if let providerUserInfos = user.providerUserInfo {
- for providerUserInfo in providerUserInfos {
- let userInfo = UserInfoImpl.userInfo(
- withGetAccountInfoResponseProviderUserInfo: providerUserInfo
- )
- if let providerID = providerUserInfo.providerID {
- providerData[providerID] = userInfo
- }
- }
- }
- providerDataRaw = providerData
- #if os(iOS)
- if let enrollments = user.mfaEnrollments {
- multiFactor = MultiFactor(withMFAEnrollments: enrollments)
- }
- multiFactor.user = self
- #endif
- }
- #if os(iOS)
- /// Updates the phone number for the user. On success, the cached user profile data is updated.
- ///
- /// Invoked asynchronously on the global work queue in the future.
- /// - Parameter credential: The new phone number credential corresponding to the phone
- /// number to be added to the Firebase account. If a phone number is already linked to the
- /// account, this new phone number will replace it.
- /// - Parameter isLinkOperation: Boolean value indicating whether or not this is a link
- /// operation.
- /// - Parameter completion: Optionally; the block invoked when the user profile change has
- /// finished.
- private func internalUpdateOrLinkPhoneNumber(credential: PhoneAuthCredential,
- isLinkOperation: Bool,
- completion: @escaping (Error?) -> Void) {
- internalGetToken(backend: backend) { accessToken, error in
- if let error {
- completion(error)
- return
- }
- guard let accessToken = accessToken else {
- fatalError("Auth Internal Error: Both accessToken and error are nil")
- }
- guard let configuration = self.auth?.requestConfiguration else {
- fatalError("Auth Internal Error: nil value for VerifyPhoneNumberRequest initializer")
- }
- switch credential.credentialKind {
- case .phoneNumber: fatalError("Internal Error: Missing verificationCode")
- case let .verification(verificationID, code):
- let operation = isLinkOperation ? AuthOperationType.link : AuthOperationType.update
- let request = VerifyPhoneNumberRequest(verificationID: verificationID,
- verificationCode: code,
- operation: operation,
- requestConfiguration: configuration)
- request.accessToken = accessToken
- Task {
- do {
- let verifyResponse = try await self.backend.call(with: request)
- guard let idToken = verifyResponse.idToken,
- let refreshToken = verifyResponse.refreshToken else {
- fatalError("Internal Auth Error: missing token in internalUpdateOrLinkPhoneNumber")
- }
- // Update the new token and refresh user info again.
- self.tokenService = SecureTokenService(
- withRequestConfiguration: configuration,
- accessToken: idToken,
- accessTokenExpirationDate: verifyResponse.approximateExpirationDate,
- refreshToken: refreshToken
- )
- // Get account info to update cached user info.
- self.getAccountInfoRefreshingCache { user, error in
- if let error {
- self.signOutIfTokenIsInvalid(withError: error)
- completion(error)
- return
- }
- self.isAnonymous = false
- if let error = self.updateKeychain() {
- completion(error)
- return
- }
- completion(nil)
- }
- } catch {
- self.signOutIfTokenIsInvalid(withError: error)
- completion(error)
- }
- }
- }
- }
- }
- #endif
- private func link(withEmail email: String,
- password: String,
- authResult: AuthDataResult,
- _ completion: ((AuthDataResult?, Error?) -> Void)?) {
- internalGetToken(backend: backend) { accessToken, error in
- guard let requestConfiguration = self.auth?.requestConfiguration else {
- fatalError("Internal auth error: missing auth on User")
- }
- let request = SignUpNewUserRequest(email: email,
- password: password,
- displayName: nil,
- idToken: accessToken,
- requestConfiguration: requestConfiguration)
- Task {
- do {
- #if os(iOS)
- guard let auth = self.auth else {
- fatalError("Internal Auth error: missing auth instance on user")
- }
- let response = try await auth.injectRecaptcha(request: request,
- action: AuthRecaptchaAction
- .signUpPassword)
- #else
- let response = try await self.backend.call(with: request)
- #endif
- guard let refreshToken = response.refreshToken,
- let idToken = response.idToken else {
- fatalError("Internal auth error: Invalid SignUpNewUserResponse")
- }
- // Update the new token and refresh user info again.
- try await self.updateTokenAndRefreshUser(
- idToken: idToken,
- refreshToken: refreshToken,
- expirationDate: response.approximateExpirationDate,
- requestConfiguration: requestConfiguration
- )
- User.callInMainThreadWithAuthDataResultAndError(
- callback: completion,
- result: AuthDataResult(withUser: self, additionalUserInfo: nil),
- error: nil
- )
- } catch {
- self.signOutIfTokenIsInvalid(withError: error)
- User.callInMainThreadWithAuthDataResultAndError(callback: completion,
- result: nil, error: error)
- }
- }
- }
- }
- private func link(withEmailCredential emailCredential: EmailAuthCredential,
- completion: ((AuthDataResult?, Error?) -> Void)?) {
- if hasEmailPasswordCredential {
- User.callInMainThreadWithAuthDataResultAndError(
- callback: completion,
- result: nil,
- error: AuthErrorUtils
- .providerAlreadyLinkedError()
- )
- return
- }
- switch emailCredential.emailType {
- case let .password(password):
- let result = AuthDataResult(withUser: self, additionalUserInfo: nil)
- link(withEmail: emailCredential.email, password: password, authResult: result, completion)
- case let .link(link):
- internalGetToken(backend: backend) { accessToken, error in
- var queryItems = AuthWebUtils.parseURL(link)
- if link.count == 0 {
- if let urlComponents = URLComponents(string: link),
- let query = urlComponents.query {
- queryItems = AuthWebUtils.parseURL(query)
- }
- }
- guard let actionCode = queryItems["oobCode"],
- let requestConfiguration = self.auth?.requestConfiguration else {
- fatalError("Internal Auth Error: Missing oobCode or requestConfiguration")
- }
- let request = EmailLinkSignInRequest(email: emailCredential.email,
- oobCode: actionCode,
- requestConfiguration: requestConfiguration)
- request.idToken = accessToken
- Task {
- do {
- let response = try await self.backend.call(with: request)
- guard let idToken = response.idToken,
- let refreshToken = response.refreshToken else {
- fatalError("Internal Auth Error: missing token in EmailLinkSignInResponse")
- }
- try await self.updateTokenAndRefreshUser(
- idToken: idToken,
- refreshToken: refreshToken,
- expirationDate: response.approximateExpirationDate,
- requestConfiguration: requestConfiguration
- )
- User.callInMainThreadWithAuthDataResultAndError(
- callback: completion,
- result: AuthDataResult(withUser: self, additionalUserInfo: nil),
- error: nil
- )
- } catch {
- User.callInMainThreadWithAuthDataResultAndError(callback: completion,
- result: nil,
- error: error)
- }
- }
- }
- }
- }
- #if !os(watchOS)
- private func link(withGameCenterCredential gameCenterCredential: GameCenterAuthCredential,
- completion: ((AuthDataResult?, Error?) -> Void)?) {
- internalGetToken(backend: backend) { accessToken, error in
- guard let requestConfiguration = self.auth?.requestConfiguration,
- let publicKeyURL = gameCenterCredential.publicKeyURL,
- let signature = gameCenterCredential.signature,
- let salt = gameCenterCredential.salt else {
- fatalError("Internal Auth Error: Nil value field for SignInWithGameCenterRequest")
- }
- let request = SignInWithGameCenterRequest(playerID: gameCenterCredential.playerID,
- teamPlayerID: gameCenterCredential.teamPlayerID,
- gamePlayerID: gameCenterCredential.gamePlayerID,
- publicKeyURL: publicKeyURL,
- signature: signature,
- salt: salt,
- timestamp: gameCenterCredential.timestamp,
- displayName: gameCenterCredential.displayName,
- requestConfiguration: requestConfiguration)
- request.accessToken = accessToken
- Task {
- do {
- let response = try await self.backend.call(with: request)
- guard let idToken = response.idToken,
- let refreshToken = response.refreshToken else {
- fatalError("Internal Auth Error: missing token in link(withGameCredential")
- }
- try await self.updateTokenAndRefreshUser(
- idToken: idToken,
- refreshToken: refreshToken,
- expirationDate: response.approximateExpirationDate,
- requestConfiguration: requestConfiguration
- )
- User.callInMainThreadWithAuthDataResultAndError(
- callback: completion,
- result: AuthDataResult(withUser: self, additionalUserInfo: nil),
- error: nil
- )
- } catch {
- User.callInMainThreadWithAuthDataResultAndError(callback: completion,
- result: nil,
- error: error)
- }
- }
- }
- }
- #endif
- #if os(iOS)
- private func link(withPhoneCredential phoneCredential: PhoneAuthCredential,
- completion: ((AuthDataResult?, Error?) -> Void)?) {
- internalUpdateOrLinkPhoneNumber(credential: phoneCredential,
- isLinkOperation: true) { error in
- if let error {
- User.callInMainThreadWithAuthDataResultAndError(
- callback: completion,
- result: nil,
- error: error
- )
- } else {
- let result = AuthDataResult(withUser: self, additionalUserInfo: nil)
- User.callInMainThreadWithAuthDataResultAndError(
- callback: completion,
- result: result,
- error: nil
- )
- }
- }
- }
- #endif
- // Update the new token and refresh user info again.
- private func updateTokenAndRefreshUser(idToken: String,
- refreshToken: String,
- expirationDate: Date?,
- requestConfiguration: AuthRequestConfiguration) async throws {
- return try await userProfileUpdate
- .updateTokenAndRefreshUser(
- user: self,
- idToken: idToken,
- refreshToken: refreshToken,
- expirationDate: expirationDate
- )
- }
- /// Signs out this user if the user or the token is invalid.
- /// - Parameter error: The error from the server.
- func signOutIfTokenIsInvalid(withError error: Error) {
- let code = (error as NSError).code
- if code == AuthErrorCode.userNotFound.rawValue ||
- code == AuthErrorCode.userDisabled.rawValue ||
- code == AuthErrorCode.invalidUserToken.rawValue ||
- code == AuthErrorCode.userTokenExpired.rawValue {
- AuthLog.logNotice(code: "I-AUT000016",
- message: "Invalid user token detected, user is automatically signed out.")
- try? auth?.signOutByForce(withUserID: uid)
- }
- }
- /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
- /// - Parameter callback: The block to invoke when the token is available. Invoked asynchronously
- /// on the global work thread in the future.
- func internalGetToken(forceRefresh: Bool = false,
- backend: AuthBackend,
- callback: @escaping (String?, Error?) -> Void,
- callCallbackOnMain: Bool = false) {
- Task {
- do {
- let token = try await internalGetTokenAsync(forceRefresh: forceRefresh, backend: backend)
- if callCallbackOnMain {
- Auth.wrapMainAsync(callback: callback, with: .success(token))
- } else {
- callback(token, nil)
- }
- } catch {
- if callCallbackOnMain {
- Auth.wrapMainAsync(callback: callback, with: .failure(error))
- } else {
- callback(nil, error)
- }
- }
- }
- }
- /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
- /// - Parameter forceRefresh
- func internalGetTokenAsync(forceRefresh: Bool = false,
- backend: AuthBackend) async throws -> String {
- var keychainError = false
- do {
- let (token, tokenUpdated) = try await tokenService.fetchAccessToken(
- forcingRefresh: forceRefresh, backend: backend
- )
- if tokenUpdated {
- if let error = updateKeychain() {
- keychainError = true
- throw error
- }
- }
- return token!
- } catch {
- if !keychainError {
- signOutIfTokenIsInvalid(withError: error)
- }
- throw error
- }
- }
- /// Updates the keychain for user token or info changes.
- /// - Returns: An `Error` on failure.
- func updateKeychain() -> Error? {
- return auth?.updateKeychain(withUser: self)
- }
- /// Calls a callback in main thread with error.
- /// - Parameter callback: The callback to be called in main thread.
- /// - Parameter error: The error to pass to callback.
- class func callInMainThreadWithError(callback: ((Error?) -> Void)?, error: Error?) {
- if let callback {
- DispatchQueue.main.async {
- callback(error)
- }
- }
- }
- /// Calls a callback in main thread with user and error.
- /// - Parameter callback: The callback to be called in main thread.
- /// - Parameter user: The user to pass to callback if there is no error.
- /// - Parameter error: The error to pass to callback.
- private class func callInMainThreadWithUserAndError(callback: ((User?, Error?) -> Void)?,
- user: User,
- error: Error?) {
- if let callback {
- DispatchQueue.main.async {
- callback((error != nil) ? nil : user, error)
- }
- }
- }
- /// Calls a callback in main thread with user and error.
- /// - Parameter callback: The callback to be called in main thread.
- private class func callInMainThreadWithAuthDataResultAndError(callback: (
- (AuthDataResult?, Error?) -> Void
- )?,
- result: AuthDataResult? = nil,
- error: Error? = nil) {
- if let callback {
- DispatchQueue.main.async {
- callback(result, error)
- }
- }
- }
- // MARK: NSSecureCoding
- private let kUserIDCodingKey = "userID"
- private let kHasEmailPasswordCredentialCodingKey = "hasEmailPassword"
- private let kAnonymousCodingKey = "anonymous"
- private let kEmailCodingKey = "email"
- private let kPhoneNumberCodingKey = "phoneNumber"
- private let kEmailVerifiedCodingKey = "emailVerified"
- private let kDisplayNameCodingKey = "displayName"
- private let kPhotoURLCodingKey = "photoURL"
- private let kProviderDataKey = "providerData"
- private let kAPIKeyCodingKey = "APIKey"
- private let kFirebaseAppIDCodingKey = "firebaseAppID"
- private let kTokenServiceCodingKey = "tokenService"
- private let kMetadataCodingKey = "metadata"
- private let kMultiFactorCodingKey = "multiFactor"
- private let kTenantIDCodingKey = "tenantID"
- public static let supportsSecureCoding = true
- public func encode(with coder: NSCoder) {
- coder.encode(uid, forKey: kUserIDCodingKey)
- coder.encode(isAnonymous, forKey: kAnonymousCodingKey)
- coder.encode(hasEmailPasswordCredential, forKey: kHasEmailPasswordCredentialCodingKey)
- coder.encode(providerDataRaw, forKey: kProviderDataKey)
- coder.encode(email, forKey: kEmailCodingKey)
- coder.encode(phoneNumber, forKey: kPhoneNumberCodingKey)
- coder.encode(isEmailVerified, forKey: kEmailVerifiedCodingKey)
- coder.encode(photoURL, forKey: kPhotoURLCodingKey)
- coder.encode(displayName, forKey: kDisplayNameCodingKey)
- coder.encode(metadata, forKey: kMetadataCodingKey)
- coder.encode(tenantID, forKey: kTenantIDCodingKey)
- if let auth {
- coder.encode(auth.requestConfiguration.apiKey, forKey: kAPIKeyCodingKey)
- coder.encode(auth.requestConfiguration.appID, forKey: kFirebaseAppIDCodingKey)
- }
- coder.encode(tokenService, forKey: kTokenServiceCodingKey)
- #if os(iOS)
- coder.encode(multiFactor, forKey: kMultiFactorCodingKey)
- #endif
- }
- public required init?(coder: NSCoder) {
- guard let userID = coder.decodeObject(of: NSString.self, forKey: kUserIDCodingKey) as? String,
- let tokenService = coder.decodeObject(of: SecureTokenService.self,
- forKey: kTokenServiceCodingKey) else {
- return nil
- }
- let anonymous = coder.decodeBool(forKey: kAnonymousCodingKey)
- let hasEmailPasswordCredential = coder.decodeBool(forKey: kHasEmailPasswordCredentialCodingKey)
- let displayName = coder.decodeObject(
- of: NSString.self,
- forKey: kDisplayNameCodingKey
- ) as? String
- let photoURL = coder.decodeObject(of: NSURL.self, forKey: kPhotoURLCodingKey) as? URL
- let email = coder.decodeObject(of: NSString.self, forKey: kEmailCodingKey) as? String
- let phoneNumber = coder.decodeObject(
- of: NSString.self,
- forKey: kPhoneNumberCodingKey
- ) as? String
- let emailVerified = coder.decodeBool(forKey: kEmailVerifiedCodingKey)
- let classes = [NSDictionary.self, NSString.self, UserInfoImpl.self]
- let providerData = coder.decodeObject(of: classes, forKey: kProviderDataKey)
- as? [String: UserInfoImpl]
- let metadata = coder.decodeObject(of: UserMetadata.self, forKey: kMetadataCodingKey)
- let tenantID = coder.decodeObject(of: NSString.self, forKey: kTenantIDCodingKey) as? String
- #if os(iOS)
- let multiFactor = coder.decodeObject(of: MultiFactor.self, forKey: kMultiFactorCodingKey)
- #endif
- self.tokenService = tokenService
- uid = userID
- isAnonymous = anonymous
- self.hasEmailPasswordCredential = hasEmailPasswordCredential
- self.email = email
- isEmailVerified = emailVerified
- self.displayName = displayName
- self.photoURL = photoURL
- providerDataRaw = providerData ?? [:]
- self.phoneNumber = phoneNumber
- self.metadata = metadata ?? UserMetadata(withCreationDate: nil, lastSignInDate: nil)
- self.tenantID = tenantID
- // Note, in practice, the caller will set the `auth` property of this user
- // instance which will as a side-effect overwrite the request configuration.
- // The assignment here is a best-effort placeholder.
- let apiKey = coder.decodeObject(of: NSString.self, forKey: kAPIKeyCodingKey) as? String
- let appID = coder.decodeObject(
- of: NSString.self,
- forKey: kFirebaseAppIDCodingKey
- ) as? String
- requestConfiguration = AuthRequestConfiguration(apiKey: apiKey ?? "", appID: appID ?? "")
- // This property will be overwritten later via the `user.auth` property update. For now, a
- // placeholder is set as the property update should happen right after this initializer.
- backend = AuthBackend(rpcIssuer: AuthBackendRPCIssuer())
- userProfileUpdate = UserProfileUpdate()
- #if os(iOS)
- self.multiFactor = multiFactor ?? MultiFactor()
- super.init()
- multiFactor?.user = self
- #endif
- }
- }
|