Skip to main content

authkestra_engine/auth/
mod.rs

1//! # Engine Core
2//!
3//! `authkestra-core` provides the foundational traits and types for the Engine authentication framework.
4//! It defines the core abstractions for identities, authentication flows and providers that are used across the entire ecosystem.
5
6#![warn(missing_docs)]
7
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10
11/// PKCE (Proof Key for Code Exchange) utilities.
12pub mod pkce;
13
14/// Strategy-based authentication.
15pub mod strategy;
16
17/// Errors that can occur during the authentication process.
18pub mod error;
19pub use error::AuthError;
20
21/// A unified identity structure returned by all providers.
22pub mod state;
23pub use state::{AuthResult, Identity, OAuth2State, OAuthToken};
24
25/// Discovery utilities for OAuth2 providers.
26pub mod discovery;
27
28/// Session management traits and types.
29pub mod session;
30pub use session::{Session, SessionConfig, SessionStore};
31
32/// Credential storage traits.
33#[cfg(any(feature = "webauthn", feature = "totp"))]
34pub mod store;
35#[cfg(any(feature = "webauthn", feature = "totp"))]
36pub use store::CredentialStore;
37
38/// WebAuthn authentication method.
39#[cfg(feature = "webauthn")]
40pub mod webauthn;
41
42/// TOTP authentication method.
43#[cfg(feature = "totp")]
44pub mod totp;
45
46/// Trait for starting a WebAuthn authentication ceremony.
47#[cfg(feature = "webauthn")]
48pub trait WebAuthnStarter: Send + Sync {
49    /// Helper to generate an authentication challenge.
50    fn start_authentication(
51        &self,
52        passkeys: &[webauthn_rs::prelude::Passkey],
53    ) -> Result<
54        (
55            webauthn_rs::prelude::RequestChallengeResponse,
56            webauthn_rs::prelude::PasskeyAuthentication,
57        ),
58        AuthError,
59    >;
60}
61
62/// Represents the input for an authentication method.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type", content = "data")]
65pub enum AuthInput {
66    /// Password-based authentication.
67    Password {
68        /// The username or email.
69        identifier: String,
70        /// The secret password.
71        password: String,
72    },
73    /// OAuth2/OIDC authorization code.
74    OAuthCode {
75        /// The authorization code.
76        code: String,
77        /// Optional PKCE verifier.
78        code_verifier: Option<String>,
79    },
80    /// Token-based authentication (e.g., Bearer token).
81    Token(String),
82    /// Custom input for extensible methods.
83    Custom(serde_json::Value),
84    /// MFA Challenge Submission
85    MfaChallenge {
86        /// The temporary MFA JWT token
87        mfa_token: String,
88        /// The specific factor input (e.g., Totp or WebAuthnAuthentication)
89        challenge_input: Box<AuthInput>,
90    },
91    /// WebAuthn Passkeys authentication input
92    #[cfg(feature = "webauthn")]
93    WebAuthnAuthentication {
94        /// The local user ID
95        user_id: String,
96        /// The credential ID of the passkey
97        credential_id: String,
98        /// The client data JSON from the ceremony
99        client_data_json: String,
100        /// The authenticator data from the ceremony
101        authenticator_data: String,
102        /// The signature generated by the key
103        signature: String,
104        /// Optional user handle
105        user_handle: Option<String>,
106        /// Optional authentication state serialized as JSON (injected by the server session, not the client)
107        #[serde(default)]
108        auth_state_json: Option<String>,
109    },
110    /// TOTP validation input
111    #[cfg(feature = "totp")]
112    Totp {
113        /// The local user ID
114        user_id: String,
115        /// The 6-digit TOTP code
116        code: String,
117    },
118}
119
120/// A mechanism used to authenticate a user.
121#[async_trait]
122pub trait AuthMethod: Send + Sync {
123    /// Returns the name of the authentication method.
124    fn name(&self) -> &str;
125
126    /// Optional downcast to WebAuthnStarter
127    #[cfg(feature = "webauthn")]
128    fn as_webauthn_starter(&self) -> Option<&dyn WebAuthnStarter> {
129        None
130    }
131
132    /// Authenticate a user with the given input.
133    async fn authenticate(&self, input: AuthInput) -> Result<Identity, AuthError>;
134
135    /// Check if a given user has enrolled in this authentication method.
136    /// Default implementation returns false.
137    async fn has_enrolled(&self, _user_id: &str) -> Result<bool, AuthError> {
138        Ok(false)
139    }
140
141    /// Indicates whether this method provides multi-factor-equivalent security.
142    /// If true, users logging in with this method will not be prompted for an additional MFA step.
143    /// Default implementation returns false.
144    fn is_mfa_equivalent(&self) -> bool {
145        false
146    }
147}
148
149/// Configuration for an identity provider.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ProviderConfig {
152    /// The unique identifier for the provider.
153    pub id: String,
154    /// The display name of the provider.
155    pub name: String,
156    /// Additional configuration parameters.
157    pub extra: std::collections::HashMap<String, String>,
158}
159
160/// An external identity source (e.g., Google, GitHub).
161/// Providers should contain zero business logic—only configuration and mapping.
162#[async_trait]
163pub trait Provider: Send + Sync {
164    /// Returns the provider configuration.
165    async fn config(&self) -> ProviderConfig;
166}
167
168/// Controls whether a cookie is sent with cross-site requests.
169#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
170pub enum SameSite {
171    /// The cookie is sent with "safe" cross-site requests (e.g., following a link).
172    Lax,
173    /// The cookie is only sent for same-site requests.
174    Strict,
175    /// The cookie is sent with all requests, including cross-site. Requires `Secure`.
176    None,
177}
178
179/// Trait for an OAuth2-compatible provider.
180#[async_trait]
181pub trait OAuthProvider: Provider {
182    /// Get the provider identifier.
183    fn provider_id(&self) -> &str;
184
185    /// Helper to get the authorization URL.
186    fn get_authorization_url(
187        &self,
188        state: &str,
189        scopes: &[&str],
190        code_challenge: Option<&str>,
191        nonce: Option<&str>,
192    ) -> String;
193
194    /// Exchange an authorization code for an Identity.
195    async fn exchange_code_for_identity(
196        &self,
197        code: &str,
198        code_verifier: Option<&str>,
199        nonce: Option<&str>,
200    ) -> Result<(Identity, OAuthToken), AuthError>;
201
202    /// Refresh an access token using a refresh token.
203    async fn refresh_token(&self, _refresh_token: &str) -> Result<OAuthToken, AuthError> {
204        Err(AuthError::Provider(
205            "Token refresh not supported by this provider".into(),
206        ))
207    }
208
209    /// Revoke an access token.
210    async fn revoke_token(&self, _token: &str) -> Result<(), AuthError> {
211        Err(AuthError::Provider(
212            "Token revocation not supported by this provider".into(),
213        ))
214    }
215}
216
217/// Trait for a Credentials-based provider (e.g., Email/Password).
218#[async_trait]
219pub trait CredentialsProvider: Send + Sync {
220    /// The type of credentials accepted by this provider.
221    type Credentials;
222
223    /// Validate credentials and return an Identity.
224    async fn authenticate(&self, creds: Self::Credentials) -> Result<Identity, AuthError>;
225}
226
227/// Trait for mapping a provider identity to a local user.
228#[async_trait]
229pub trait UserMapper: Send + Sync {
230    /// The type of the local user object.
231    type LocalUser: Send + Sync;
232
233    /// Map an identity to a local user.
234    /// This could involve creating a new user or finding an existing one.
235    async fn map_user(&self, identity: &Identity) -> Result<Self::LocalUser, AuthError>;
236}
237
238/// Orchestrates the Authorization Code flow.
239#[async_trait]
240pub trait ErasedOAuthFlow: Send + Sync {
241    /// Get the provider identifier.
242    fn provider_id(&self) -> String;
243    /// Generates the redirect URL and CSRF state.
244    fn initiate_login(
245        &self,
246        scopes: &[&str],
247        pkce_challenge: Option<&str>,
248    ) -> (String, OAuth2State);
249    /// Completes the flow by exchanging the code.
250    async fn finalize_login(
251        &self,
252        code: &str,
253        received_state: &str,
254        expected_state: &OAuth2State,
255    ) -> Result<(Identity, OAuthToken), AuthError>;
256}
257
258#[async_trait]
259impl UserMapper for () {
260    type LocalUser = ();
261    async fn map_user(&self, _identity: &Identity) -> Result<Self::LocalUser, AuthError> {
262        Ok(())
263    }
264}
265
266#[async_trait]
267impl<T: ErasedOAuthFlow + ?Sized> ErasedOAuthFlow for std::sync::Arc<T> {
268    fn provider_id(&self) -> String {
269        (**self).provider_id()
270    }
271
272    fn initiate_login(
273        &self,
274        scopes: &[&str],
275        pkce_challenge: Option<&str>,
276    ) -> (String, OAuth2State) {
277        (**self).initiate_login(scopes, pkce_challenge)
278    }
279
280    async fn finalize_login(
281        &self,
282        code: &str,
283        received_state: &str,
284        expected_state: &OAuth2State,
285    ) -> Result<(Identity, OAuthToken), AuthError> {
286        (**self)
287            .finalize_login(code, received_state, expected_state)
288            .await
289    }
290}
291
292#[async_trait]
293impl<T: ErasedOAuthFlow + ?Sized> ErasedOAuthFlow for Box<T> {
294    fn provider_id(&self) -> String {
295        (**self).provider_id()
296    }
297
298    fn initiate_login(
299        &self,
300        scopes: &[&str],
301        pkce_challenge: Option<&str>,
302    ) -> (String, OAuth2State) {
303        (**self).initiate_login(scopes, pkce_challenge)
304    }
305
306    async fn finalize_login(
307        &self,
308        code: &str,
309        received_state: &str,
310        expected_state: &OAuth2State,
311    ) -> Result<(Identity, OAuthToken), AuthError> {
312        (**self)
313            .finalize_login(code, received_state, expected_state)
314            .await
315    }
316}