Files
Oxicloud/src/interfaces/middleware/auth.rs
T

181 lines
6.0 KiB
Rust
Raw Normal View History

2025-03-20 09:22:31 +01:00
use std::sync::Arc;
use axum::{
2026-02-07 04:02:38 +01:00
extract::{State, Request, FromRequestParts},
http::{StatusCode, HeaderMap, header, request::Parts},
2025-03-20 09:22:31 +01:00
middleware::Next,
response::{Response, IntoResponse},
};
use crate::common::di::AppState;
// Re-export CurrentUser from application layer for use in handlers
pub use crate::application::dtos::user_dto::CurrentUser;
2025-03-20 09:22:31 +01:00
// Structure for use in Axum extractors
#[derive(Clone, Debug)]
pub struct AuthUser {
pub id: String,
pub username: String,
}
/// Reusable extractor that gets the user_id of the authenticated user.
/// Automatically extracted from the `CurrentUser` inserted by the auth middleware.
2026-02-07 04:02:38 +01:00
///
/// Usage in handlers:
/// ```ignore
2026-02-07 04:02:38 +01:00
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
/// ```
#[derive(Clone, Debug)]
pub struct CurrentUserId(pub String);
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
2026-02-07 04:02:38 +01:00
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
parts
.extensions
.get::<CurrentUser>()
.map(|cu| AuthUser {
id: cu.id.clone(),
username: cu.username.clone(),
})
.ok_or(AuthError::UserNotFound)
}
}
// Implement FromRequestParts for CurrentUserId — lightweight extractor for user_id only
2026-02-07 04:02:38 +01:00
impl<S> FromRequestParts<S> for CurrentUserId
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
parts
.extensions
.get::<CurrentUser>()
.map(|cu| CurrentUserId(cu.id.clone()))
.ok_or(AuthError::UserNotFound)
}
}
// Error for authentication operations
2025-03-20 09:22:31 +01:00
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("Token not provided")]
2025-03-20 09:22:31 +01:00
TokenNotProvided,
#[error("Invalid token: {0}")]
2025-03-20 09:22:31 +01:00
InvalidToken(String),
#[error("Token expired")]
2025-03-20 09:22:31 +01:00
TokenExpired,
#[error("User not found")]
2025-03-20 09:22:31 +01:00
UserNotFound,
#[error("Access denied: {0}")]
2025-03-20 09:22:31 +01:00
AccessDenied(String),
#[error("Authentication service unavailable")]
AuthServiceUnavailable,
2025-03-20 09:22:31 +01:00
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token not provided".to_string()),
2025-03-20 09:22:31 +01:00
AuthError::InvalidToken(msg) => (StatusCode::UNAUTHORIZED, msg),
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired".to_string()),
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "User not found".to_string()),
2025-03-20 09:22:31 +01:00
AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg),
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Authentication service unavailable".to_string()),
2025-03-20 09:22:31 +01:00
};
let body = axum::Json(serde_json::json!({
"error": error_message
}));
(status, body).into_response()
}
}
/// Secure authentication middleware.
///
/// Validates the JWT token against the configured authentication service.
/// Does not accept bypasses, mock tokens, or URL parameters to skip validation.
2025-03-20 09:22:31 +01:00
pub async fn auth_middleware(
2026-02-03 17:59:04 +01:00
State(state): State<Arc<AppState>>,
2025-03-20 09:22:31 +01:00
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, AuthError> {
// Extract the Bearer token from the Authorization header
let token_str = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or(AuthError::TokenNotProvided)?;
2025-03-31 06:20:15 +02:00
// Validate that the token is not empty
let token_str = token_str.trim();
if token_str.is_empty() {
return Err(AuthError::TokenNotProvided);
2025-03-31 06:20:15 +02:00
}
tracing::debug!("Processing authentication token");
// Validate the token using the authentication service
if let Some(auth_service) = state.auth_service.as_ref() {
let token_service = &auth_service.token_service;
match token_service.validate_token(token_str) {
Ok(claims) => {
tracing::debug!("Token validated successfully for user: {}", claims.username);
2026-02-03 17:59:04 +01:00
let current_user = CurrentUser {
id: claims.sub,
username: claims.username,
email: claims.email,
role: claims.role,
2026-02-03 17:59:04 +01:00
};
request.extensions_mut().insert(current_user);
return Ok(next.run(request).await);
},
Err(e) => {
tracing::warn!("Token validation failed: {}", e);
return Err(AuthError::InvalidToken(format!("Invalid token: {}", e)));
2026-02-03 17:59:04 +01:00
}
}
2025-03-20 09:22:31 +01:00
}
// If no authentication service is available, deny access
tracing::error!("Auth middleware invoked but auth service is not configured");
Err(AuthError::AuthServiceUnavailable)
2025-03-20 09:22:31 +01:00
}
/// Middleware to verify that the authenticated user has an admin role.
///
/// Must be applied AFTER auth_middleware, as it depends on
/// `CurrentUser` being present in the request extensions.
2025-03-20 09:22:31 +01:00
pub async fn require_admin(
request: Request,
2025-03-20 09:22:31 +01:00
next: Next,
) -> Response {
// Get the CurrentUser inserted by auth_middleware
if let Some(current_user) = request.extensions().get::<CurrentUser>() {
if current_user.role == "admin" {
tracing::debug!("Admin access granted for user: {}", current_user.username);
return next.run(request).await;
2025-03-20 09:22:31 +01:00
}
tracing::warn!("Admin access denied for user: {} (role: {})", current_user.username, current_user.role);
} else {
tracing::warn!("Admin check failed: no authenticated user in request");
2025-03-20 09:22:31 +01:00
}
// Access denied
let error = AuthError::AccessDenied("Admin role required".to_string());
2025-03-20 09:22:31 +01:00
error.into_response()
}