authkestra_engine/auth/
strategy.rs1use crate::error::AuthError;
2use async_trait::async_trait;
3use http::request::Parts;
4use std::marker::PhantomData;
5
6#[async_trait]
11pub trait AuthenticationStrategy<I>: Send + Sync {
12 async fn authenticate(&self, parts: &Parts) -> Result<Option<I>, AuthError>;
19}
20
21#[async_trait]
23pub trait BasicAuthenticator: Send + Sync {
24 type Identity;
26 async fn authenticate(
28 &self,
29 username: &str,
30 password: &str,
31 ) -> Result<Option<Self::Identity>, AuthError>;
32}
33
34#[non_exhaustive]
36pub struct BasicStrategy<P, I> {
37 authenticator: P,
38 _marker: PhantomData<I>,
39}
40
41impl<P, I> BasicStrategy<P, I> {
42 pub fn new(authenticator: P) -> Self {
44 Self {
45 authenticator,
46 _marker: PhantomData,
47 }
48 }
49}
50
51#[async_trait]
52impl<P, I> AuthenticationStrategy<I> for BasicStrategy<P, I>
53where
54 P: BasicAuthenticator<Identity = I> + Send + Sync,
55 I: Send + Sync + 'static,
56{
57 async fn authenticate(&self, parts: &Parts) -> Result<Option<I>, AuthError> {
58 if let Some((username, password)) = utils::extract_basic_credentials(&parts.headers) {
59 self.authenticator.authenticate(&username, &password).await
60 } else {
61 Ok(None)
62 }
63 }
64}
65
66#[async_trait]
68pub trait TokenValidator: Send + Sync {
69 type Identity;
71 async fn validate(&self, token: &str) -> Result<Option<Self::Identity>, AuthError>;
73}
74
75#[non_exhaustive]
77pub struct TokenStrategy<V, I> {
78 validator: V,
79 _marker: PhantomData<I>,
80}
81
82impl<V, I> TokenStrategy<V, I> {
83 pub fn new(validator: V) -> Self {
85 Self {
86 validator,
87 _marker: PhantomData,
88 }
89 }
90}
91
92#[async_trait]
93impl<V, I> AuthenticationStrategy<I> for TokenStrategy<V, I>
94where
95 V: TokenValidator<Identity = I> + Send + Sync,
96 I: Send + Sync + 'static,
97{
98 async fn authenticate(&self, parts: &Parts) -> Result<Option<I>, AuthError> {
99 if let Some(token) = utils::extract_bearer_token(&parts.headers) {
100 self.validator.validate(token).await
101 } else {
102 Ok(None)
103 }
104 }
105}
106
107#[non_exhaustive]
109pub struct HeaderStrategy<F, I> {
110 header_name: http::header::HeaderName,
111 validator: F,
112 _marker: PhantomData<I>,
113}
114
115impl<F, I> HeaderStrategy<F, I> {
116 pub fn new(header_name: http::header::HeaderName, validator: F) -> Self {
118 Self {
119 header_name,
120 validator,
121 _marker: PhantomData,
122 }
123 }
124}
125
126#[async_trait]
127impl<F, I, Fut> AuthenticationStrategy<I> for HeaderStrategy<F, I>
128where
129 F: Fn(String) -> Fut + Send + Sync,
130 Fut: std::future::Future<Output = Result<Option<I>, AuthError>> + Send,
131 I: Send + Sync + 'static,
132{
133 async fn authenticate(&self, parts: &Parts) -> Result<Option<I>, AuthError> {
134 if let Some(value) = parts.headers.get(&self.header_name) {
135 if let Ok(value_str) = value.to_str() {
136 return (self.validator)(value_str.to_string()).await;
137 }
138 }
139 Ok(None)
140 }
141}
142
143#[async_trait]
145pub trait SessionProvider: Send + Sync {
146 type Identity;
148 async fn load_session(&self, session_id: &str) -> Result<Option<Self::Identity>, AuthError>;
150}
151
152#[non_exhaustive]
154pub struct SessionStrategy<P, I> {
155 provider: P,
156 cookie_name: String,
157 _marker: PhantomData<I>,
158}
159
160impl<P, I> SessionStrategy<P, I> {
161 pub fn new(provider: P, cookie_name: impl Into<String>) -> Self {
163 Self {
164 provider,
165 cookie_name: cookie_name.into(),
166 _marker: PhantomData,
167 }
168 }
169}
170
171#[async_trait]
172impl<P, I> AuthenticationStrategy<I> for SessionStrategy<P, I>
173where
174 P: SessionProvider<Identity = I> + Send + Sync,
175 I: Send + Sync + 'static,
176{
177 async fn authenticate(&self, parts: &Parts) -> Result<Option<I>, AuthError> {
178 if let Some(session_id) = utils::extract_cookie(&parts.headers, &self.cookie_name) {
179 self.provider.load_session(session_id).await
180 } else {
181 Ok(None)
182 }
183 }
184}
185
186pub mod utils {
188 use http::header::{HeaderMap, AUTHORIZATION};
189
190 pub fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
192 headers
193 .get(AUTHORIZATION)?
194 .to_str()
195 .ok()?
196 .strip_prefix("Bearer ")
197 .map(|s| s.trim())
198 }
199
200 pub fn extract_basic_credentials(headers: &HeaderMap) -> Option<(String, String)> {
202 let auth_header = headers.get(AUTHORIZATION)?.to_str().ok()?;
203 if !auth_header.starts_with("Basic ") {
204 return None;
205 }
206 let encoded = auth_header.strip_prefix("Basic ")?.trim();
207 let decoded =
208 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded).ok()?;
209 let decoded_str = String::from_utf8(decoded).ok()?;
210 let mut parts = decoded_str.splitn(2, ':');
211 let username = parts.next()?.to_string();
212 let password = parts.next()?.to_string();
213 Some((username, password))
214 }
215
216 pub fn extract_cookie<'a>(headers: &'a http::HeaderMap, name: &str) -> Option<&'a str> {
218 let cookie_header = headers.get(http::header::COOKIE)?.to_str().ok()?;
219 for cookie in cookie_header.split(';') {
220 let mut parts = cookie.splitn(2, '=');
221 let k = parts.next()?.trim();
222 let v = parts.next()?.trim();
223 if k == name {
224 return Some(v);
225 }
226 }
227 None
228 }
229}