136 lines
4.4 KiB
Rust
136 lines
4.4 KiB
Rust
//! Auth extractors for API handlers
|
|
//!
|
|
//! Provides the `CurrentUser` extractor that validates JWT Bearer tokens.
|
|
|
|
use axum::{extract::FromRequestParts, http::request::Parts};
|
|
use domain::User;
|
|
|
|
use crate::error::ApiError;
|
|
use crate::state::AppState;
|
|
|
|
/// Extracted current user from the request.
|
|
///
|
|
/// Validates a JWT Bearer token from the `Authorization` header.
|
|
pub struct CurrentUser(pub User);
|
|
|
|
impl FromRequestParts<AppState> for CurrentUser {
|
|
type Rejection = ApiError;
|
|
|
|
async fn from_request_parts(
|
|
parts: &mut Parts,
|
|
state: &AppState,
|
|
) -> Result<Self, Self::Rejection> {
|
|
#[cfg(feature = "auth-jwt")]
|
|
{
|
|
return match try_jwt_auth(parts, state).await {
|
|
Ok(user) => Ok(CurrentUser(user)),
|
|
Err(e) => Err(e),
|
|
};
|
|
}
|
|
|
|
#[cfg(not(feature = "auth-jwt"))]
|
|
{
|
|
let _ = (parts, state);
|
|
Err(ApiError::Unauthorized(
|
|
"No authentication backend configured".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Optional current user — returns None instead of error when auth is missing/invalid.
|
|
///
|
|
/// Checks `Authorization: Bearer <token>` first; falls back to `?token=<jwt>` query param
|
|
/// so IPTV clients and direct stream links work without custom headers.
|
|
pub struct OptionalCurrentUser(pub Option<User>);
|
|
|
|
impl FromRequestParts<AppState> for OptionalCurrentUser {
|
|
type Rejection = ApiError;
|
|
|
|
async fn from_request_parts(
|
|
parts: &mut Parts,
|
|
state: &AppState,
|
|
) -> Result<Self, Self::Rejection> {
|
|
#[cfg(feature = "auth-jwt")]
|
|
{
|
|
// Try Authorization header first
|
|
if let Ok(user) = try_jwt_auth(parts, state).await {
|
|
return Ok(OptionalCurrentUser(Some(user)));
|
|
}
|
|
// Fall back to ?token= query param
|
|
let query_token = parts.uri.query().and_then(|q| {
|
|
q.split('&')
|
|
.find(|seg| seg.starts_with("token="))
|
|
.map(|seg| seg[6..].to_owned())
|
|
});
|
|
if let Some(token) = query_token {
|
|
let user = validate_jwt_token(&token, state).await.ok();
|
|
return Ok(OptionalCurrentUser(user));
|
|
}
|
|
return Ok(OptionalCurrentUser(None));
|
|
}
|
|
|
|
#[cfg(not(feature = "auth-jwt"))]
|
|
{
|
|
let _ = (parts, state);
|
|
Ok(OptionalCurrentUser(None))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Authenticate using JWT Bearer token from the `Authorization` header.
|
|
#[cfg(feature = "auth-jwt")]
|
|
async fn try_jwt_auth(parts: &mut Parts, state: &AppState) -> Result<User, ApiError> {
|
|
use axum::http::header::AUTHORIZATION;
|
|
|
|
let auth_header = parts
|
|
.headers
|
|
.get(AUTHORIZATION)
|
|
.ok_or_else(|| ApiError::Unauthorized("Missing Authorization header".to_string()))?;
|
|
|
|
let auth_str = auth_header
|
|
.to_str()
|
|
.map_err(|_| ApiError::Unauthorized("Invalid Authorization header encoding".to_string()))?;
|
|
|
|
let token = auth_str.strip_prefix("Bearer ").ok_or_else(|| {
|
|
ApiError::Unauthorized("Authorization header must use Bearer scheme".to_string())
|
|
})?;
|
|
|
|
validate_jwt_token(token, state).await
|
|
}
|
|
|
|
/// Validate a raw JWT string and return the corresponding `User`.
|
|
#[cfg(feature = "auth-jwt")]
|
|
pub(crate) async fn validate_jwt_token(token: &str, state: &AppState) -> Result<User, ApiError> {
|
|
let validator = state
|
|
.jwt_validator
|
|
.as_ref()
|
|
.ok_or_else(|| ApiError::Internal("JWT validator not configured".to_string()))?;
|
|
|
|
let claims = validator.validate_token(token).map_err(|e| {
|
|
tracing::debug!("JWT validation failed: {:?}", e);
|
|
match e {
|
|
infra::auth::jwt::JwtError::Expired => {
|
|
ApiError::Unauthorized("Token expired".to_string())
|
|
}
|
|
infra::auth::jwt::JwtError::InvalidFormat => {
|
|
ApiError::Unauthorized("Invalid token format".to_string())
|
|
}
|
|
_ => ApiError::Unauthorized("Token validation failed".to_string()),
|
|
}
|
|
})?;
|
|
|
|
let user_id: uuid::Uuid = claims
|
|
.sub
|
|
.parse()
|
|
.map_err(|_| ApiError::Unauthorized("Invalid user ID in token".to_string()))?;
|
|
|
|
let user = state
|
|
.user_service
|
|
.find_by_id(user_id)
|
|
.await
|
|
.map_err(|e| ApiError::Internal(format!("Failed to fetch user: {}", e)))?;
|
|
|
|
Ok(user)
|
|
}
|