Skip to main content

authkestra_core/
lib.rs

1//! # Authkestra Core
2//!
3//! `authkestra-core` provides the foundational traits and types for the Authkestra authentication framework.
4//! It defines the core abstractions for identities, sessions, and providers that are used across the entire ecosystem.
5//!
6//! ## Key Components
7//!
8//! - **[`Identity`]**: A unified structure representing a user's identity across different providers.
9//! - **[`SessionStore`]**: A trait for implementing session persistence (e.g., in-memory, Redis, SQL).
10//! - **[`OAuthProvider`]**: A trait for implementing OAuth2 and OpenID Connect providers.
11//! - **[`AuthError`]**: A comprehensive error type for authentication-related issues.
12
13#![warn(missing_docs)]
14
15use async_trait::async_trait;
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19/// PKCE (Proof Key for Code Exchange) utilities.
20pub mod pkce;
21
22/// Controls whether a cookie is sent with cross-site requests.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
24pub enum SameSite {
25    /// The cookie is sent with "safe" cross-site requests (e.g., following a link).
26    Lax,
27    /// The cookie is only sent for same-site requests.
28    Strict,
29    /// The cookie is sent with all requests, including cross-site. Requires `Secure`.
30    None,
31}
32
33/// Configuration for session cookies.
34#[derive(Clone, Debug)]
35pub struct SessionConfig {
36    /// The name of the session cookie.
37    pub cookie_name: String,
38    /// Whether the cookie should only be sent over HTTPS.
39    pub secure: bool,
40    /// Whether the cookie should be inaccessible to client-side scripts.
41    pub http_only: bool,
42    /// The `SameSite` attribute for the cookie.
43    pub same_site: SameSite,
44    /// The path for which the cookie is valid.
45    pub path: String,
46    /// The maximum age of the session.
47    pub max_age: Option<chrono::Duration>,
48}
49
50impl Default for SessionConfig {
51    fn default() -> Self {
52        Self {
53            cookie_name: "authkestra_session".to_string(),
54            secure: true,
55            http_only: true,
56            same_site: SameSite::Lax,
57            path: "/".to_string(),
58            max_age: Some(chrono::Duration::hours(24)),
59        }
60    }
61}
62
63/// Represents an active user session.
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct Session {
66    /// Unique session identifier.
67    pub id: String,
68    /// The identity associated with this session.
69    pub identity: Identity,
70    /// When the session expires.
71    pub expires_at: chrono::DateTime<chrono::Utc>,
72}
73
74/// Trait for implementing session persistence.
75#[async_trait]
76pub trait SessionStore: Send + Sync + 'static {
77    /// Load a session by its ID.
78    async fn load_session(&self, id: &str) -> Result<Option<Session>, AuthError>;
79    /// Save or update a session.
80    async fn save_session(&self, session: &Session) -> Result<(), AuthError>;
81    /// Delete a session by its ID.
82    async fn delete_session(&self, id: &str) -> Result<(), AuthError>;
83}
84
85/// A unified identity structure returned by all providers.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Identity {
88    /// The provider identifier (e.g., "github", "google")
89    pub provider_id: String,
90    /// The unique ID of the user within the provider's system
91    pub external_id: String,
92    /// The user's email address, if available and authorized
93    pub email: Option<String>,
94    /// The user's username or display name, if available
95    pub username: Option<String>,
96    /// Additional provider-specific attributes
97    pub attributes: HashMap<String, String>,
98}
99
100/// Represents the tokens returned by an OAuth2 provider.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct OAuthToken {
103    /// The access token used for API requests
104    pub access_token: String,
105    /// The type of token (usually "Bearer")
106    pub token_type: String,
107    /// Seconds until the access token expires
108    pub expires_in: Option<u64>,
109    /// The refresh token used to obtain new access tokens
110    pub refresh_token: Option<String>,
111    /// The scopes granted by the user
112    pub scope: Option<String>,
113    /// The OIDC ID Token
114    pub id_token: Option<String>,
115}
116
117/// Errors that can occur during the authentication process.
118#[derive(Debug, thiserror::Error)]
119pub enum AuthError {
120    /// An error returned by the authentication provider
121    #[error("Provider error: {0}")]
122    Provider(String),
123    /// The provided credentials (email/password) are invalid
124    #[error("Invalid credentials")]
125    InvalidCredentials,
126    /// The authorization code is invalid or expired
127    #[error("Invalid code")]
128    InvalidCode,
129    /// A network error occurred during communication with the provider
130    #[error("Network error")]
131    Network,
132    /// An error occurred during session management
133    #[error("Session error: {0}")]
134    Session(String),
135    /// An error occurred during token processing
136    #[error("Token error: {0}")]
137    Token(String),
138    /// The CSRF state parameter does not match the expected value
139    #[error("CSRF state mismatch")]
140    CsrfMismatch,
141}
142
143/// Trait for an OAuth2-compatible provider.
144#[async_trait]
145pub trait OAuthProvider: Send + Sync {
146    /// Get the provider identifier.
147    fn provider_id(&self) -> &str;
148
149    /// Helper to get the authorization URL.
150    fn get_authorization_url(
151        &self,
152        state: &str,
153        scopes: &[&str],
154        code_challenge: Option<&str>,
155    ) -> String;
156
157    /// Exchange an authorization code for an Identity.
158    async fn exchange_code_for_identity(
159        &self,
160        code: &str,
161        code_verifier: Option<&str>,
162    ) -> Result<(Identity, OAuthToken), AuthError>;
163
164    /// Refresh an access token using a refresh token.
165    async fn refresh_token(&self, _refresh_token: &str) -> Result<OAuthToken, AuthError> {
166        Err(AuthError::Provider(
167            "Token refresh not supported by this provider".into(),
168        ))
169    }
170
171    /// Revoke an access token.
172    async fn revoke_token(&self, _token: &str) -> Result<(), AuthError> {
173        Err(AuthError::Provider(
174            "Token revocation not supported by this provider".into(),
175        ))
176    }
177}
178
179/// Trait for a Credentials-based provider (e.g., Email/Password).
180#[async_trait]
181pub trait CredentialsProvider: Send + Sync {
182    /// The type of credentials accepted by this provider.
183    type Credentials;
184
185    /// Validate credentials and return an Identity.
186    async fn authenticate(&self, creds: Self::Credentials) -> Result<Identity, AuthError>;
187}
188
189/// Trait for mapping a provider identity to a local user.
190#[async_trait]
191pub trait UserMapper: Send + Sync {
192    /// The type of the local user object.
193    type LocalUser: Send + Sync;
194
195    /// Map an identity to a local user.
196    /// This could involve creating a new user or finding an existing one.
197    async fn map_user(&self, identity: &Identity) -> Result<Self::LocalUser, AuthError>;
198}
199
200#[async_trait]
201impl UserMapper for () {
202    type LocalUser = ();
203    async fn map_user(&self, _identity: &Identity) -> Result<Self::LocalUser, AuthError> {
204        Ok(())
205    }
206}
207
208/// An in-memory implementation of [`SessionStore`].
209///
210/// **Note**: This store is not persistent and will be cleared when the application restarts.
211/// It is primarily intended for development and testing.
212#[derive(Default)]
213pub struct MemoryStore {
214    sessions: std::sync::Mutex<HashMap<String, Session>>,
215}
216
217impl MemoryStore {
218    /// Create a new, empty `MemoryStore`.
219    pub fn new() -> Self {
220        Self::default()
221    }
222}
223
224#[async_trait]
225impl SessionStore for MemoryStore {
226    async fn load_session(&self, id: &str) -> Result<Option<Session>, AuthError> {
227        Ok(self.sessions.lock().unwrap().get(id).cloned())
228    }
229    async fn save_session(&self, session: &Session) -> Result<(), AuthError> {
230        self.sessions
231            .lock()
232            .unwrap()
233            .insert(session.id.clone(), session.clone());
234        Ok(())
235    }
236    async fn delete_session(&self, id: &str) -> Result<(), AuthError> {
237        self.sessions.lock().unwrap().remove(id);
238        Ok(())
239    }
240}