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
144/// A request for the next step of an authentication.
145#[derive(Debug, Serialize, Deserialize, ToSchema)]
146pub struct AuthRequest {
147    pub step: AuthStep,
148}
149
150/// A challenge containing the list of allowed authentication types
151/// that can satisfy the next step. These may have inner types with
152/// required context.
153#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
154#[serde(rename_all = "lowercase")]
155pub enum AuthAllowed {
156    Anonymous,
157    BackupCode,
158    Password,
159    Totp,
160
161    #[schema(value_type = HashMap<String, Value>)]
162    SecurityKey(RequestChallengeResponse),
163    #[schema(value_type = HashMap<String, Value>)]
164    Passkey(RequestChallengeResponse),
165}
166
167impl PartialEq for AuthAllowed {
168    fn eq(&self, other: &Self) -> bool {
169        std::mem::discriminant(self) == std::mem::discriminant(other)
170    }
171}
172
173impl From<&AuthAllowed> for u8 {
174    fn from(a: &AuthAllowed) -> u8 {
175        match a {
176            AuthAllowed::Anonymous => 0,
177            AuthAllowed::Password => 1,
178            AuthAllowed::BackupCode => 2,
179            AuthAllowed::Totp => 3,
180            AuthAllowed::Passkey(_) => 4,
181            AuthAllowed::SecurityKey(_) => 5,
182        }
183    }
184}
185
186impl Eq for AuthAllowed {}
187
188impl Ord for AuthAllowed {
189    fn cmp(&self, other: &Self) -> Ordering {
190        let self_ord: u8 = self.into();
191        let other_ord: u8 = other.into();
192        self_ord.cmp(&other_ord)
193    }
194}
195
196impl PartialOrd for AuthAllowed {
197    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
198        Some(self.cmp(other))
199    }
200}
201
202impl fmt::Display for AuthAllowed {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        match self {
205            AuthAllowed::Anonymous => write!(f, "Anonymous (no credentials)"),
206            AuthAllowed::Password => write!(f, "Password"),
207            AuthAllowed::BackupCode => write!(f, "Backup Code"),
208            AuthAllowed::Totp => write!(f, "TOTP"),
209            AuthAllowed::SecurityKey(_) => write!(f, "Security Token"),
210            AuthAllowed::Passkey(_) => write!(f, "Passkey"),
211        }
212    }
213}
214
215#[derive(Debug, Serialize, Deserialize, ToSchema)]
216pub struct AuthResponse {
217    pub sessionid: Uuid,
218    pub state: AuthState,
219}