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}