MultiFactor.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. // Copyright 2023 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. #if os(iOS)
  16. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  17. extension MultiFactor: NSSecureCoding {}
  18. /// The interface defining the multi factor related properties and operations pertaining to a
  19. /// user.
  20. ///
  21. /// This class is available on iOS only.
  22. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  23. @objc(FIRMultiFactor) open class MultiFactor: NSObject {
  24. @objc open var enrolledFactors: [MultiFactorInfo]
  25. /// Get a session for a second factor enrollment operation.
  26. ///
  27. /// This is used to identify the current user trying to enroll a second factor.
  28. /// - Parameter completion: A block with the session identifier for a second factor enrollment
  29. /// operation.
  30. @objc(getSessionWithCompletion:)
  31. open func getSessionWithCompletion(_ completion: ((sending MultiFactorSession?, Error?)
  32. -> Void)?) {
  33. let session = MultiFactorSession.session(for: user)
  34. if let completion {
  35. completion(session, nil)
  36. }
  37. }
  38. /// Get a session for a second factor enrollment operation.
  39. ///
  40. /// This is used to identify the current user trying to enroll a second factor.
  41. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  42. open func session() async throws -> MultiFactorSession {
  43. return try await withCheckedThrowingContinuation { continuation in
  44. self.getSessionWithCompletion { session, error in
  45. if let session {
  46. continuation.resume(returning: session)
  47. } else {
  48. continuation.resume(throwing: error!)
  49. }
  50. }
  51. }
  52. }
  53. /// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the
  54. /// current user.
  55. /// - Parameter assertion: The `MultiFactorAssertion`.
  56. /// - Parameter displayName: An optional display name associated with the multi factor to
  57. /// enroll.
  58. /// - Parameter completion: The block invoked when the request is complete, or fails.
  59. @objc(enrollWithAssertion:displayName:completion:)
  60. open func enroll(with assertion: MultiFactorAssertion,
  61. displayName: String?,
  62. completion: (@Sendable (Error?) -> Void)?) {
  63. // TODO: Refactor classes so this duplicated code isn't necessary for phone and totp.
  64. guard
  65. assertion.factorID == PhoneMultiFactorInfo.TOTPMultiFactorID ||
  66. assertion.factorID == PhoneMultiFactorInfo.PhoneMultiFactorID
  67. else {
  68. return
  69. }
  70. guard let user, let auth = user.auth else {
  71. fatalError("Internal Auth error: failed to get user enrolling in MultiFactor")
  72. }
  73. let request = Self.enrollmentFinalizationRequest(
  74. with: assertion,
  75. displayName: displayName,
  76. user: user,
  77. auth: auth
  78. )
  79. Task {
  80. do {
  81. let response = try await auth.backend.call(with: request)
  82. let user = try await auth.completeSignIn(withAccessToken: response.idToken,
  83. accessTokenExpirationDate: nil,
  84. refreshToken: response.refreshToken,
  85. anonymous: false)
  86. try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
  87. if let completion {
  88. DispatchQueue.main.async {
  89. completion(nil)
  90. }
  91. }
  92. } catch {
  93. if let completion {
  94. DispatchQueue.main.async {
  95. completion(error)
  96. }
  97. }
  98. }
  99. }
  100. }
  101. private static func enrollmentFinalizationRequest(with assertion: MultiFactorAssertion,
  102. displayName: String?,
  103. user: User,
  104. auth: Auth) -> FinalizeMFAEnrollmentRequest {
  105. var request: FinalizeMFAEnrollmentRequest? = nil
  106. if assertion.factorID == PhoneMultiFactorInfo.TOTPMultiFactorID {
  107. guard let totpAssertion = assertion as? TOTPMultiFactorAssertion else {
  108. fatalError("Auth Internal Error: Failed to find TOTPMultiFactorAssertion")
  109. }
  110. switch totpAssertion.secretOrID {
  111. case .enrollmentID: fatalError("Missing secret in totpAssertion")
  112. case let .secret(secret):
  113. let finalizeMFATOTPRequestInfo =
  114. AuthProtoFinalizeMFATOTPEnrollmentRequestInfo(sessionInfo: secret.sessionInfo,
  115. verificationCode: totpAssertion
  116. .oneTimePassword)
  117. request = FinalizeMFAEnrollmentRequest(idToken: user.rawAccessToken(),
  118. displayName: displayName,
  119. totpVerificationInfo: finalizeMFATOTPRequestInfo,
  120. requestConfiguration: user
  121. .requestConfiguration)
  122. }
  123. } else if assertion.factorID == PhoneMultiFactorInfo.PhoneMultiFactorID {
  124. let phoneAssertion = assertion as? PhoneMultiFactorAssertion
  125. guard let credential = phoneAssertion?.authCredential else {
  126. fatalError("Internal Error: Missing credential")
  127. }
  128. switch credential.credentialKind {
  129. case .phoneNumber: fatalError("Internal Error: Missing verificationCode")
  130. case let .verification(verificationID, code):
  131. let finalizeMFAPhoneRequestInfo =
  132. AuthProtoFinalizeMFAPhoneRequestInfo(
  133. sessionInfo: verificationID,
  134. verificationCode: code
  135. )
  136. request = FinalizeMFAEnrollmentRequest(
  137. idToken: user.rawAccessToken(),
  138. displayName: displayName,
  139. phoneVerificationInfo: finalizeMFAPhoneRequestInfo,
  140. requestConfiguration: user.requestConfiguration
  141. )
  142. }
  143. }
  144. guard let request else {
  145. // Assertion is not a phone assertion or TOTP assertion.
  146. fatalError("Internal Error: Unsupported assertion with factor ID: \(assertion.factorID).")
  147. }
  148. return request
  149. }
  150. /// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the
  151. /// current user.
  152. /// - Parameter assertion: The `MultiFactorAssertion`.
  153. /// - Parameter displayName: An optional display name associated with the multi factor to
  154. /// enroll.
  155. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  156. open func enroll(with assertion: MultiFactorAssertion, displayName: String?) async throws {
  157. return try await withCheckedThrowingContinuation { continuation in
  158. self.enroll(with: assertion, displayName: displayName) { error in
  159. if let error {
  160. continuation.resume(throwing: error)
  161. } else {
  162. continuation.resume()
  163. }
  164. }
  165. }
  166. }
  167. /// Unenroll the given multi factor.
  168. /// - Parameter factorInfo: The second factor instance to unenroll.
  169. /// - Parameter completion: The block invoked when the request to send the verification email is
  170. /// complete, or fails.
  171. @objc(unenrollWithInfo:completion:)
  172. open func unenroll(with factorInfo: MultiFactorInfo,
  173. completion: (@Sendable (Error?) -> Void)?) {
  174. unenroll(withFactorUID: factorInfo.uid, completion: completion)
  175. }
  176. /// Unenroll the given multi factor.
  177. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  178. open func unenroll(with factorInfo: MultiFactorInfo) async throws {
  179. try await unenroll(withFactorUID: factorInfo.uid)
  180. }
  181. /// Unenroll the given multi factor.
  182. /// - Parameter factorUID: The unique identifier corresponding to the
  183. /// second factor being unenrolled.
  184. /// - Parameter completion: The block invoked when the request to send the verification email is
  185. /// complete, or fails.
  186. @objc(unenrollWithFactorUID:completion:)
  187. open func unenroll(withFactorUID factorUID: String,
  188. completion: (@Sendable (Error?) -> Void)?) {
  189. guard let user = user, let auth = user.auth else {
  190. fatalError("Internal Auth error: failed to get user unenrolling in MultiFactor")
  191. }
  192. let request = WithdrawMFARequest(idToken: user.rawAccessToken(),
  193. mfaEnrollmentID: factorUID,
  194. requestConfiguration: user.requestConfiguration)
  195. Task {
  196. do {
  197. let response = try await auth.backend.call(with: request)
  198. do {
  199. let user = try await auth.completeSignIn(withAccessToken: response.idToken,
  200. accessTokenExpirationDate: nil,
  201. refreshToken: response.refreshToken,
  202. anonymous: false)
  203. try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
  204. if let completion {
  205. DispatchQueue.main.async {
  206. completion(nil)
  207. }
  208. }
  209. } catch {
  210. DispatchQueue.main.async {
  211. try? auth.signOut()
  212. if let completion {
  213. completion(error)
  214. }
  215. }
  216. }
  217. } catch {
  218. if let completion {
  219. completion(error)
  220. }
  221. }
  222. }
  223. }
  224. /// Unenroll the given multi factor.
  225. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  226. open func unenroll(withFactorUID factorUID: String) async throws {
  227. return try await withCheckedThrowingContinuation { continuation in
  228. self.unenroll(withFactorUID: factorUID) { error in
  229. if let error {
  230. continuation.resume(throwing: error)
  231. } else {
  232. continuation.resume()
  233. }
  234. }
  235. }
  236. }
  237. weak var user: User?
  238. convenience init(withMFAEnrollments mfaEnrollments: [AuthProtoMFAEnrollment]) {
  239. self.init()
  240. var multiFactorInfoArray: [MultiFactorInfo] = []
  241. for enrollment in mfaEnrollments {
  242. if enrollment.phoneInfo != nil {
  243. let multiFactorInfo = PhoneMultiFactorInfo(proto: enrollment)
  244. multiFactorInfoArray.append(multiFactorInfo)
  245. } else if enrollment.totpInfo != nil {
  246. let multiFactorInfo = TOTPMultiFactorInfo(proto: enrollment)
  247. multiFactorInfoArray.append(multiFactorInfo)
  248. }
  249. }
  250. enrolledFactors = multiFactorInfoArray
  251. }
  252. override init() {
  253. enrolledFactors = []
  254. }
  255. // MARK: - NSSecureCoding
  256. private let kEnrolledFactorsCodingKey = "enrolledFactors"
  257. public static let supportsSecureCoding = true
  258. public func encode(with coder: NSCoder) {
  259. coder.encode(enrolledFactors, forKey: kEnrolledFactorsCodingKey)
  260. // Do not encode `user` weak property.
  261. }
  262. public required init?(coder: NSCoder) {
  263. let classes = [NSArray.self, MultiFactorInfo.self, PhoneMultiFactorInfo.self,
  264. TOTPMultiFactorInfo.self]
  265. let enrolledFactors = coder
  266. .decodeObject(of: classes, forKey: kEnrolledFactorsCodingKey) as? [MultiFactorInfo]
  267. self.enrolledFactors = enrolledFactors ?? []
  268. // Do not decode `user` weak property.
  269. }
  270. }
  271. #endif