Files
firebase-ios-sdk/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.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