Skip to main content

authkestra_engine/flow/
mod.rs

1//! # Engine 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//! - **[`Engine`]**: The main service that holds providers, session stores, and token managers.
11//! - **[`EngineBuilder`]**: A builder for configuring and creating an [`Engine`] instance.
12//! - **[`CredentialsFlow`]**: Orchestrates direct credentials-based authentication (e.g., email/password).
13
14#![warn(missing_docs)]
15
16use crate::auth::{error::AuthError, state::Identity, CredentialsProvider, UserMapper};
17pub use crate::auth::{ErasedOAuthFlow, Session, SessionConfig, SessionStore};
18use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20
21pub use chrono;
22
23/// Context for an authentication flow.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[non_exhaustive]
26pub struct FlowContext {
27    /// The current state identifier.
28    pub state: String,
29    /// Parameters associated with the flow.
30    pub params: HashMap<String, String>,
31}
32
33/// Result of an authentication flow execution.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub enum FlowResult {
36    /// The flow is complete and has returned an identity.
37    Complete(Identity),
38    /// The flow requires a redirect to another URL.
39    Redirect(String),
40    /// The flow is pending (e.g., waiting for user interaction).
41    Pending,
42}
43
44/// Orchestrates the steps of an authentication protocol (e.g., OAuth2, Device Flow).
45#[async_trait]
46pub trait Flow: Send + Sync {
47    /// Returns the unique identifier for the flow.
48    fn id(&self) -> &str;
49
50    /// Executes the flow with the given context.
51    async fn execute(&self, ctx: FlowContext) -> Result<FlowResult, AuthError>;
52}
53
54use std::collections::HashMap;
55
56pub use crate::engine::{Configured, Engine, EngineBuilder, Missing};
57
58/// Client Credentials flow implementation.
59pub mod client_credentials_flow;
60/// Device Authorization flow implementation.
61pub mod device_flow;
62/// OAuth2 Authorization Code flow implementation.
63pub mod oauth2;
64
65pub use client_credentials_flow::ClientCredentialsFlow;
66pub use device_flow::{DeviceAuthorizationResponse, DeviceFlow};
67pub use oauth2::OAuth2Flow;
68
69/// Orchestrates a direct credentials flow.
70#[non_exhaustive]
71pub struct CredentialsFlow<P: CredentialsProvider, M: UserMapper = ()> {
72    provider: P,
73    mapper: Option<M>,
74}
75
76impl<P: CredentialsProvider> CredentialsFlow<P, ()> {
77    /// Create a new `CredentialsFlow` with the given provider.
78    pub fn new(provider: P) -> Self {
79        Self {
80            provider,
81            mapper: None,
82        }
83    }
84}
85
86impl<P: CredentialsProvider, M: UserMapper> CredentialsFlow<P, M> {
87    /// Create a new `CredentialsFlow` with the given provider and user mapper.
88    pub fn with_mapper(provider: P, mapper: M) -> Self {
89        Self {
90            provider,
91            mapper: Some(mapper),
92        }
93    }
94
95    /// Authenticate using the given credentials.
96    pub async fn authenticate(
97        &self,
98        creds: P::Credentials,
99    ) -> Result<(Identity, Option<M::LocalUser>), AuthError> {
100        let identity = self.provider.authenticate(creds).await?;
101
102        let local_user = if let Some(mapper) = &self.mapper {
103            Some(mapper.map_user(&identity).await?)
104        } else {
105            None
106        };
107
108        Ok((identity, local_user))
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use async_trait::async_trait;
116
117    #[derive(Debug, PartialEq, Clone)]
118    struct DummyCreds(String);
119
120    struct DummyProvider;
121    #[async_trait]
122    impl CredentialsProvider for DummyProvider {
123        type Credentials = DummyCreds;
124        async fn authenticate(&self, creds: Self::Credentials) -> Result<Identity, AuthError> {
125            if creds.0 == "valid" {
126                Ok(Identity {
127                    provider_id: "dummy".to_string(),
128                    external_id: "user_123".to_string(),
129                    email: None,
130                    username: None,
131                    attributes: std::collections::HashMap::new(),
132                })
133            } else {
134                Err(AuthError::InvalidCredentials)
135            }
136        }
137    }
138
139    #[derive(Debug, PartialEq)]
140    struct DummyUser(String);
141
142    struct DummyMapper;
143    #[async_trait]
144    impl UserMapper for DummyMapper {
145        type LocalUser = DummyUser;
146        async fn map_user(&self, identity: &Identity) -> Result<Self::LocalUser, AuthError> {
147            Ok(DummyUser(identity.external_id.clone()))
148        }
149    }
150
151    #[tokio::test]
152    async fn test_credentials_flow() {
153        let flow = CredentialsFlow::new(DummyProvider);
154        let res = flow
155            .authenticate(DummyCreds("valid".to_string()))
156            .await
157            .unwrap();
158        assert_eq!(res.0.external_id, "user_123");
159        assert!(res.1.is_none());
160
161        let err = flow.authenticate(DummyCreds("invalid".to_string())).await;
162        assert!(err.is_err());
163    }
164
165    #[tokio::test]
166    async fn test_credentials_flow_with_mapper() {
167        let flow = CredentialsFlow::with_mapper(DummyProvider, DummyMapper);
168        let res = flow
169            .authenticate(DummyCreds("valid".to_string()))
170            .await
171            .unwrap();
172        assert_eq!(res.0.external_id, "user_123");
173        assert_eq!(res.1.unwrap(), DummyUser("user_123".to_string()));
174    }
175}