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
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 pub owner_is_admin: bool,
149}
150
151impl ApiKeyAuth {
152 pub fn has_scope(&self, required: &ApiKeyScope) -> bool {
154 ApiKeyScope::has_permission(&self.scopes, required)
155 }
156}
157
158pub struct ApiKeyRejection {
160 status: StatusCode,
161 code: &'static str,
162 message: &'static str,
163}
164
165impl IntoResponse for ApiKeyRejection {
166 fn into_response(self) -> Response {
167 let body = json!({
168 "error": {
169 "code": self.code,
170 "message": self.message,
171 }
172 });
173 (self.status, Json(body)).into_response()
174 }
175}
176
177impl<S> FromRequestParts<S> for ApiKeyAuth
178where
179 S: Send + Sync,
180 Arc<dyn Store>: FromRef<S>,
181{
182 type Rejection = ApiKeyRejection;
183
184 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
185 let store = Arc::<dyn Store>::from_ref(state);
186
187 let token = parts
188 .headers
189 .get("authorization")
190 .and_then(|v| v.to_str().ok())
191 .and_then(|v| v.strip_prefix("Bearer "))
192 .ok_or(ApiKeyRejection {
193 status: StatusCode::UNAUTHORIZED,
194 code: "MISSING_TOKEN",
195 message: "No authentication token provided",
196 })?;
197
198 if !token.starts_with(API_KEY_PREFIX) {
199 return Err(ApiKeyRejection {
200 status: StatusCode::UNAUTHORIZED,
201 code: "INVALID_TOKEN",
202 message: "Expected API key (irfl_...) in Authorization header",
203 });
204 }
205
206 let suffix_len = (token.len() - API_KEY_PREFIX.len()).min(8);
207 let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
208
209 let api_key = store
210 .find_api_key_by_prefix(prefix)
211 .await
212 .map_err(|_| ApiKeyRejection {
213 status: StatusCode::INTERNAL_SERVER_ERROR,
214 code: "INTERNAL_ERROR",
215 message: "Failed to look up API key",
216 })?
217 .ok_or(ApiKeyRejection {
218 status: StatusCode::UNAUTHORIZED,
219 code: "INVALID_TOKEN",
220 message: "Invalid API key",
221 })?;
222
223 if !api_key.is_active {
224 return Err(ApiKeyRejection {
225 status: StatusCode::UNAUTHORIZED,
226 code: "KEY_DISABLED",
227 message: "API key is disabled",
228 });
229 }
230
231 if let Some(expires_at) = api_key.expires_at
232 && expires_at < Utc::now()
233 {
234 return Err(ApiKeyRejection {
235 status: StatusCode::UNAUTHORIZED,
236 code: "KEY_EXPIRED",
237 message: "API key has expired",
238 });
239 }
240
241 let valid = password::verify(token, &api_key.key_hash).map_err(|_| ApiKeyRejection {
242 status: StatusCode::INTERNAL_SERVER_ERROR,
243 code: "INTERNAL_ERROR",
244 message: "Failed to verify API key",
245 })?;
246
247 if !valid {
248 return Err(ApiKeyRejection {
249 status: StatusCode::UNAUTHORIZED,
250 code: "INVALID_TOKEN",
251 message: "Invalid API key",
252 });
253 }
254
255 let _ = store.touch_api_key(api_key.id).await;
256
257 let owner = store
258 .find_user_by_id(api_key.user_id)
259 .await
260 .map_err(|_| ApiKeyRejection {
261 status: StatusCode::INTERNAL_SERVER_ERROR,
262 code: "INTERNAL_ERROR",
263 message: "Failed to look up API key owner",
264 })?;
265 let owner_is_admin = owner.map(|u| u.is_admin).unwrap_or(false);
266
267 Ok(ApiKeyAuth {
268 key_id: api_key.id,
269 user_id: api_key.user_id,
270 key_name: api_key.name,
271 scopes: api_key.scopes,
272 owner_is_admin,
273 })
274 }
275}
276
277#[derive(Debug, Clone)]
296pub struct Authenticated {
297 pub user_id: Uuid,
299 pub method: AuthMethod,
301}
302
303#[derive(Debug, Clone)]
305pub enum AuthMethod {
306 Jwt {
308 username: String,
310 is_admin: bool,
312 },
313 ApiKey {
315 key_id: Uuid,
317 key_name: String,
319 scopes: Vec<ApiKeyScope>,
321 owner_is_admin: bool,
323 },
324}
325
326impl Authenticated {
327 pub fn is_admin(&self) -> bool {
333 match &self.method {
334 AuthMethod::Jwt { is_admin, .. } => *is_admin,
335 AuthMethod::ApiKey { owner_is_admin, .. } => *owner_is_admin,
336 }
337 }
338}
339
340impl<S> FromRequestParts<S> for Authenticated
341where
342 S: Send + Sync,
343 Arc<JwtConfig>: FromRef<S>,
344 Arc<dyn Store>: FromRef<S>,
345{
346 type Rejection = AuthRejection;
347
348 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
349 let jar = CookieJar::from_headers(&parts.headers);
350 let cookie_token = jar.get(AUTH_COOKIE_NAME).map(|c| c.value().to_string());
351
352 let header_token = parts
353 .headers
354 .get("authorization")
355 .and_then(|v| v.to_str().ok())
356 .and_then(|v| v.strip_prefix("Bearer "))
357 .map(|t| t.to_string());
358
359 if let Some(ref token) = header_token
361 && token.starts_with(API_KEY_PREFIX)
362 {
363 let api_key_auth =
364 ApiKeyAuth::from_request_parts(parts, state)
365 .await
366 .map_err(|_| AuthRejection {
367 status: StatusCode::UNAUTHORIZED,
368 code: "INVALID_TOKEN",
369 message: "Invalid or expired authentication token",
370 })?;
371 return Ok(Authenticated {
372 user_id: api_key_auth.user_id,
373 method: AuthMethod::ApiKey {
374 key_id: api_key_auth.key_id,
375 key_name: api_key_auth.key_name,
376 scopes: api_key_auth.scopes,
377 owner_is_admin: api_key_auth.owner_is_admin,
378 },
379 });
380 }
381
382 let token = cookie_token.or(header_token).ok_or(AuthRejection {
384 status: StatusCode::UNAUTHORIZED,
385 code: "MISSING_TOKEN",
386 message: "No authentication token provided",
387 })?;
388
389 let jwt_config = Arc::<JwtConfig>::from_ref(state);
390 let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
391 status: StatusCode::UNAUTHORIZED,
392 code: "INVALID_TOKEN",
393 message: "Invalid or expired authentication token",
394 })?;
395
396 Ok(Authenticated {
397 user_id: claims.user_id,
398 method: AuthMethod::Jwt {
399 username: claims.username,
400 is_admin: claims.is_admin,
401 },
402 })
403 }
404}