authkestra_engine/flow/
mod.rs1#![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#[derive(Debug, Clone, Serialize, Deserialize)]
25#[non_exhaustive]
26pub struct FlowContext {
27 pub state: String,
29 pub params: HashMap<String, String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub enum FlowResult {
36 Complete(Identity),
38 Redirect(String),
40 Pending,
42}
43
44#[async_trait]
46pub trait Flow: Send + Sync {
47 fn id(&self) -> &str;
49
50 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
58pub mod client_credentials_flow;
60pub mod device_flow;
62pub mod oauth2;
64
65pub use client_credentials_flow::ClientCredentialsFlow;
66pub use device_flow::{DeviceAuthorizationResponse, DeviceFlow};
67pub use oauth2::OAuth2Flow;
68
69#[non_exhaustive]
71pub struct CredentialsFlow<P: CredentialsProvider, M: UserMapper = ()> {
72 provider: P,
73 mapper: Option<M>,
74}
75
76impl<P: CredentialsProvider> CredentialsFlow<P, ()> {
77 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 pub fn with_mapper(provider: P, mapper: M) -> Self {
89 Self {
90 provider,
91 mapper: Some(mapper),
92 }
93 }
94
95 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}