MultiFactor.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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: ((MultiFactorSession?, Error?) -> Void)?) {
  32. let session = MultiFactorSession.session(for: user)
  33. if let completion {
  34. completion(session, nil)
  35. }
  36. }
  37. /// Get a session for a second factor enrollment operation.
  38. ///
  39. /// This is used to identify the current user trying to enroll a second factor.
  40. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  41. open func session() async throws -> MultiFactorSession {
  42. return try await withCheckedThrowingContinuation { continuation in
  43. self.getSessionWithCompletion { session, error in
  44. if let session {
  45. continuation.resume(returning: session)
  46. } else {
  47. continuation.resume(throwing: error!)
  48. }
  49. }
  50. }
  51. }
  52. /// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the
  53. /// current user.
  54. /// - Parameter assertion: The `MultiFactorAssertion`.
  55. /// - Parameter displayName: An optional display name associated with the multi factor to
  56. /// enroll.
  57. /// - Parameter completion: The block invoked when the request is complete, or fails.
  58. @objc(enrollWithAssertion:displayName:completion:)
  59. open func enroll(with assertion: MultiFactorAssertion,
  60. displayName: String?,
  61. completion: ((Error?) -> Void)?) {
  62. // TODO: Refactor classes so this duplicated code isn't necessary for phone and totp.
  63. if assertion.factorID == PhoneMultiFactorInfo.TOTPMultiFactorID {
  64. guard let totpAssertion = assertion as? TOTPMultiFactorAssertion else {
  65. fatalError("Auth Internal Error: Failed to find TOTPMultiFactorAssertion")
  66. }
  67. switch totpAssertion.secretOrID {
  68. case .enrollmentID: fatalError("Missing secret in totpAssertion")
  69. case let .secret(secret):
  70. guard let user = user, let auth = user.auth else {
  71. fatalError("Internal Auth error: failed to get user enrolling in MultiFactor")
  72. }
  73. let finalizeMFATOTPRequestInfo =
  74. AuthProtoFinalizeMFATOTPEnrollmentRequestInfo(sessionInfo: secret.sessionInfo,
  75. verificationCode: totpAssertion
  76. .oneTimePassword)
  77. let request = FinalizeMFAEnrollmentRequest(idToken: self.user?.rawAccessToken(),
  78. displayName: displayName,
  79. totpVerificationInfo: finalizeMFATOTPRequestInfo,
  80. requestConfiguration: user
  81. .requestConfiguration)
  82. Task {
  83. do {
  84. let response = try await auth.backend.call(with: request)
  85. do {
  86. let user = try await auth.completeSignIn(withAccessToken: response.idToken,
  87. accessTokenExpirationDate: nil,
  88. refreshToken: response.refreshToken,
  89. anonymous: false)
  90. try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
  91. if let completion {
  92. DispatchQueue.main.async {
  93. completion(nil)
  94. }
  95. }
  96. } catch {
  97. DispatchQueue.main.async {
  98. if let completion {
  99. completion(error)
  100. }
  101. }
  102. }
  103. } catch {
  104. if let completion {
  105. completion(error)
  106. }
  107. }
  108. }
  109. }
  110. return
  111. } else if assertion.factorID != PhoneMultiFactorInfo.PhoneMultiFactorID {
  112. return
  113. }
  114. let phoneAssertion = assertion as? PhoneMultiFactorAssertion
  115. guard let credential = phoneAssertion?.authCredential else {
  116. fatalError("Internal Error: Missing credential")
  117. }
  118. switch credential.credentialKind {
  119. case .phoneNumber: fatalError("Internal Error: Missing verificationCode")
  120. case let .verification(verificationID, code):
  121. let finalizeMFAPhoneRequestInfo =
  122. AuthProtoFinalizeMFAPhoneRequestInfo(sessionInfo: verificationID, verificationCode: code)
  123. guard let user = user, let auth = user.auth else {
  124. fatalError("Internal Auth error: failed to get user enrolling in MultiFactor")
  125. }
  126. let request = FinalizeMFAEnrollmentRequest(
  127. idToken: self.user?.rawAccessToken(),
  128. displayName: displayName,
  129. phoneVerificationInfo: finalizeMFAPhoneRequestInfo,
  130. requestConfiguration: user.requestConfiguration
  131. )
  132. Task {
  133. do {
  134. let response = try await auth.backend.call(with: request)
  135. do {
  136. let user = try await auth.completeSignIn(withAccessToken: response.idToken,
  137. accessTokenExpirationDate: nil,
  138. refreshToken: response.refreshToken,
  139. anonymous: false)
  140. try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
  141. if let completion {
  142. DispatchQueue.main.async {
  143. completion(nil)
  144. }
  145. }
  146. } catch {
  147. DispatchQueue.main.async {
  148. if let completion {
  149. completion(error)
  150. }
  151. }
  152. }
  153. } catch {
  154. if let completion {
  155. completion(error)
  156. }
  157. }
  158. }
  159. }
  160. }
  161. /// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the
  162. /// current user.
  163. /// - Parameter assertion: The `MultiFactorAssertion`.
  164. /// - Parameter displayName: An optional display name associated with the multi factor to
  165. /// enroll.
  166. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  167. open func enroll(with assertion: MultiFactorAssertion, displayName: String?) async throws {
  168. return try await withCheckedThrowingContinuation { continuation in
  169. self.enroll(with: assertion, displayName: displayName) { error in
  170. if let error {
  171. continuation.resume(throwing: error)
  172. } else {
  173. continuation.resume()
  174. }
  175. }
  176. }
  177. }
  178. /// Unenroll the given multi factor.
  179. /// - Parameter factorInfo: The second factor instance to unenroll.
  180. /// - Parameter completion: The block invoked when the request to send the verification email is
  181. /// complete, or fails.
  182. @objc(unenrollWithInfo:completion:)
  183. open func unenroll(with factorInfo: MultiFactorInfo,
  184. completion: ((Error?) -> Void)?) {
  185. unenroll(withFactorUID: factorInfo.uid, completion: completion)
  186. }
  187. /// Unenroll the given multi factor.
  188. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  189. open func unenroll(with factorInfo: MultiFactorInfo) async throws {
  190. try await unenroll(withFactorUID: factorInfo.uid)
  191. }
  192. /// Unenroll the given multi factor.
  193. /// - Parameter factorUID: The unique identifier corresponding to the
  194. /// second factor being unenrolled.
  195. /// - Parameter completion: The block invoked when the request to send the verification email is
  196. /// complete, or fails.
  197. @objc(unenrollWithFactorUID:completion:)
  198. open func unenroll(withFactorUID factorUID: String,
  199. completion: ((Error?) -> Void)?) {
  200. guard let user = user, let auth = user.auth else {
  201. fatalError("Internal Auth error: failed to get user unenrolling in MultiFactor")
  202. }
  203. let request = WithdrawMFARequest(idToken: user.rawAccessToken(),
  204. mfaEnrollmentID: factorUID,
  205. requestConfiguration: user.requestConfiguration)
  206. Task {
  207. do {
  208. let response = try await auth.backend.call(with: request)
  209. do {
  210. let user = try await auth.completeSignIn(withAccessToken: response.idToken,
  211. accessTokenExpirationDate: nil,
  212. refreshToken: response.refreshToken,
  213. anonymous: false)
  214. try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
  215. if let completion {
  216. DispatchQueue.main.async {
  217. completion(nil)
  218. }
  219. }
  220. } catch {
  221. DispatchQueue.main.async {
  222. try? auth.signOut()
  223. if let completion {
  224. completion(error)
  225. }
  226. }
  227. }
  228. } catch {
  229. if let completion {
  230. completion(error)
  231. }
  232. }
  233. }
  234. }
  235. /// Unenroll the given multi factor.
  236. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  237. open func unenroll(withFactorUID factorUID: String) async throws {
  238. return try await withCheckedThrowingContinuation { continuation in
  239. self.unenroll(withFactorUID: factorUID) { error in
  240. if let error {
  241. continuation.resume(throwing: error)
  242. } else {
  243. continuation.resume()
  244. }
  245. }
  246. }
  247. }
  248. weak var user: User?
  249. convenience init(withMFAEnrollments mfaEnrollments: [AuthProtoMFAEnrollment]) {
  250. self.init()
  251. var multiFactorInfoArray: [MultiFactorInfo] = []
  252. for enrollment in mfaEnrollments {
  253. if enrollment.phoneInfo != nil {
  254. let multiFactorInfo = PhoneMultiFactorInfo(proto: enrollment)
  255. multiFactorInfoArray.append(multiFactorInfo)
  256. } else if enrollment.totpInfo != nil {
  257. let multiFactorInfo = TOTPMultiFactorInfo(proto: enrollment)
  258. multiFactorInfoArray.append(multiFactorInfo)
  259. }
  260. }
  261. enrolledFactors = multiFactorInfoArray
  262. }
  263. override init() {
  264. enrolledFactors = []
  265. }
  266. // MARK: - NSSecureCoding
  267. private let kEnrolledFactorsCodingKey = "enrolledFactors"
  268. public static let supportsSecureCoding = true
  269. public func encode(with coder: NSCoder) {
  270. coder.encode(enrolledFactors, forKey: kEnrolledFactorsCodingKey)
  271. // Do not encode `user` weak property.
  272. }
  273. public required init?(coder: NSCoder) {
  274. let classes = [NSArray.self, MultiFactorInfo.self, PhoneMultiFactorInfo.self,
  275. TOTPMultiFactorInfo.self]
  276. let enrolledFactors = coder
  277. .decodeObject(of: classes, forKey: kEnrolledFactorsCodingKey) as? [MultiFactorInfo]
  278. self.enrolledFactors = enrolledFactors ?? []
  279. // Do not decode `user` weak property.
  280. }
  281. }
  282. #endif