use axum::http::{HeaderMap, header};
use jsonwebtoken::Algorithm;
use zeroize::Zeroizing;
use crate::{
error::{AuthError, Result},
jwt::JwtValidator,
};
pub struct SessionBearerAuthenticator {
validator: JwtValidator,
secret: Zeroizing<Vec<u8>>,
}
impl std::fmt::Debug for SessionBearerAuthenticator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionBearerAuthenticator").finish_non_exhaustive()
}
}
impl SessionBearerAuthenticator {
pub fn new(secret: Vec<u8>, issuer: &str, audience: &str) -> Result<Self> {
if audience.is_empty() {
return Err(AuthError::ConfigError {
message: "SessionBearerAuthenticator requires a non-empty audience: an unpinned \
validator accepts tokens minted for any service"
.to_string(),
});
}
let validator = JwtValidator::new(issuer, Algorithm::HS256)?.with_audiences(&[audience])?;
Ok(Self {
validator,
secret: Zeroizing::new(secret),
})
}
pub fn subject(&self, headers: &HeaderMap) -> Result<String> {
let raw =
headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.ok_or_else(|| AuthError::InvalidToken {
reason: "missing Authorization header".to_string(),
})?;
let token = raw
.split_once(' ')
.filter(|(scheme, _)| scheme.eq_ignore_ascii_case("bearer"))
.map(|(_, token)| token.trim())
.ok_or_else(|| AuthError::InvalidToken {
reason: "Authorization header is not a Bearer credential".to_string(),
})?;
let claims = self.validator.validate_hmac(token, &self.secret)?;
if claims.sub.is_empty() {
return Err(AuthError::InvalidToken {
reason: "session token carries an empty subject".to_string(),
});
}
Ok(claims.sub)
}
}