1use std::sync::Arc;
10
11use axum::Json;
12use axum::extract::{FromRef, FromRequestParts};
13use axum::http::StatusCode;
14use axum::http::request::Parts;
15use axum::response::{IntoResponse, Response};
16use axum_extra::extract::cookie::CookieJar;
17use chrono::Utc;
18use ironflow_store::api_key_store::ApiKeyStore;
19use ironflow_store::entities::ApiKeyScope;
20use serde_json::json;
21use uuid::Uuid;
22
23use crate::cookies::AUTH_COOKIE_NAME;
24use crate::jwt::{AccessToken, JwtConfig};
25use crate::password;
26
27#[derive(Debug, Clone)]
46pub struct AuthenticatedUser {
47 pub user_id: Uuid,
49 pub username: String,
51 pub is_admin: bool,
53}
54
55impl<S> FromRequestParts<S> for AuthenticatedUser
56where
57 S: Send + Sync,
58 Arc<JwtConfig>: FromRef<S>,
59{
60 type Rejection = AuthRejection;
61
62 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
63 let jwt_config = Arc::<JwtConfig>::from_ref(state);
64
65 let jar = CookieJar::from_headers(&parts.headers);
66 let token = jar
67 .get(AUTH_COOKIE_NAME)
68 .map(|c| c.value().to_string())
69 .or_else(|| {
70 parts
71 .headers
72 .get("authorization")
73 .and_then(|v| v.to_str().ok())
74 .and_then(|v| v.strip_prefix("Bearer "))
75 .map(|t| t.to_string())
76 });
77
78 let token = token.ok_or(AuthRejection {
79 status: StatusCode::UNAUTHORIZED,
80 code: "MISSING_TOKEN",
81 message: "No authentication token provided",
82 })?;
83
84 let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
85 status: StatusCode::UNAUTHORIZED,
86 code: "INVALID_TOKEN",
87 message: "Invalid or expired authentication token",
88 })?;
89
90 Ok(AuthenticatedUser {
91 user_id: claims.user_id,
92 username: claims.username,
93 is_admin: claims.is_admin,
94 })
95 }
96}
97
98pub struct AuthRejection {
100 status: StatusCode,
101 code: &'static str,
102 message: &'static str,
103}
104
105impl IntoResponse for AuthRejection {
106 fn into_response(self) -> Response {
107 let body = json!({
108 "error": {
109 "code": self.code,
110 "message": self.message,
111 }
112 });
113 (self.status, Json(body)).into_response()
114 }
115}
116
117pub const API_KEY_PREFIX: &str = "irfl_";
123
124#[derive(Debug, Clone)]
138pub struct ApiKeyAuth {
139 pub key_id: Uuid,
141 pub user_id: Uuid,
143 pub key_name: String,
145 pub scopes: Vec<ApiKeyScope>,
147}
148
149impl ApiKeyAuth {
150 pub fn has_scope(&self, required: &ApiKeyScope) -> bool {
152 ApiKeyScope::has_permission(&self.scopes, required)
153 }
154}
155
156pub struct ApiKeyRejection {
158 status: StatusCode,
159 code: &'static str,
160 message: &'static str,
161}
162
163impl IntoResponse for ApiKeyRejection {
164 fn into_response(self) -> Response {
165 let body = json!({
166 "error": {
167 "code": self.code,
168 "message": self.message,
169 }
170 });
171 (self.status, Json(body)).into_response()
172 }
173}
174
175impl<S> FromRequestParts<S> for ApiKeyAuth
176where
177 S: Send + Sync,
178 Arc<dyn ApiKeyStore>: FromRef<S>,
179{
180 type Rejection = ApiKeyRejection;
181
182 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
183 let api_key_store = Arc::<dyn ApiKeyStore>::from_ref(state);
184
185 let token = parts
186 .headers
187 .get("authorization")
188 .and_then(|v| v.to_str().ok())
189 .and_then(|v| v.strip_prefix("Bearer "))
190 .ok_or(ApiKeyRejection {
191 status: StatusCode::UNAUTHORIZED,
192 code: "MISSING_TOKEN",
193 message: "No authentication token provided",
194 })?;
195
196 if !token.starts_with(API_KEY_PREFIX) {
197 return Err(ApiKeyRejection {
198 status: StatusCode::UNAUTHORIZED,
199 code: "INVALID_TOKEN",
200 message: "Expected API key (irfl_...) in Authorization header",
201 });
202 }
203
204 let suffix_len = (token.len() - API_KEY_PREFIX.len()).min(8);
205 let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
206
207 let api_key = api_key_store
208 .find_api_key_by_prefix(prefix)
209 .await
210 .map_err(|_| ApiKeyRejection {
211 status: StatusCode::INTERNAL_SERVER_ERROR,
212 code: "INTERNAL_ERROR",
213 message: "Failed to look up API key",
214 })?
215 .ok_or(ApiKeyRejection {
216 status: StatusCode::UNAUTHORIZED,
217 code: "INVALID_TOKEN",
218 message: "Invalid API key",
219 })?;
220
221 if !api_key.is_active {
222 return Err(ApiKeyRejection {
223 status: StatusCode::UNAUTHORIZED,
224 code: "KEY_DISABLED",
225 message: "API key is disabled",
226 });
227 }
228
229 if let Some(expires_at) = api_key.expires_at
230 && expires_at < Utc::now()
231 {
232 return Err(ApiKeyRejection {
233 status: StatusCode::UNAUTHORIZED,
234 code: "KEY_EXPIRED",
235 message: "API key has expired",
236 });
237 }
238
239 let valid = password::verify(token, &api_key.key_hash).map_err(|_| ApiKeyRejection {
240 status: StatusCode::INTERNAL_SERVER_ERROR,
241 code: "INTERNAL_ERROR",
242 message: "Failed to verify API key",
243 })?;
244
245 if !valid {
246 return Err(ApiKeyRejection {
247 status: StatusCode::UNAUTHORIZED,
248 code: "INVALID_TOKEN",
249 message: "Invalid API key",
250 });
251 }
252
253 let _ = api_key_store.touch_api_key(api_key.id).await;
254
255 Ok(ApiKeyAuth {
256 key_id: api_key.id,
257 user_id: api_key.user_id,
258 key_name: api_key.name,
259 scopes: api_key.scopes,
260 })
261 }
262}
263
264#[derive(Debug, Clone)]
283pub struct Authenticated {
284 pub user_id: Uuid,
286 pub method: AuthMethod,
288}
289
290#[derive(Debug, Clone)]
292pub enum AuthMethod {
293 Jwt {
295 username: String,
297 is_admin: bool,
299 },
300 ApiKey {
302 key_id: Uuid,
304 key_name: String,
306 scopes: Vec<ApiKeyScope>,
308 },
309}
310
311impl<S> FromRequestParts<S> for Authenticated
312where
313 S: Send + Sync,
314 Arc<JwtConfig>: FromRef<S>,
315 Arc<dyn ApiKeyStore>: FromRef<S>,
316{
317 type Rejection = AuthRejection;
318
319 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
320 let jar = CookieJar::from_headers(&parts.headers);
321 let cookie_token = jar.get(AUTH_COOKIE_NAME).map(|c| c.value().to_string());
322
323 let header_token = parts
324 .headers
325 .get("authorization")
326 .and_then(|v| v.to_str().ok())
327 .and_then(|v| v.strip_prefix("Bearer "))
328 .map(|t| t.to_string());
329
330 if let Some(ref token) = header_token
332 && token.starts_with(API_KEY_PREFIX)
333 {
334 let api_key_auth =
335 ApiKeyAuth::from_request_parts(parts, state)
336 .await
337 .map_err(|_| AuthRejection {
338 status: StatusCode::UNAUTHORIZED,
339 code: "INVALID_TOKEN",
340 message: "Invalid or expired authentication token",
341 })?;
342 return Ok(Authenticated {
343 user_id: api_key_auth.user_id,
344 method: AuthMethod::ApiKey {
345 key_id: api_key_auth.key_id,
346 key_name: api_key_auth.key_name,
347 scopes: api_key_auth.scopes,
348 },
349 });
350 }
351
352 let token = cookie_token.or(header_token).ok_or(AuthRejection {
354 status: StatusCode::UNAUTHORIZED,
355 code: "MISSING_TOKEN",
356 message: "No authentication token provided",
357 })?;
358
359 let jwt_config = Arc::<JwtConfig>::from_ref(state);
360 let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
361 status: StatusCode::UNAUTHORIZED,
362 code: "INVALID_TOKEN",
363 message: "Invalid or expired authentication token",
364 })?;
365
366 Ok(Authenticated {
367 user_id: claims.user_id,
368 method: AuthMethod::Jwt {
369 username: claims.username,
370 is_admin: claims.is_admin,
371 },
372 })
373 }
374}