MultiFactor.swift 12 KB

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