kanidm_proto/v1/
auth.rs

1use serde::{Deserialize, Serialize};
2use std::cmp::Ordering;
3use std::fmt;
4use utoipa::ToSchema;
5use uuid::Uuid;
6
7use webauthn_rs_proto::PublicKeyCredential;
8use webauthn_rs_proto::RequestChallengeResponse;
9
10/// Authentication to Kanidm is a stepped process.
11///
12/// The session is first initialised with the requested username.
13///
14/// In response the list of supported authentication mechanisms is provided.
15///
16/// The user chooses the authentication mechanism to proceed with.
17///
18/// The server responds with a challenge that the user provides a credential
19/// to satisfy. This challenge and response process continues until a credential
20/// fails to validate, an error occurs, or successful authentication is complete.
21#[derive(Debug, Serialize, Deserialize, ToSchema)]
22#[serde(rename_all = "lowercase")]
23pub enum AuthStep {
24    /// Initialise a new authentication session
25    Init(String),
26    /// Initialise a new authentication session with extra flags
27    /// for requesting different types of session tokens or
28    /// immediate access to privileges.
29    Init2 {
30        username: String,
31        issue: AuthIssueSession,
32        #[serde(default)]
33        /// If true, the session will have r/w access.
34        privileged: bool,
35    },
36    /// Request the named authentication mechanism to proceed
37    Begin(AuthMech),
38    /// Provide a credential in response to a challenge
39    Cred(AuthCredential),
40}
41
42/// The response to an AuthStep request.
43#[derive(Debug, Serialize, Deserialize, ToSchema)]
44#[serde(rename_all = "lowercase")]
45pub enum AuthState {
46    /// You need to select how you want to proceed.
47    Choose(Vec<AuthMech>),
48    /// Continue to auth, allowed mechanisms/challenges listed.
49    Continue(Vec<AuthAllowed>),
50    /// Something was bad, your session is terminated and no cookie.
51    Denied(String),
52    /// Everything is good, your bearer token has been issued and is within.
53    Success(String),
54}
55
56/// The credential challenge provided by a user.
57#[derive(Serialize, Deserialize, ToSchema)]
58#[serde(rename_all = "lowercase")]
59pub enum AuthCredential {
60    Anonymous,
61    Password(String),
62    Totp(u32),
63    SecurityKey(Box<PublicKeyCredential>),
64    BackupCode(String),
65    // Should this just be discoverable?
66    Passkey(Box<PublicKeyCredential>),
67}
68
69impl fmt::Debug for AuthCredential {
70    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
71        match self {
72            AuthCredential::Anonymous => write!(fmt, "Anonymous"),
73            AuthCredential::Password(_) => write!(fmt, "Password(_)"),
74            AuthCredential::Totp(_) => write!(fmt, "TOTP(_)"),
75            AuthCredential::SecurityKey(_) => write!(fmt, "SecurityKey(_)"),
76            AuthCredential::BackupCode(_) => write!(fmt, "BackupCode(_)"),
77            AuthCredential::Passkey(_) => write!(fmt, "Passkey(_)"),
78        }
79    }
80}
81
82/// The mechanisms that may proceed in this authentication
83#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialOrd, Ord, ToSchema)]
84#[serde(rename_all = "lowercase")]
85pub enum AuthMech {
86    Anonymous,
87    Password,
88    PasswordBackupCode,
89    // Now represents TOTP.
90    #[serde(rename = "passwordmfa")]
91    PasswordTotp,
92    PasswordSecurityKey,
93    Passkey,
94}
95
96impl AuthMech {
97    pub fn to_value(&self) -> &'static str {
98        match self {
99            AuthMech::Anonymous => "anonymous",
100            AuthMech::Password => "password",
101            AuthMech::PasswordTotp => "passwordmfa",
102            AuthMech::PasswordBackupCode => "passwordbackupcode",
103            AuthMech::PasswordSecurityKey => "passwordsecuritykey",
104            AuthMech::Passkey => "passkey",
105        }
106    }
107}
108
109impl PartialEq for AuthMech {
110    fn eq(&self, other: &Self) -> bool {
111        std::mem::discriminant(self) == std::mem::discriminant(other)
112    }
113}
114
115impl fmt::Display for AuthMech {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        match self {
118            AuthMech::Anonymous => write!(f, "Anonymous (no credentials)"),
119            AuthMech::Password => write!(f, "Password"),
120            AuthMech::PasswordTotp => write!(f, "TOTP and Password"),
121            AuthMech::PasswordBackupCode => write!(f, "Backup Code and Password"),
122            AuthMech::PasswordSecurityKey => write!(f, "Security Key and Password"),
123            AuthMech::Passkey => write!(f, "Passkey"),
124        }
125    }
126}
127
128/// The type of session that should be issued to the client.
129#[derive(Debug, Serialize, Deserialize, Copy, Clone, ToSchema)]
130#[serde(rename_all = "lowercase")]
131pub enum AuthIssueSession {
132    /// Issue a bearer token for this client. This is the default.
133    Token,
134    /// Issue a cookie for this client.
135    Cookie,
136}
137
138/// A request for the next step of an authentication.
139#[derive(Debug, Serialize, Deserialize, ToSchema)]
140pub struct AuthRequest {
141    pub step: AuthStep,
142}
143
144/// A challenge containing the list of allowed authentication types
145/// that can satisfy the next step. These may have inner types with
146/// required context.
147#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
148#[serde(rename_all = "lowercase")]
149pub enum AuthAllowed {
150    Anonymous,
151    BackupCode,
152    Password,
153    Totp,
154    SecurityKey(RequestChallengeResponse),
155    Passkey(RequestChallengeResponse),
156}
157
158impl PartialEq for AuthAllowed {
159    fn eq(&self, other: &Self) -> bool {
160        std::mem::discriminant(self) == std::mem::discriminant(other)
161    }
162}
163
164impl From<&AuthAllowed> for u8 {
165    fn from(a: &AuthAllowed) -> u8 {
166        match a {
167            AuthAllowed::Anonymous => 0,
168            AuthAllowed::Password => 1,
169            AuthAllowed::BackupCode => 2,
170            AuthAllowed::Totp => 3,
171            AuthAllowed::Passkey(_) => 4,
172            AuthAllowed::SecurityKey(_) => 5,
173        }
174    }
175}
176
177impl Eq for AuthAllowed {}
178
179impl Ord for AuthAllowed {
180    fn cmp(&self, other: &Self) -> Ordering {
181        let self_ord: u8 = self.into();
182        let other_ord: u8 = other.into();
183        self_ord.cmp(&other_ord)
184    }
185}
186
187impl PartialOrd for AuthAllowed {
188    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
189        Some(self.cmp(other))
190    }
191}
192
193impl fmt::Display for AuthAllowed {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        match self {
196            AuthAllowed::Anonymous => write!(f, "Anonymous (no credentials)"),
197            AuthAllowed::Password => write!(f, "Password"),
198            AuthAllowed::BackupCode => write!(f, "Backup Code"),
199            AuthAllowed::Totp => write!(f, "TOTP"),
200            AuthAllowed::SecurityKey(_) => write!(f, "Security Token"),
201            AuthAllowed::Passkey(_) => write!(f, "Passkey"),
202        }
203    }
204}
205
206#[derive(Debug, Serialize, Deserialize, ToSchema)]
207pub struct AuthResponse {
208    pub sessionid: Uuid,
209    pub state: AuthState,
210}