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 ironflow_store::user_store::UserStore;
21use serde_json::json;
22use uuid::Uuid;
23
24use crate::cookies::AUTH_COOKIE_NAME;
25use crate::jwt::{AccessToken, JwtConfig};
26use crate::password;
27
28#[derive(Debug, Clone)]
47pub struct AuthenticatedUser {
48 pub user_id: Uuid,
50 pub username: String,
52 pub is_admin: bool,
54}
55
56impl<S> FromRequestParts<S> for AuthenticatedUser
57where
58 S: Send + Sync,
59 Arc<JwtConfig>: FromRef<S>,
60{
61 type Rejection = AuthRejection;
62
63 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
64 let jwt_config = Arc::<JwtConfig>::from_ref(state);
65
66 let jar = CookieJar::from_headers(&parts.headers);
67 let token = jar
68 .get(AUTH_COOKIE_NAME)
69 .map(|c| c.value().to_string())
70 .or_else(|| {
71 parts
72 .headers
73 .get("authorization")
74 .and_then(|v| v.to_str().ok())
75 .and_then(|v| v.strip_prefix("Bearer "))
76 .map(|t| t.to_string())
77 });
78
79 let token = token.ok_or(AuthRejection {
80 status: StatusCode::UNAUTHORIZED,
81 code: "MISSING_TOKEN",
82 message: "No authentication token provided",
83 })?;
84
85 let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
86 status: StatusCode::UNAUTHORIZED,
87 code: "INVALID_TOKEN",
88 message: "Invalid or expired authentication token",
89 })?;
90
91 Ok(AuthenticatedUser {
92 user_id: claims.user_id,
93 username: claims.username,
94 is_admin: claims.is_admin,
95 })
96 }
97}
98
99pub struct AuthRejection {
101 status: StatusCode,
102 code: &'static str,
103 message: &'static str,
104}
105
106impl IntoResponse for AuthRejection {
107 fn into_response(self) -> Response {
108 let body = json!({
109 "error": {
110 "code": self.code,
111 "message": self.message,
112 }
113 });
114 (self.status, Json(body)).into_response()
115 }
116}
117
118pub const API_KEY_PREFIX: &str = "irfl_";
124
125#[derive(Debug, Clone)]
139pub struct ApiKeyAuth {
140 pub key_id: Uuid,
142 pub user_id: Uuid,
144 pub key_name: String,
146 pub scopes: Vec<ApiKeyScope>,
148 pub owner_is_admin: bool,
150}
151
152impl ApiKeyAuth {
153 pub fn has_scope(&self, required: &ApiKeyScope) -> bool {
155 ApiKeyScope::has_permission(&self.scopes, required)
156 }
157}
158
159pub struct ApiKeyRejection {
161 status: StatusCode,
162 code: &'static str,
163 message: &'static str,
164}
165
166impl IntoResponse for ApiKeyRejection {
167 fn into_response(self) -> Response {
168 let body = json!({
169 "error": {
170 "code": self.code,
171 "message": self.message,
172 }
173 });
174 (self.status, Json(body)).into_response()
175 }
176}
177
178impl<S> FromRequestParts<S> for ApiKeyAuth
179where
180 S: Send + Sync,
181 Arc<dyn ApiKeyStore>: FromRef<S>,
182 Arc<dyn UserStore>: FromRef<S>,
183{
184 type Rejection = ApiKeyRejection;
185
186 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
187 let api_key_store = Arc::<dyn ApiKeyStore>::from_ref(state);
188 let user_store = Arc::<dyn UserStore>::from_ref(state);
189
190 let token = parts
191 .headers
192 .get("authorization")
193 .and_then(|v| v.to_str().ok())
194 .and_then(|v| v.strip_prefix("Bearer "))
195 .ok_or(ApiKeyRejection {
196 status: StatusCode::UNAUTHORIZED,
197 code: "MISSING_TOKEN",
198 message: "No authentication token provided",
199 })?;
200
201 if !token.starts_with(API_KEY_PREFIX) {
202 return Err(ApiKeyRejection {
203 status: StatusCode::UNAUTHORIZED,
204 code: "INVALID_TOKEN",
205 message: "Expected API key (irfl_...) in Authorization header",
206 });
207 }
208
209 let suffix_len = (token.len() - API_KEY_PREFIX.len()).min(8);
210 let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
211
212 let api_key = api_key_store
213 .find_api_key_by_prefix(prefix)
214 .await
215 .map_err(|_| ApiKeyRejection {
216 status: StatusCode::INTERNAL_SERVER_ERROR,
217 code: "INTERNAL_ERROR",
218 message: "Failed to look up API key",
219 })?
220 .ok_or(ApiKeyRejection {
221 status: StatusCode::UNAUTHORIZED,
222 code: "INVALID_TOKEN",
223 message: "Invalid API key",
224 })?;
225
226 if !api_key.is_active {
227 return Err(ApiKeyRejection {
228 status: StatusCode::UNAUTHORIZED,
229 code: "KEY_DISABLED",
230 message: "API key is disabled",
231 });
232 }
233
234 if let Some(expires_at) = api_key.expires_at
235 && expires_at < Utc::now()
236 {
237 return Err(ApiKeyRejection {
238 status: StatusCode::UNAUTHORIZED,
239 code: "KEY_EXPIRED",
240 message: "API key has expired",
241 });
242 }
243
244 let valid = password::verify(token, &api_key.key_hash).map_err(|_| ApiKeyRejection {
245 status: StatusCode::INTERNAL_SERVER_ERROR,
246 code: "INTERNAL_ERROR",
247 message: "Failed to verify API key",
248 })?;
249
250 if !valid {
251 return Err(ApiKeyRejection {
252 status: StatusCode::UNAUTHORIZED,
253 code: "INVALID_TOKEN",
254 message: "Invalid API key",
255 });
256 }
257
258 let _ = api_key_store.touch_api_key(api_key.id).await;
259
260 let owner = user_store
261 .find_user_by_id(api_key.user_id)
262 .await
263 .map_err(|_| ApiKeyRejection {
264 status: StatusCode::INTERNAL_SERVER_ERROR,
265 code: "INTERNAL_ERROR",
266 message: "Failed to look up API key owner",
267 })?;
268 let owner_is_admin = owner.map(|u| u.is_admin).unwrap_or(false);
269
270 Ok(ApiKeyAuth {
271 key_id: api_key.id,
272 user_id: api_key.user_id,
273 key_name: api_key.name,
274 scopes: api_key.scopes,
275 owner_is_admin,
276 })
277 }
278}
279
280#[derive(Debug, Clone)]
299pub struct Authenticated {
300 pub user_id: Uuid,
302 pub method: AuthMethod,
304}
305
306#[derive(Debug, Clone)]
308pub enum AuthMethod {
309 Jwt {
311 username: String,
313 is_admin: bool,
315 },
316 ApiKey {
318 key_id: Uuid,
320 key_name: String,
322 scopes: Vec<ApiKeyScope>,
324 owner_is_admin: bool,
326 },
327}
328
329impl Authenticated {
330 pub fn is_admin(&self) -> bool {
336 match &self.method {
337 AuthMethod::Jwt { is_admin, .. } => *is_admin,
338 AuthMethod::ApiKey { owner_is_admin, .. } => *owner_is_admin,
339 }
340 }
341}
342
343impl<S> FromRequestParts<S> for Authenticated
344where
345 S: Send + Sync,
346 Arc<JwtConfig>: FromRef<S>,
347 Arc<dyn ApiKeyStore>: FromRef<S>,
348 Arc<dyn UserStore>: FromRef<S>,
349{
350 type Rejection = AuthRejection;
351
352 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
353 let jar = CookieJar::from_headers(&parts.headers);
354 let cookie_token = jar.get(AUTH_COOKIE_NAME).map(|c| c.value().to_string());
355
356 let header_token = parts
357 .headers
358 .get("authorization")
359 .and_then(|v| v.to_str().ok())
360 .and_then(|v| v.strip_prefix("Bearer "))
361 .map(|t| t.to_string());
362
363 if let Some(ref token) = header_token
365 && token.starts_with(API_KEY_PREFIX)
366 {
367 let api_key_auth =
368 ApiKeyAuth::from_request_parts(parts, state)
369 .await
370 .map_err(|_| AuthRejection {
371 status: StatusCode::UNAUTHORIZED,
372 code: "INVALID_TOKEN",
373 message: "Invalid or expired authentication token",
374 })?;
375 return Ok(Authenticated {
376 user_id: api_key_auth.user_id,
377 method: AuthMethod::ApiKey {
378 key_id: api_key_auth.key_id,
379 key_name: api_key_auth.key_name,
380 scopes: api_key_auth.scopes,
381 owner_is_admin: api_key_auth.owner_is_admin,
382 },
383 });
384 }
385
386 let token = cookie_token.or(header_token).ok_or(AuthRejection {
388 status: StatusCode::UNAUTHORIZED,
389 code: "MISSING_TOKEN",
390 message: "No authentication token provided",
391 })?;
392
393 let jwt_config = Arc::<JwtConfig>::from_ref(state);
394 let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
395 status: StatusCode::UNAUTHORIZED,
396 code: "INVALID_TOKEN",
397 message: "Invalid or expired authentication token",
398 })?;
399
400 Ok(Authenticated {
401 user_id: claims.user_id,
402 method: AuthMethod::Jwt {
403 username: claims.username,
404 is_admin: claims.is_admin,
405 },
406 })
407 }
408}