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