use crate::helpers::jwt::decode_token;
use actix_web::body::EitherBody;
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::{Error, HttpMessage, HttpResponse};
use arc_core::session::{SessionStore, SessionStoreError};
use futures_util::future::LocalBoxFuture;
use std::future::{ready, Ready};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
pub struct JwtMiddleware;
impl<S, B> Transform<S, ServiceRequest> for JwtMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type InitError = ();
type Transform = JwtCheck<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(JwtCheck { service }))
}
}
pub struct JwtCheck<S> {
service: S,
}
fn now_us() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_micros() as i64)
.unwrap_or(0)
}
fn legacy_grandfather_enabled() -> bool {
std::env::var("JWT_GRANDFATHER_LEGACY")
.map(|v| v == "true" || v == "1")
.unwrap_or(false)
}
fn unauthorized<B>(req: ServiceRequest, msg: &str) -> ServiceResponse<EitherBody<B>>
where
B: 'static,
{
let body = format!(r#"{{"error": "{}"}}"#, msg);
req.into_response(
HttpResponse::Unauthorized()
.content_type("application/json")
.body(body)
.map_into_right_body(),
)
}
#[allow(dead_code)]
fn service_unavailable<B>(req: ServiceRequest) -> ServiceResponse<EitherBody<B>>
where
B: 'static,
{
req.into_response(
HttpResponse::ServiceUnavailable()
.content_type("application/json")
.body(r#"{"error": "Authentication backend unavailable"}"#)
.map_into_right_body(),
)
}
impl<S, B> Service<ServiceRequest> for JwtCheck<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let auth_value = req.headers().get("Authorization");
let token = if let Some(auth) = auth_value {
auth.to_str()
.ok()
.and_then(|header| header.strip_prefix("Bearer "))
.map(str::to_string)
} else {
None
};
let token = match token {
Some(t) => t,
None => {
let resp = req.into_response(
HttpResponse::Unauthorized()
.content_type("application/json")
.body(r#"{"error": "Missing or invalid Authorization header. Expected: Bearer <token>"}"#)
.map_into_right_body(),
);
return Box::pin(async move { Ok(resp) });
}
};
let claims = match decode_token(&token) {
Ok(c) => c,
Err(_) => {
let resp = unauthorized(req, "Invalid or expired token");
return Box::pin(async move { Ok(resp) });
}
};
let actor_id = claims.sub.clone();
let jti_opt = claims.jti;
let store_opt = req
.app_data::<actix_web::web::Data<dyn SessionStore>>()
.cloned();
let fut = self
.service
.call(req_with_extensions(req, actor_id.clone(), jti_opt));
if let Some(store) = store_opt {
let jti = match jti_opt {
Some(j) => j,
None => {
if legacy_grandfather_enabled() {
tracing::warn!(reason = "legacy_jwt_no_jti", "accepting jwt without jti");
return Box::pin(async move {
fut.await.map(ServiceResponse::map_into_left_body)
});
} else {
return Box::pin(async move {
let r = fut.await?;
Ok(r.map_into_left_body())
});
}
}
};
return Box::pin(async move {
match store.is_valid(jti, now_us()).await {
Ok(true) => fut.await.map(ServiceResponse::map_into_left_body),
Ok(false) => {
Err(actix_web::error::ErrorUnauthorized("Session revoked"))
}
Err(SessionStoreError::Sink(e)) => {
tracing::error!(error = %e, "session store unavailable");
Err(actix_web::error::ErrorServiceUnavailable(
"Authentication backend unavailable",
))
}
Err(other) => {
tracing::error!(error = ?other, "session store error");
Err(actix_web::error::ErrorServiceUnavailable(
"Authentication backend unavailable",
))
}
}
});
}
Box::pin(async move { fut.await.map(ServiceResponse::map_into_left_body) })
}
}
fn req_with_extensions(req: ServiceRequest, actor_id: String, jti: Option<Uuid>) -> ServiceRequest {
{
let mut ext = req.extensions_mut();
ext.insert(actor_id);
if let Some(j) = jti {
ext.insert(j);
}
}
req
}
#[allow(dead_code)]
pub(crate) fn _arc_store_marker(_: &Arc<dyn SessionStore>) {}