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::entities::ApiKeyScope;
19use ironflow_store::store::Store;
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
124pub const API_KEY_SUFFIX_LEN: usize = 8;
126
127#[derive(Debug, Clone)]
141pub struct ApiKeyAuth {
142 pub key_id: Uuid,
144 pub user_id: Uuid,
146 pub key_name: String,
148 pub scopes: Vec<ApiKeyScope>,
150 pub owner_is_admin: bool,
152}
153
154impl ApiKeyAuth {
155 pub fn has_scope(&self, required: &ApiKeyScope) -> bool {
157 ApiKeyScope::has_permission(&self.scopes, required)
158 }
159}
160
161pub struct ApiKeyRejection {
163 status: StatusCode,
164 code: &'static str,
165 message: &'static str,
166}
167
168impl IntoResponse for ApiKeyRejection {
169 fn into_response(self) -> Response {
170 let body = json!({
171 "error": {
172 "code": self.code,
173 "message": self.message,
174 }
175 });
176 (self.status, Json(body)).into_response()
177 }
178}
179
180impl<S> FromRequestParts<S> for ApiKeyAuth
181where
182 S: Send + Sync,
183 Arc<dyn Store>: FromRef<S>,
184{
185 type Rejection = ApiKeyRejection;
186
187 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
188 let store = Arc::<dyn Store>::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(API_KEY_SUFFIX_LEN);
210 let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
211
212 let 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 _ = store.touch_api_key(api_key.id).await;
259
260 let owner = 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 Store>: FromRef<S>,
348{
349 type Rejection = AuthRejection;
350
351 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
352 let jar = CookieJar::from_headers(&parts.headers);
353 let cookie_token = jar.get(AUTH_COOKIE_NAME).map(|c| c.value().to_string());
354
355 let header_token = parts
356 .headers
357 .get("authorization")
358 .and_then(|v| v.to_str().ok())
359 .and_then(|v| v.strip_prefix("Bearer "))
360 .map(|t| t.to_string());
361
362 if let Some(ref token) = header_token
364 && token.starts_with(API_KEY_PREFIX)
365 {
366 let api_key_auth =
367 ApiKeyAuth::from_request_parts(parts, state)
368 .await
369 .map_err(|_| AuthRejection {
370 status: StatusCode::UNAUTHORIZED,
371 code: "INVALID_TOKEN",
372 message: "Invalid or expired authentication token",
373 })?;
374 return Ok(Authenticated {
375 user_id: api_key_auth.user_id,
376 method: AuthMethod::ApiKey {
377 key_id: api_key_auth.key_id,
378 key_name: api_key_auth.key_name,
379 scopes: api_key_auth.scopes,
380 owner_is_admin: api_key_auth.owner_is_admin,
381 },
382 });
383 }
384
385 let token = cookie_token.or(header_token).ok_or(AuthRejection {
387 status: StatusCode::UNAUTHORIZED,
388 code: "MISSING_TOKEN",
389 message: "No authentication token provided",
390 })?;
391
392 let jwt_config = Arc::<JwtConfig>::from_ref(state);
393 let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
394 status: StatusCode::UNAUTHORIZED,
395 code: "INVALID_TOKEN",
396 message: "Invalid or expired authentication token",
397 })?;
398
399 Ok(Authenticated {
400 user_id: claims.user_id,
401 method: AuthMethod::Jwt {
402 username: claims.username,
403 is_admin: claims.is_admin,
404 },
405 })
406 }
407}