use std::sync::Arc;
use axum::Json;
use axum::extract::{FromRef, FromRequestParts};
use axum::http::StatusCode;
use axum::http::request::Parts;
use axum::response::{IntoResponse, Response};
use axum_extra::extract::cookie::CookieJar;
use chrono::Utc;
use ironflow_store::api_key_store::ApiKeyStore;
use ironflow_store::entities::ApiKeyScope;
use ironflow_store::user_store::UserStore;
use serde_json::json;
use uuid::Uuid;
use crate::cookies::AUTH_COOKIE_NAME;
use crate::jwt::{AccessToken, JwtConfig};
use crate::password;
#[derive(Debug, Clone)]
pub struct AuthenticatedUser {
pub user_id: Uuid,
pub username: String,
pub is_admin: bool,
}
impl<S> FromRequestParts<S> for AuthenticatedUser
where
S: Send + Sync,
Arc<JwtConfig>: FromRef<S>,
{
type Rejection = AuthRejection;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let jwt_config = Arc::<JwtConfig>::from_ref(state);
let jar = CookieJar::from_headers(&parts.headers);
let token = jar
.get(AUTH_COOKIE_NAME)
.map(|c| c.value().to_string())
.or_else(|| {
parts
.headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(|t| t.to_string())
});
let token = token.ok_or(AuthRejection {
status: StatusCode::UNAUTHORIZED,
code: "MISSING_TOKEN",
message: "No authentication token provided",
})?;
let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
status: StatusCode::UNAUTHORIZED,
code: "INVALID_TOKEN",
message: "Invalid or expired authentication token",
})?;
Ok(AuthenticatedUser {
user_id: claims.user_id,
username: claims.username,
is_admin: claims.is_admin,
})
}
}
pub struct AuthRejection {
status: StatusCode,
code: &'static str,
message: &'static str,
}
impl IntoResponse for AuthRejection {
fn into_response(self) -> Response {
let body = json!({
"error": {
"code": self.code,
"message": self.message,
}
});
(self.status, Json(body)).into_response()
}
}
pub const API_KEY_PREFIX: &str = "irfl_";
#[derive(Debug, Clone)]
pub struct ApiKeyAuth {
pub key_id: Uuid,
pub user_id: Uuid,
pub key_name: String,
pub scopes: Vec<ApiKeyScope>,
pub owner_is_admin: bool,
}
impl ApiKeyAuth {
pub fn has_scope(&self, required: &ApiKeyScope) -> bool {
ApiKeyScope::has_permission(&self.scopes, required)
}
}
pub struct ApiKeyRejection {
status: StatusCode,
code: &'static str,
message: &'static str,
}
impl IntoResponse for ApiKeyRejection {
fn into_response(self) -> Response {
let body = json!({
"error": {
"code": self.code,
"message": self.message,
}
});
(self.status, Json(body)).into_response()
}
}
impl<S> FromRequestParts<S> for ApiKeyAuth
where
S: Send + Sync,
Arc<dyn ApiKeyStore>: FromRef<S>,
Arc<dyn UserStore>: FromRef<S>,
{
type Rejection = ApiKeyRejection;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let api_key_store = Arc::<dyn ApiKeyStore>::from_ref(state);
let user_store = Arc::<dyn UserStore>::from_ref(state);
let token = parts
.headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(ApiKeyRejection {
status: StatusCode::UNAUTHORIZED,
code: "MISSING_TOKEN",
message: "No authentication token provided",
})?;
if !token.starts_with(API_KEY_PREFIX) {
return Err(ApiKeyRejection {
status: StatusCode::UNAUTHORIZED,
code: "INVALID_TOKEN",
message: "Expected API key (irfl_...) in Authorization header",
});
}
let suffix_len = (token.len() - API_KEY_PREFIX.len()).min(8);
let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
let api_key = api_key_store
.find_api_key_by_prefix(prefix)
.await
.map_err(|_| ApiKeyRejection {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: "INTERNAL_ERROR",
message: "Failed to look up API key",
})?
.ok_or(ApiKeyRejection {
status: StatusCode::UNAUTHORIZED,
code: "INVALID_TOKEN",
message: "Invalid API key",
})?;
if !api_key.is_active {
return Err(ApiKeyRejection {
status: StatusCode::UNAUTHORIZED,
code: "KEY_DISABLED",
message: "API key is disabled",
});
}
if let Some(expires_at) = api_key.expires_at
&& expires_at < Utc::now()
{
return Err(ApiKeyRejection {
status: StatusCode::UNAUTHORIZED,
code: "KEY_EXPIRED",
message: "API key has expired",
});
}
let valid = password::verify(token, &api_key.key_hash).map_err(|_| ApiKeyRejection {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: "INTERNAL_ERROR",
message: "Failed to verify API key",
})?;
if !valid {
return Err(ApiKeyRejection {
status: StatusCode::UNAUTHORIZED,
code: "INVALID_TOKEN",
message: "Invalid API key",
});
}
let _ = api_key_store.touch_api_key(api_key.id).await;
let owner = user_store
.find_user_by_id(api_key.user_id)
.await
.map_err(|_| ApiKeyRejection {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: "INTERNAL_ERROR",
message: "Failed to look up API key owner",
})?;
let owner_is_admin = owner.map(|u| u.is_admin).unwrap_or(false);
Ok(ApiKeyAuth {
key_id: api_key.id,
user_id: api_key.user_id,
key_name: api_key.name,
scopes: api_key.scopes,
owner_is_admin,
})
}
}
#[derive(Debug, Clone)]
pub struct Authenticated {
pub user_id: Uuid,
pub method: AuthMethod,
}
#[derive(Debug, Clone)]
pub enum AuthMethod {
Jwt {
username: String,
is_admin: bool,
},
ApiKey {
key_id: Uuid,
key_name: String,
scopes: Vec<ApiKeyScope>,
owner_is_admin: bool,
},
}
impl Authenticated {
pub fn is_admin(&self) -> bool {
match &self.method {
AuthMethod::Jwt { is_admin, .. } => *is_admin,
AuthMethod::ApiKey { owner_is_admin, .. } => *owner_is_admin,
}
}
}
impl<S> FromRequestParts<S> for Authenticated
where
S: Send + Sync,
Arc<JwtConfig>: FromRef<S>,
Arc<dyn ApiKeyStore>: FromRef<S>,
Arc<dyn UserStore>: FromRef<S>,
{
type Rejection = AuthRejection;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let jar = CookieJar::from_headers(&parts.headers);
let cookie_token = jar.get(AUTH_COOKIE_NAME).map(|c| c.value().to_string());
let header_token = parts
.headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(|t| t.to_string());
if let Some(ref token) = header_token
&& token.starts_with(API_KEY_PREFIX)
{
let api_key_auth =
ApiKeyAuth::from_request_parts(parts, state)
.await
.map_err(|_| AuthRejection {
status: StatusCode::UNAUTHORIZED,
code: "INVALID_TOKEN",
message: "Invalid or expired authentication token",
})?;
return Ok(Authenticated {
user_id: api_key_auth.user_id,
method: AuthMethod::ApiKey {
key_id: api_key_auth.key_id,
key_name: api_key_auth.key_name,
scopes: api_key_auth.scopes,
owner_is_admin: api_key_auth.owner_is_admin,
},
});
}
let token = cookie_token.or(header_token).ok_or(AuthRejection {
status: StatusCode::UNAUTHORIZED,
code: "MISSING_TOKEN",
message: "No authentication token provided",
})?;
let jwt_config = Arc::<JwtConfig>::from_ref(state);
let claims = AccessToken::decode(&token, &jwt_config).map_err(|_| AuthRejection {
status: StatusCode::UNAUTHORIZED,
code: "INVALID_TOKEN",
message: "Invalid or expired authentication token",
})?;
Ok(Authenticated {
user_id: claims.user_id,
method: AuthMethod::Jwt {
username: claims.username,
is_admin: claims.is_admin,
},
})
}
}