use axum::{
Extension,
extract::FromRequestParts,
http::{StatusCode, request::Parts},
response::IntoResponse,
};
use std::sync::Arc;
use crate::auth::{AuthenticatedPrincipal, IjimaAuth};
#[derive(Debug, Clone)]
pub struct AuthPrincipal(pub AuthenticatedPrincipal);
#[derive(Debug)]
pub enum AuthRejection {
Unauthorized(String),
#[cfg(feature = "rate-limit")]
RateLimited,
}
pub type AuthError = AuthRejection;
impl IntoResponse for AuthRejection {
fn into_response(self) -> axum::response::Response {
match self {
AuthRejection::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg).into_response(),
#[cfg(feature = "rate-limit")]
AuthRejection::RateLimited => {
(StatusCode::TOO_MANY_REQUESTS, "rate limit exceeded").into_response()
}
}
}
}
impl<S> FromRequestParts<S> for AuthPrincipal
where
S: Send + Sync,
{
type Rejection = AuthRejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let Extension(auth): Extension<Arc<IjimaAuth>> =
Extension::from_request_parts(parts, _state)
.await
.map_err(|_| AuthRejection::Unauthorized("auth state not installed".into()))?;
let header = parts
.headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.ok_or(AuthRejection::Unauthorized(
"missing Authorization header".into(),
))?;
let token = header
.strip_prefix("Bearer ")
.ok_or(AuthRejection::Unauthorized(
"expected 'Bearer <token>'".into(),
))?;
let principal = auth.verify_bearer(token).map_err(|e| {
AuthRejection::Unauthorized(e.to_string())
})?;
#[cfg(feature = "rate-limit")]
{
if let Some(rl) = parts.extensions.get::<crate::rate_limit::RateLimitState>() {
crate::rate_limit::consume(rl, &principal)
.map_err(|_| AuthRejection::RateLimited)?;
}
}
Ok(AuthPrincipal(principal))
}
}
#[cfg(test)]
mod tests {
use super::*;
use ijima_core::capabilities::MEMORY_READ;
fn build_request(bearer: Option<String>) -> (Parts, ()) {
let mut builder = axum::http::Request::<()>::builder();
if let Some(b) = bearer {
builder = builder.header("authorization", format!("Bearer {b}"));
}
let req = builder.body(()).unwrap();
req.into_parts()
}
#[tokio::test]
async fn valid_bearer_header_yields_principal() {
let auth = Arc::new(IjimaAuth::from_embedded_policy().expect("policy must load"));
let bearer = auth.issue_bearer("elliott", MEMORY_READ).expect("issue");
let (mut parts, ()) = build_request(Some(bearer));
parts.extensions.insert(auth.clone());
let state: () = ();
let got = AuthPrincipal::from_request_parts(&mut parts, &state)
.await
.expect("must extract");
assert_eq!(got.0.principal.as_str(), "elliott");
assert!(got.0.may(MEMORY_READ));
}
#[tokio::test]
async fn missing_header_is_401() {
let auth = Arc::new(IjimaAuth::from_embedded_policy().expect("policy must load"));
let (mut parts, ()) = build_request(None);
parts.extensions.insert(auth);
let state: () = ();
let err = AuthPrincipal::from_request_parts(&mut parts, &state)
.await
.expect_err("must reject");
assert_eq!(err.into_response().status(), StatusCode::UNAUTHORIZED);
}
}