Repository navigation
Expand file tree
/
Copy pathAuthMFARecoveryCodes.swift
More file actions
125 lines (115 loc) · 5.13 KB
/
Copy pathAuthMFARecoveryCodes.swift
File metadata and controls
125 lines (115 loc) · 5.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//
// AuthMFARecoveryCodes.swift
//
//
// Created by Ranbir Singh on 23/09/26.
//
import Foundation
/// Single-use backup codes that let a user reach `aal2` when they cannot use any of their other
/// MFA factors, for example after losing an authenticator app.
///
/// Access this namespace via ``AuthMFA/recoveryCodes``.
///
/// A user has one set of recovery codes, and the codes themselves are returned exactly once, when
/// the set is created. Recovery codes can never be a user's only factor, so generate them once
/// another factor is verified.
///
/// > Warning: Recovery codes are an experimental Supabase Auth feature and have to be enabled on
/// > the Auth server before these methods can be used.
///
/// ```swift
/// let generated = try await client.auth.mfa.recoveryCodes.generate(friendlyName: "Backup codes")
/// // Show generated.codes to the user once and ask them to store the codes safely.
///
/// try await client.auth.mfa.recoveryCodes.verify(code: "K4M9-X7QP-2AB8-HT3Z")
/// ```
///
/// ## Topics
///
/// ### Creating and replacing codes
/// - ``generate(friendlyName:)``
/// - ``regenerate()``
///
/// ### Using codes
/// - ``verify(code:)``
///
/// ### Inspecting and removing
/// - ``status()``
/// - ``unenroll()``
public struct AuthMFARecoveryCodes: Sendable {
let dependencies: Dependencies
private var api: SessionAPIClient { dependencies.sessionAPI }
private var authAPI: AuthAPI { dependencies.authAPI }
private var sessionStore: SessionStore { dependencies.sessionStore }
/// Returns how many recovery codes the user's current set holds and how many of them are still
/// unused. Never returns the codes themselves.
///
/// Works at any authenticator assurance level. When
/// ``AuthMFARecoveryCodesStatusResponse/remaining`` reaches zero, prompt the user to call
/// ``regenerate()``.
///
/// - Returns: The enrollment status of the user's recovery codes.
/// - Throws: An error with code `mfa_factor_not_found` when the user has no recovery codes.
public func status() async throws -> AuthMFARecoveryCodesStatusResponse {
try await api.authorizedExecute { authAPI.recoveryCodesStatus(accessToken: $0) }
}
/// Generates the user's set of recovery codes.
///
/// The codes come back in ``AuthMFARecoveryCodesGenerateResponse/codes`` and cannot be retrieved
/// again, so show them to the user and ask them to store the codes safely.
///
/// The session has to be at `aal2` and the user has to already have another verified factor. A
/// user can only hold one set of recovery codes; use ``regenerate()`` to replace an existing set.
///
/// - Parameter friendlyName: The name the factor is listed under in ``AuthMFA/factors()``,
/// unique among the user's factors. The server names it `Recovery codes` when omitted.
/// - Returns: The new set of recovery codes.
/// - Throws: ``AuthError/sessionMissing`` when no user is signed in. Else the error of the
/// refresh or of the request.
public func generate(
friendlyName: String? = nil
) async throws -> AuthMFARecoveryCodesGenerateResponse {
try await api.authorizedExecute {
try authAPI.generateRecoveryCodes(friendlyName: friendlyName, accessToken: $0)
}
}
/// Verifies one of the user's recovery codes and upgrades the current session to `aal2`. Each
/// code works only once.
///
/// On success the stored session is replaced with the upgraded one, the user's other `aal1`
/// sessions are signed out, and ``AuthEvent/mfaChallengeVerified`` is emitted.
///
/// - Parameter code: A recovery code, exactly as the user typed it. The server ignores letter
/// case, whitespace and `-` separators.
/// - Returns: The upgraded session.
/// - Throws: An error with code `mfa_verification_failed` when the code is wrong, already used or
/// missing.
@discardableResult
public func verify(code: String) async throws -> Session {
let response = try await api.authorizedExecute {
try authAPI.verifyRecoveryCode(code, accessToken: $0)
}
try await sessionStore.commit(.signedIn(response, event: .mfaChallengeVerified))
return response
}
/// Replaces the user's recovery codes with a new set.
///
/// Every remaining code from the previous set stops working immediately and any verification
/// lockout is cleared. The factor keeps its id and friendly name. The session has to be at `aal2`.
///
/// - Returns: The new set of recovery codes.
/// - Throws: An error with code `mfa_factor_not_found` when the user has no recovery codes to
/// replace. Use ``generate(friendlyName:)`` instead.
public func regenerate() async throws -> AuthMFARecoveryCodesGenerateResponse {
try await api.authorizedExecute { authAPI.regenerateRecoveryCodes(accessToken: $0) }
}
/// Removes the user's recovery codes factor and every code in it. The session has to be at
/// `aal2`.
///
/// - Returns: The id of the factor that was removed.
/// - Throws: An error with code `mfa_factor_not_found` when the user has no recovery codes.
@discardableResult
public func unenroll() async throws -> AuthMFAUnenrollResponse {
try await api.authorizedExecute { authAPI.unenrollRecoveryCodes(accessToken: $0) }
}
}