mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
290 lines
11 KiB
Swift
290 lines
11 KiB
Swift
// 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
|
|
|
|
#if os(iOS) || os(macOS)
|
|
|
|
extension MultiFactor: NSSecureCoding {}
|
|
|
|
/// The interface defining the multi factor related properties and operations pertaining to a
|
|
/// user.
|
|
///
|
|
/// This class is available on iOS and macOS.
|
|
@objc(FIRMultiFactor) open class MultiFactor: NSObject {
|
|
@objc open var enrolledFactors: [MultiFactorInfo]
|
|
|
|
/// Get a session for a second factor enrollment operation.
|
|
///
|
|
/// This is used to identify the current user trying to enroll a second factor.
|
|
/// - Parameter completion: A block with the session identifier for a second factor enrollment
|
|
/// operation.
|
|
@objc(getSessionWithCompletion:)
|
|
open func getSessionWithCompletion(_ completion: ((MultiFactorSession?, Error?) -> Void)?) {
|
|
let session = MultiFactorSession.session(for: user)
|
|
if let completion {
|
|
completion(session, nil)
|
|
}
|
|
}
|
|
|
|
/// Get a session for a second factor enrollment operation.
|
|
///
|
|
/// This is used to identify the current user trying to enroll a second factor.
|
|
open func session() async throws -> MultiFactorSession {
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.getSessionWithCompletion { session, error in
|
|
if let session {
|
|
continuation.resume(returning: session)
|
|
} else {
|
|
continuation.resume(throwing: error!)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the
|
|
/// current user.
|
|
/// - Parameter assertion: The `MultiFactorAssertion`.
|
|
/// - Parameter displayName: An optional display name associated with the multi factor to
|
|
/// enroll.
|
|
/// - Parameter completion: The block invoked when the request is complete, or fails.
|
|
@objc(enrollWithAssertion:displayName:completion:)
|
|
open func enroll(with assertion: MultiFactorAssertion,
|
|
displayName: String?,
|
|
completion: ((Error?) -> Void)?) {
|
|
// TODO: Refactor classes so this duplicated code isn't necessary for phone and totp.
|
|
|
|
guard
|
|
assertion.factorID == PhoneMultiFactorInfo.TOTPMultiFactorID ||
|
|
assertion.factorID == PhoneMultiFactorInfo.PhoneMultiFactorID
|
|
else {
|
|
return
|
|
}
|
|
|
|
guard let user, let auth = user.auth else {
|
|
fatalError("Internal Auth error: failed to get user enrolling in MultiFactor")
|
|
}
|
|
|
|
let request = Self.enrollmentFinalizationRequest(
|
|
with: assertion,
|
|
displayName: displayName,
|
|
user: user,
|
|
auth: auth
|
|
)
|
|
|
|
Task {
|
|
do {
|
|
let response = try await auth.backend.call(with: request)
|
|
let user = try await auth.completeSignIn(withAccessToken: response.idToken,
|
|
accessTokenExpirationDate: nil,
|
|
refreshToken: response.refreshToken,
|
|
anonymous: false)
|
|
try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
|
|
if let completion {
|
|
DispatchQueue.main.async {
|
|
completion(nil)
|
|
}
|
|
}
|
|
} catch {
|
|
if let completion {
|
|
DispatchQueue.main.async {
|
|
completion(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func enrollmentFinalizationRequest(with assertion: MultiFactorAssertion,
|
|
displayName: String?,
|
|
user: User,
|
|
auth: Auth) -> FinalizeMFAEnrollmentRequest {
|
|
var request: FinalizeMFAEnrollmentRequest? = nil
|
|
if assertion.factorID == PhoneMultiFactorInfo.TOTPMultiFactorID {
|
|
guard let totpAssertion = assertion as? TOTPMultiFactorAssertion else {
|
|
fatalError("Auth Internal Error: Failed to find TOTPMultiFactorAssertion")
|
|
}
|
|
switch totpAssertion.secretOrID {
|
|
case .enrollmentID: fatalError("Missing secret in totpAssertion")
|
|
case let .secret(secret):
|
|
let finalizeMFATOTPRequestInfo =
|
|
AuthProtoFinalizeMFATOTPEnrollmentRequestInfo(sessionInfo: secret.sessionInfo,
|
|
verificationCode: totpAssertion
|
|
.oneTimePassword)
|
|
request = FinalizeMFAEnrollmentRequest(idToken: user.rawAccessToken(),
|
|
displayName: displayName,
|
|
totpVerificationInfo: finalizeMFATOTPRequestInfo,
|
|
requestConfiguration: user
|
|
.requestConfiguration)
|
|
}
|
|
} else if assertion.factorID == PhoneMultiFactorInfo.PhoneMultiFactorID {
|
|
let phoneAssertion = assertion as? PhoneMultiFactorAssertion
|
|
guard let credential = phoneAssertion?.authCredential else {
|
|
fatalError("Internal Error: Missing credential")
|
|
}
|
|
switch credential.credentialKind {
|
|
case .phoneNumber: fatalError("Internal Error: Missing verificationCode")
|
|
case let .verification(verificationID, code):
|
|
let finalizeMFAPhoneRequestInfo =
|
|
AuthProtoFinalizeMFAPhoneRequestInfo(
|
|
sessionInfo: verificationID,
|
|
verificationCode: code
|
|
)
|
|
request = FinalizeMFAEnrollmentRequest(
|
|
idToken: user.rawAccessToken(),
|
|
displayName: displayName,
|
|
phoneVerificationInfo: finalizeMFAPhoneRequestInfo,
|
|
requestConfiguration: user.requestConfiguration
|
|
)
|
|
}
|
|
}
|
|
|
|
guard let request else {
|
|
// Assertion is not a phone assertion or TOTP assertion.
|
|
fatalError("Internal Error: Unsupported assertion with factor ID: \(assertion.factorID).")
|
|
}
|
|
|
|
return request
|
|
}
|
|
|
|
/// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the
|
|
/// current user.
|
|
/// - Parameter assertion: The `MultiFactorAssertion`.
|
|
/// - Parameter displayName: An optional display name associated with the multi factor to
|
|
/// enroll.
|
|
open func enroll(with assertion: MultiFactorAssertion, displayName: String?) async throws {
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.enroll(with: assertion, displayName: displayName) { error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Unenroll the given multi factor.
|
|
/// - Parameter factorInfo: The second factor instance to unenroll.
|
|
/// - Parameter completion: The block invoked when the request to send the verification email is
|
|
/// complete, or fails.
|
|
@objc(unenrollWithInfo:completion:)
|
|
open func unenroll(with factorInfo: MultiFactorInfo,
|
|
completion: ((Error?) -> Void)?) {
|
|
unenroll(withFactorUID: factorInfo.uid, completion: completion)
|
|
}
|
|
|
|
/// Unenroll the given multi factor.
|
|
open func unenroll(with factorInfo: MultiFactorInfo) async throws {
|
|
try await unenroll(withFactorUID: factorInfo.uid)
|
|
}
|
|
|
|
/// Unenroll the given multi factor.
|
|
/// - Parameter factorUID: The unique identifier corresponding to the
|
|
/// second factor being unenrolled.
|
|
/// - Parameter completion: The block invoked when the request to send the verification email is
|
|
/// complete, or fails.
|
|
@objc(unenrollWithFactorUID:completion:)
|
|
open func unenroll(withFactorUID factorUID: String,
|
|
completion: ((Error?) -> Void)?) {
|
|
guard let user = user, let auth = user.auth else {
|
|
fatalError("Internal Auth error: failed to get user unenrolling in MultiFactor")
|
|
}
|
|
let request = WithdrawMFARequest(idToken: user.rawAccessToken(),
|
|
mfaEnrollmentID: factorUID,
|
|
requestConfiguration: user.requestConfiguration)
|
|
Task {
|
|
do {
|
|
let response = try await auth.backend.call(with: request)
|
|
do {
|
|
let user = try await auth.completeSignIn(withAccessToken: response.idToken,
|
|
accessTokenExpirationDate: nil,
|
|
refreshToken: response.refreshToken,
|
|
anonymous: false)
|
|
try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
|
|
if let completion {
|
|
DispatchQueue.main.async {
|
|
completion(nil)
|
|
}
|
|
}
|
|
} catch {
|
|
DispatchQueue.main.async {
|
|
try? auth.signOut()
|
|
if let completion {
|
|
completion(error)
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
if let completion {
|
|
completion(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Unenroll the given multi factor.
|
|
open func unenroll(withFactorUID factorUID: String) async throws {
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.unenroll(withFactorUID: factorUID) { error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
weak var user: User?
|
|
|
|
convenience init(withMFAEnrollments mfaEnrollments: [AuthProtoMFAEnrollment]) {
|
|
self.init()
|
|
enrolledFactors = mfaEnrollments.compactMap { enrollment in
|
|
if enrollment.phoneInfo != nil {
|
|
return PhoneMultiFactorInfo(proto: enrollment)
|
|
} else if enrollment.totpInfo != nil {
|
|
return TOTPMultiFactorInfo(proto: enrollment)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
override init() {
|
|
enrolledFactors = []
|
|
}
|
|
|
|
// MARK: - NSSecureCoding
|
|
|
|
private let kEnrolledFactorsCodingKey = "enrolledFactors"
|
|
|
|
public static let supportsSecureCoding = true
|
|
|
|
public func encode(with coder: NSCoder) {
|
|
coder.encode(enrolledFactors, forKey: kEnrolledFactorsCodingKey)
|
|
// Do not encode `user` weak property.
|
|
}
|
|
|
|
public required init?(coder: NSCoder) {
|
|
let classes = [NSArray.self, MultiFactorInfo.self, PhoneMultiFactorInfo.self,
|
|
TOTPMultiFactorInfo.self]
|
|
let enrolledFactors = coder
|
|
.decodeObject(of: classes, forKey: kEnrolledFactorsCodingKey) as? [MultiFactorInfo]
|
|
self.enrolledFactors = enrolledFactors ?? []
|
|
// Do not decode `user` weak property.
|
|
}
|
|
}
|
|
#endif
|