Skip to main content

authkestra_flow/
lib.rs

1//! # Authkestra Flow
2//!
3//! `authkestra-flow` orchestrates authentication flows, such as OAuth2 Authorization Code,
4//! PKCE, Client Credentials, and Device Flow. It acts as the bridge between the core traits
5//! and the framework-specific adapters.
6//!
7//! ## Key Components
8//!
9//! - **[`OAuth2Flow`]**: Orchestrates the standard OAuth2 Authorization Code flow.
10//! - **[`Authkestra`]**: The main service that holds providers, session stores, and token managers.
11//! - **[`AuthkestraBuilder`]**: A builder for configuring and creating an [`Authkestra`] instance.
12//! - **[`CredentialsFlow`]**: Orchestrates direct credentials-based authentication (e.g., email/password).
13
14#![warn(missing_docs)]
15
16pub use authkestra_core::ErasedOAuthFlow;
17use authkestra_core::{
18    error::AuthError, state::Identity, CredentialsProvider, OAuthProvider, UserMapper,
19};
20#[cfg(feature = "session")]
21pub use authkestra_session::{Session, SessionConfig, SessionStore};
22
23#[cfg(not(feature = "session"))]
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
25/// Configuration for the OAuth flow state cookies when sessions are disabled.
26pub struct SessionConfig {
27    /// Whether the cookie should only be sent over HTTPS.
28    pub secure: bool,
29    /// The maximum age of the state cookie.
30    pub max_age: Option<chrono::Duration>,
31}
32
33#[cfg(not(feature = "session"))]
34impl Default for SessionConfig {
35    fn default() -> Self {
36        Self {
37            secure: true,
38            max_age: Some(chrono::Duration::minutes(15)),
39        }
40    }
41}
42
43pub use chrono;
44
45/// Trait for components that can be used as a session store.
46#[cfg(feature = "session")]
47pub trait SessionStoreState: Send + Sync + 'static {
48    /// Returns the session store if configured.
49    fn get_store(&self) -> Arc<dyn SessionStore>;
50}
51
52#[cfg(feature = "session")]
53impl SessionStoreState for Configured<Arc<dyn SessionStore>> {
54    fn get_store(&self) -> Arc<dyn SessionStore> {
55        self.0.clone()
56    }
57}
58
59#[cfg(feature = "token")]
60use authkestra_token::TokenManager;
61
62#[cfg(feature = "token")]
63/// Trait for components that can be used as a token manager.
64pub trait TokenManagerState: Send + Sync + 'static {
65    /// Returns the token manager if configured.
66    fn get_manager(&self) -> Arc<TokenManager>;
67}
68
69#[cfg(feature = "token")]
70impl TokenManagerState for Configured<Arc<TokenManager>> {
71    fn get_manager(&self) -> Arc<TokenManager> {
72        self.0.clone()
73    }
74}
75
76use std::collections::HashMap;
77use std::sync::Arc;
78
79/// Client Credentials flow implementation.
80pub mod client_credentials_flow;
81/// Device Authorization flow implementation.
82pub mod device_flow;
83/// OAuth2 Authorization Code flow implementation.
84pub mod oauth2;
85
86pub use client_credentials_flow::ClientCredentialsFlow;
87pub use device_flow::{DeviceAuthorizationResponse, DeviceFlow};
88pub use oauth2::OAuth2Flow;
89
90/// Marker for a missing component in the typestate pattern.
91#[derive(Clone, Default)]
92pub struct Missing;
93
94/// Marker for a configured component in the typestate pattern.
95#[derive(Clone)]
96pub struct Configured<T>(pub T);
97
98/// The unified Authkestra service.
99pub struct Authkestra<S = Missing, T = Missing> {
100    /// Map of registered OAuth providers.
101    pub providers: HashMap<String, Arc<dyn ErasedOAuthFlow>>,
102    /// The session storage backend.
103    #[cfg(feature = "session")]
104    pub session_store: S,
105    /// Configuration for session cookies.
106    #[cfg(feature = "session")]
107    pub session_config: SessionConfig,
108    /// Manager for JWT signing and verification.
109    #[cfg(feature = "token")]
110    pub token_manager: T,
111    /// Phantom data to keep type parameters S and T when they are not used in fields.
112    #[cfg(all(not(feature = "session"), not(feature = "token")))]
113    pub _marker: std::marker::PhantomData<(S, T)>,
114    /// Phantom data to keep type parameter T when it's not used in fields.
115    #[cfg(all(feature = "session", not(feature = "token")))]
116    pub _marker: std::marker::PhantomData<T>,
117    /// Phantom data to keep type parameter S when it's not used in fields.
118    #[cfg(all(not(feature = "session"), feature = "token"))]
119    pub _marker: std::marker::PhantomData<S>,
120}
121
122impl<S, T> Clone for Authkestra<S, T>
123where
124    S: Clone,
125    T: Clone,
126{
127    fn clone(&self) -> Self {
128        Self {
129            providers: self.providers.clone(),
130            #[cfg(feature = "session")]
131            session_store: self.session_store.clone(),
132            #[cfg(feature = "session")]
133            session_config: self.session_config.clone(),
134            #[cfg(feature = "token")]
135            token_manager: self.token_manager.clone(),
136            #[cfg(any(not(feature = "session"), not(feature = "token")))]
137            _marker: std::marker::PhantomData,
138        }
139    }
140}
141
142impl Authkestra<Missing, Missing> {
143    /// Create a new [`AuthkestraBuilder`] to configure the service.
144    pub fn builder() -> AuthkestraBuilder<Missing, Missing> {
145        AuthkestraBuilder::default()
146    }
147}
148
149#[cfg(feature = "session")]
150impl<T> Authkestra<Configured<Arc<dyn SessionStore>>, T> {
151    /// Create a new session for the given identity.
152    pub async fn create_session(&self, identity: Identity) -> Result<Session, AuthError> {
153        let session_duration = self
154            .session_config
155            .max_age
156            .unwrap_or(chrono::Duration::hours(24));
157        let session = Session {
158            id: uuid::Uuid::new_v4().to_string(),
159            identity,
160            expires_at: chrono::Utc::now() + session_duration,
161        };
162
163        self.session_store
164            .0
165            .save_session(&session)
166            .await
167            .map_err(|e| AuthError::Session(e.to_string()))?;
168
169        Ok(session)
170    }
171}
172
173#[cfg(feature = "token")]
174impl<S> Authkestra<S, Configured<Arc<TokenManager>>> {
175    /// Issue a JWT for the given identity.
176    pub fn issue_token(
177        &self,
178        identity: Identity,
179        expires_in_secs: u64,
180    ) -> Result<String, AuthError> {
181        self.token_manager
182            .0
183            .issue_user_token(identity, expires_in_secs, None)
184            .map_err(|e| AuthError::Token(e.to_string()))
185    }
186}
187
188/// A builder for configuring and creating an [`Authkestra`] instance.
189pub struct AuthkestraBuilder<S, T> {
190    providers: HashMap<String, Arc<dyn ErasedOAuthFlow>>,
191    #[cfg(feature = "session")]
192    session_store: S,
193    #[cfg(feature = "session")]
194    session_config: SessionConfig,
195    #[cfg(feature = "token")]
196    token_manager: T,
197    /// Phantom data to keep type parameters S and T when they are not used in fields.
198    #[cfg(all(not(feature = "session"), not(feature = "token")))]
199    pub _marker: std::marker::PhantomData<(S, T)>,
200    /// Phantom data to keep type parameter T when it's not used in fields.
201    #[cfg(all(feature = "session", not(feature = "token")))]
202    pub _marker: std::marker::PhantomData<T>,
203    /// Phantom data to keep type parameter S when it's not used in fields.
204    #[cfg(all(not(feature = "session"), feature = "token"))]
205    pub _marker: std::marker::PhantomData<S>,
206}
207
208impl Default for AuthkestraBuilder<Missing, Missing> {
209    fn default() -> Self {
210        Self {
211            providers: HashMap::new(),
212            #[cfg(feature = "session")]
213            session_store: Missing,
214            #[cfg(feature = "session")]
215            session_config: SessionConfig::default(),
216            #[cfg(feature = "token")]
217            token_manager: Missing,
218            #[cfg(any(not(feature = "session"), not(feature = "token")))]
219            _marker: std::marker::PhantomData,
220        }
221    }
222}
223
224impl<S, T> AuthkestraBuilder<S, T> {
225    /// Register an OAuth provider flow.
226    pub fn provider<P, M>(mut self, flow: OAuth2Flow<P, M>) -> Self
227    where
228        P: OAuthProvider + 'static,
229        M: UserMapper + 'static,
230    {
231        let id = flow.provider_id();
232        self.providers.insert(id, Arc::new(flow));
233        self
234    }
235
236    /// Set the session store.
237    #[cfg(feature = "session")]
238    pub fn session_store(
239        self,
240        store: Arc<dyn SessionStore>,
241    ) -> AuthkestraBuilder<Configured<Arc<dyn SessionStore>>, T> {
242        AuthkestraBuilder {
243            providers: self.providers,
244            session_store: Configured(store),
245            session_config: self.session_config,
246            #[cfg(feature = "token")]
247            token_manager: self.token_manager,
248            #[cfg(any(not(feature = "session"), not(feature = "token")))]
249            _marker: std::marker::PhantomData,
250        }
251    }
252
253    /// Set the token manager.
254    #[cfg(feature = "token")]
255    pub fn token_manager(
256        self,
257        manager: Arc<TokenManager>,
258    ) -> AuthkestraBuilder<S, Configured<Arc<TokenManager>>> {
259        AuthkestraBuilder {
260            providers: self.providers,
261            #[cfg(feature = "session")]
262            session_store: self.session_store,
263            #[cfg(feature = "session")]
264            session_config: self.session_config,
265            token_manager: Configured(manager),
266            #[cfg(any(not(feature = "session"), not(feature = "token")))]
267            _marker: std::marker::PhantomData,
268        }
269    }
270
271    /// Set the JWT secret for the default token manager.
272    #[cfg(feature = "token")]
273    pub fn jwt_secret(self, secret: &[u8]) -> AuthkestraBuilder<S, Configured<Arc<TokenManager>>> {
274        self.token_manager(Arc::new(TokenManager::new(secret, None)))
275    }
276
277    /// Build the [`Authkestra`] instance.
278    pub fn build(self) -> Authkestra<S, T> {
279        Authkestra {
280            providers: self.providers,
281            #[cfg(feature = "session")]
282            session_store: self.session_store,
283            #[cfg(feature = "session")]
284            session_config: self.session_config,
285            #[cfg(feature = "token")]
286            token_manager: self.token_manager,
287            #[cfg(any(not(feature = "session"), not(feature = "token")))]
288            _marker: std::marker::PhantomData,
289        }
290    }
291}
292
293#[cfg(feature = "session")]
294impl<S, T> AuthkestraBuilder<S, T> {
295    /// Set the session configuration.
296    pub fn session_config(mut self, config: SessionConfig) -> Self {
297        self.session_config = config;
298        self
299    }
300}
301
302#[cfg(feature = "token")]
303impl<S> AuthkestraBuilder<S, Configured<Arc<TokenManager>>> {
304    /// Set the JWT issuer for the token manager.
305    ///
306    /// This is only available if a token manager is configured.
307    pub fn jwt_issuer(self, issuer: impl Into<String>) -> Self {
308        let manager = Arc::new((*self.token_manager.0).clone().with_issuer(issuer.into()));
309        self.token_manager(manager)
310    }
311}
312
313/// Trait for Authkestra instances that have a session store configured.
314#[cfg(feature = "session")]
315pub trait HasSessionStore {
316    /// Returns the session store.
317    fn session_store(&self) -> Arc<dyn SessionStore>;
318}
319
320#[cfg(feature = "session")]
321impl<T> HasSessionStore for Authkestra<Configured<Arc<dyn SessionStore>>, T> {
322    fn session_store(&self) -> Arc<dyn SessionStore> {
323        self.session_store.0.clone()
324    }
325}
326
327/// Trait for Authkestra instances that have a token manager configured.
328#[cfg(feature = "token")]
329pub trait HasTokenManager {
330    /// Returns the token manager.
331    fn token_manager(&self) -> Arc<TokenManager>;
332}
333
334#[cfg(feature = "token")]
335impl<S> HasTokenManager for Authkestra<S, Configured<Arc<TokenManager>>> {
336    fn token_manager(&self) -> Arc<TokenManager> {
337        self.token_manager.0.clone()
338    }
339}
340
341/// Marker for a configured session store.
342#[cfg(feature = "session")]
343pub type HasSessionStoreMarker = Configured<Arc<dyn SessionStore>>;
344/// Marker for a missing session store.
345pub type NoSessionStoreMarker = Missing;
346
347#[cfg(feature = "token")]
348/// Marker for a configured token manager.
349pub type HasTokenManagerMarker = Configured<Arc<TokenManager>>;
350/// Marker for a missing token manager.
351pub type NoTokenManagerMarker = Missing;
352
353/// Authkestra with session support only.
354///
355/// This type is typically used in traditional web applications where the server
356/// manages user sessions.
357#[cfg(feature = "session")]
358pub type StatefullAuthkestra = Authkestra<HasSessionStoreMarker, NoTokenManagerMarker>;
359
360#[cfg(feature = "token")]
361/// Authkestra with token support only
362///
363/// A Resource Server (API) that validates tokens.
364/// This type is used for APIs that need to verify JWTs issued by an authorization server.
365pub type StatelessAuthkestra = Authkestra<NoSessionStoreMarker, HasTokenManagerMarker>;
366
367/// Orchestrates a direct credentials flow.
368pub struct CredentialsFlow<P: CredentialsProvider, M: UserMapper = ()> {
369    provider: P,
370    mapper: Option<M>,
371}
372
373impl<P: CredentialsProvider> CredentialsFlow<P, ()> {
374    /// Create a new `CredentialsFlow` with the given provider.
375    pub fn new(provider: P) -> Self {
376        Self {
377            provider,
378            mapper: None,
379        }
380    }
381}
382
383impl<P: CredentialsProvider, M: UserMapper> CredentialsFlow<P, M> {
384    /// Create a new `CredentialsFlow` with the given provider and user mapper.
385    pub fn with_mapper(provider: P, mapper: M) -> Self {
386        Self {
387            provider,
388            mapper: Some(mapper),
389        }
390    }
391
392    /// Authenticate using the given credentials.
393    pub async fn authenticate(
394        &self,
395        creds: P::Credentials,
396    ) -> Result<(Identity, Option<M::LocalUser>), AuthError> {
397        let identity = self.provider.authenticate(creds).await?;
398
399        let local_user = if let Some(mapper) = &self.mapper {
400            Some(mapper.map_user(&identity).await?)
401        } else {
402            None
403        };
404
405        Ok((identity, local_user))
406    }
407}