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
64    #[schema(value_type = HashMap<String, Value>)]
65    SecurityKey(Box<PublicKeyCredential>),
66    BackupCode(String),
67    // Should this just be discoverable?
68    #[schema(value_type = String)]
69    Passkey(Box<PublicKeyCredential>),
70}
71
72impl fmt::Debug for AuthCredential {
73    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
74        match self {
75            AuthCredential::Anonymous => write!(fmt, "Anonymous"),
76            AuthCredential::Password(_) => write!(fmt, "Password(_)"),
77            AuthCredential::Totp(_) => write!(fmt, "TOTP(_)"),
78            AuthCredential::SecurityKey(_) => write!(fmt, "SecurityKey(_)"),
79            AuthCredential::BackupCode(_) => write!(fmt, "BackupCode(_)"),
80            AuthCredential::Passkey(_) => write!(fmt, "Passkey(_)"),
81        }
82    }
83}
84
85/// The mechanisms that may proceed in this authentication
86#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialOrd, Ord, ToSchema)]
87#[serde(rename_all = "lowercase")]
88pub enum AuthMech {
89    Anonymous,
90    Password,
91    PasswordBackupCode,
92    // Now represents TOTP.
93    #[serde(rename = "passwordmfa")]
94    PasswordTotp,
95    PasswordSecurityKey,
96    Passkey,
97    OAuth2Trust,
98}
99
100impl AuthMech {
101    pub fn to_value(&self) -> &'static str {
102        match self {
103            AuthMech::Anonymous => "anonymous",
104            AuthMech::Password => "password",
105            AuthMech::PasswordTotp => "passwordmfa",
106            AuthMech::PasswordBackupCode => "passwordbackupcode",
107            AuthMech::PasswordSecurityKey => "passwordsecuritykey",
108            AuthMech::Passkey => "passkey",
109            AuthMech::OAuth2Trust => "oauth2trust",
110        }
111    }
112}
113
114impl PartialEq for AuthMech {
115    fn eq(&self, other: &Self) -> bool {
116        std::mem::discriminant(self) == std::mem::discriminant(other)
117    }
118}
119
120impl fmt::Display for AuthMech {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        match self {
123            AuthMech::Anonymous => write!(f, "Anonymous (no credentials)"),
124            AuthMech::Password => write!(f, "Password"),
125            AuthMech::PasswordTotp => write!(f, "TOTP and Password"),
126            AuthMech::PasswordBackupCode => write!(f, "Backup Code and Password"),
127            AuthMech::PasswordSecurityKey => write!(f, "Security Key and Password"),
128            AuthMech::Passkey => write!(f, "Passkey"),
129            AuthMech::OAuth2Trust => write!(f, "OAuth2 Trust"),
130        }
131    }
132}
133
134/// The type of session that should be issued to the client.
135#[derive(Debug, Serialize, Deserialize, Copy, Clone, ToSchema)]
136#[serde(rename_all = "lowercase")]
137pub enum AuthIssueSession {
138    /// Issue a bearer token for this client. This is the default.
139    Token,
140    /// Issue a cookie for this client.
141    Cookie,
142}
143
144impl std::fmt::Display for AuthIssueSession {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        match self {
147            AuthIssueSession::Token => write!(f, "Token"),
148            AuthIssueSession::Cookie => write!(f, "Cookie"),
149        }
150    }
151}
152
153/// A request for the next step of an authentication.
154#[derive(Debug, Serialize, Deserialize, ToSchema)]
155pub struct AuthRequest {
156    pub step: AuthStep,
157}
158
159/// A challenge containing the list of allowed authentication types
160/// that can satisfy the next step. These may have inner types with
161/// required context.
162#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
163#[serde(rename_all = "lowercase")]
164pub enum AuthAllowed {
165    Anonymous,
166    BackupCode,
167    Password,
168    Totp,
169
170    #[schema(value_type = HashMap<String, Value>)]
171    SecurityKey(RequestChallengeResponse),
172    #[schema(value_type = HashMap<String, Value>)]
173    Passkey(RequestChallengeResponse),
174}
175
176impl PartialEq for AuthAllowed {
177    fn eq(&self, other: &Self) -> bool {
178        std::mem::discriminant(self) == std::mem::discriminant(other)
179    }
180}
181
182impl From<&AuthAllowed> for u8 {
183    fn from(a: &AuthAllowed) -> u8 {
184        match a {
185            AuthAllowed::Anonymous => 0,
186            AuthAllowed::Password => 1,
187            AuthAllowed::BackupCode => 2,
188            AuthAllowed::Totp => 3,
189            AuthAllowed::Passkey(_) => 4,
190            AuthAllowed::SecurityKey(_) => 5,
191        }
192    }
193}
194
195impl Eq for AuthAllowed {}
196
197impl Ord for AuthAllowed {
198    fn cmp(&self, other: &Self) -> Ordering {
199        let self_ord: u8 = self.into();
200        let other_ord: u8 = other.into();
201        self_ord.cmp(&other_ord)
202    }
203}
204
205impl PartialOrd for AuthAllowed {
206    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
207        Some(self.cmp(other))
208    }
209}
210
211impl fmt::Display for AuthAllowed {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            AuthAllowed::Anonymous => write!(f, "Anonymous (no credentials)"),
215            AuthAllowed::Password => write!(f, "Password"),
216            AuthAllowed::BackupCode => write!(f, "Backup Code"),
217            AuthAllowed::Totp => write!(f, "TOTP"),
218            AuthAllowed::SecurityKey(_) => write!(f, "Security Token"),
219            AuthAllowed::Passkey(_) => write!(f, "Passkey"),
220        }
221    }
222}
223
224#[derive(Debug, Serialize, Deserialize, ToSchema)]
225pub struct AuthResponse {
226    pub sessionid: Uuid,
227    pub state: AuthState,
228}