use crate::cache::SharedCache;
use crate::security::types::{
serialize_auth_context, AuthConfigError, AuthContext, AuthMetadata, CacheNamespace,
};
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
use std::sync::Arc;
#[derive(Clone)]
pub struct BearerAuth {
secret: Vec<u8>,
valid_tokens: SharedCache,
blacklisted_tokens: SharedCache,
expected_audience: Option<String>,
expected_issuer: Option<String>,
}
impl BearerAuth {
pub fn new(secret: impl Into<String>) -> Self {
Self::try_new(secret).expect("Failed to create BearerAuth: invalid secret")
}
pub fn try_new(secret: impl Into<String>) -> Result<Self, AuthConfigError> {
let secret_str = secret.into();
if secret_str.len() < 32 {
return Err(AuthConfigError::SecretTooShort {
length: secret_str.len(),
});
}
if !secret_str.chars().any(|c| c.is_uppercase()) {
return Err(AuthConfigError::MissingCharacterClass {
required_type: "uppercase letter",
});
}
if !secret_str.chars().any(|c| c.is_lowercase()) {
return Err(AuthConfigError::MissingCharacterClass {
required_type: "lowercase letter",
});
}
if !secret_str.chars().any(|c| c.is_ascii_digit()) {
return Err(AuthConfigError::MissingCharacterClass {
required_type: "digit",
});
}
if !secret_str.chars().any(|c| !c.is_alphanumeric()) {
return Err(AuthConfigError::MissingCharacterClass {
required_type: "special character",
});
}
let cache = Arc::new(crate::cache::DashMapCache::new()) as SharedCache;
Ok(Self {
secret: secret_str.into_bytes(),
valid_tokens: cache.clone(),
blacklisted_tokens: cache,
expected_audience: None,
expected_issuer: None,
})
}
pub fn with_audience(secret: impl Into<String>, expected_audience: impl Into<String>) -> Self {
Self::try_new(secret)
.expect("Failed to create BearerAuth: invalid secret")
.with_audience_inner(expected_audience)
}
pub fn with_claims(
secret: impl Into<String>,
expected_audience: impl Into<String>,
expected_issuer: impl Into<String>,
) -> Self {
let mut auth = Self::try_new(secret).expect("Failed to create BearerAuth: invalid secret");
auth.expected_audience = Some(expected_audience.into());
auth.expected_issuer = Some(expected_issuer.into());
auth
}
fn with_audience_inner(mut self, expected_audience: impl Into<String>) -> Self {
self.expected_audience = Some(expected_audience.into());
self
}
pub fn with_dependencies(
secret: Vec<u8>,
valid_tokens: SharedCache,
blacklisted_tokens: SharedCache,
expected_audience: Option<String>,
expected_issuer: Option<String>,
) -> Self {
Self {
secret,
valid_tokens,
blacklisted_tokens,
expected_audience,
expected_issuer,
}
}
pub fn builder() -> BearerAuthBuilder {
BearerAuthBuilder::new()
}
#[cfg(feature = "security")]
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
use subtle::ConstantTimeEq;
a.ct_eq(b).into()
}
#[cfg(not(feature = "security"))]
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (byte_a, byte_b) in a.iter().zip(b.iter()) {
result |= byte_a ^ byte_b;
}
result == 0
}
fn base64url_decode(input: &str) -> Option<Vec<u8>> {
let mut table = [0u8; 256];
for (i, b) in b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
.iter()
.enumerate()
{
table[*b as usize] = i as u8;
}
let mut result = Vec::with_capacity(input.len() * 3 / 4);
let mut buffer = 0u32;
let mut bits = 0i32;
for c in input.bytes() {
if c == b'.' {
continue; }
if c == b' ' || c == b'\n' || c == b'\r' || c == b'\t' {
continue; }
let val = table.get(c as usize)?;
buffer = (buffer << 6) | (*val as u32);
bits += 6;
if bits >= 8 {
bits -= 8;
result.push((buffer >> bits) as u8);
}
}
Some(result)
}
fn verify_jwt(&self, token: &str) -> Option<serde_json::Value> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return None;
}
let header_bytes = Self::base64url_decode(parts[0])?;
let header_str = String::from_utf8_lossy(&header_bytes);
let header_value: serde_json::Value = serde_json::from_str(&header_str).ok()?;
let alg = header_value.get("alg").and_then(|v| v.as_str())?;
if alg != "HS256" {
return None;
}
let payload = Self::base64url_decode(parts[1])?;
let payload_str = String::from_utf8_lossy(&payload);
let payload_value: serde_json::Value = serde_json::from_str(&payload_str).ok()?;
let signature_input = format!("{}.{}", parts[0], parts[1]);
let mut mac = Hmac::<Sha256>::new_from_slice(&self.secret).ok()?;
mac.update(signature_input.as_bytes());
let expected_signature = mac.finalize().into_bytes();
let provided_signature = Self::base64url_decode(parts[2])?;
if provided_signature.len() != 32 {
return None;
}
if !Self::constant_time_eq(expected_signature.as_slice(), &provided_signature) {
return None;
}
if let Some(exp) = payload_value.get("exp").and_then(|v| v.as_i64()) {
if chrono::Utc::now().timestamp() > exp {
return None; }
}
if let Some(iat) = payload_value.get("iat").and_then(|v| v.as_i64()) {
let now = chrono::Utc::now().timestamp();
const CLOCK_SKEW_SECONDS: i64 = 60;
if iat > now + CLOCK_SKEW_SECONDS {
return None; }
}
if let Some(nbf) = payload_value.get("nbf").and_then(|v| v.as_i64()) {
if chrono::Utc::now().timestamp() < nbf {
return None; }
}
if let Some(expected_aud) = &self.expected_audience {
let token_aud = payload_value
.get("aud")
.and_then(|v| v.as_str())
.or_else(|| {
payload_value
.get("aud")
.and_then(|v| v.as_array())
.and_then(|arr| arr.first().and_then(|v| v.as_str()))
});
if token_aud != Some(expected_aud.as_str()) {
return None; }
}
if let Some(expected_iss) = &self.expected_issuer {
let token_iss = payload_value.get("iss").and_then(|v| v.as_str());
if token_iss != Some(expected_iss.as_str()) {
return None; }
}
Some(payload_value)
}
pub fn validate_token(&self, token: &str) -> Option<AuthContext> {
let blacklist_key = CacheNamespace::BearerBlacklist.key(token);
if self.blacklisted_tokens.get(&blacklist_key).is_some() {
return None; }
let payload = self.verify_jwt(token)?;
let user_id = payload
.get("sub")
.and_then(|v| v.as_str())
.map(String::from);
let permissions: Vec<String> = payload
.get("permissions")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|p| p.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
Some(AuthContext {
user_id,
permissions,
metadata: AuthMetadata::default(),
})
}
pub fn register_token(&self, token: String, context: AuthContext) {
let key = CacheNamespace::BearerValid.key(&token);
self.valid_tokens
.set(&key, serialize_auth_context(&context));
}
pub fn invalidate_token(&self, token: &str) {
let key = CacheNamespace::BearerBlacklist.key(token);
self.blacklisted_tokens.set(&key, vec![1u8]);
}
#[cfg(feature = "tokio")]
pub fn start_blacklist_cleanup(&self, interval: std::time::Duration) {
let _blacklisted = Arc::clone(&self.blacklisted_tokens);
tokio::spawn(async move {
let mut interval = tokio::time::interval(interval);
loop {
interval.tick().await;
}
});
}
}
#[derive(Debug, Clone, Default)]
pub struct BearerAuthBuilder {
secret: Option<String>,
audience: Option<String>,
issuer: Option<String>,
}
impl BearerAuthBuilder {
pub fn new() -> Self {
Self {
secret: None,
audience: None,
issuer: None,
}
}
pub fn secret(mut self, secret: impl Into<String>) -> Self {
self.secret = Some(secret.into());
self
}
pub fn audience(mut self, audience: impl Into<String>) -> Self {
self.audience = Some(audience.into());
self
}
pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
pub fn build(self) -> Result<BearerAuth, AuthConfigError> {
let secret = self
.secret
.ok_or_else(|| AuthConfigError::InvalidSecret("Secret is required".to_string()))?;
if secret.len() < 32 {
return Err(AuthConfigError::SecretTooShort {
length: secret.len(),
});
}
if !secret.chars().any(|c| c.is_uppercase()) {
return Err(AuthConfigError::MissingCharacterClass {
required_type: "uppercase letter",
});
}
if !secret.chars().any(|c| c.is_lowercase()) {
return Err(AuthConfigError::MissingCharacterClass {
required_type: "lowercase letter",
});
}
if !secret.chars().any(|c| c.is_ascii_digit()) {
return Err(AuthConfigError::MissingCharacterClass {
required_type: "digit",
});
}
if !secret.chars().any(|c| !c.is_alphanumeric()) {
return Err(AuthConfigError::MissingCharacterClass {
required_type: "special character",
});
}
Ok(BearerAuth {
secret: secret.into_bytes(),
valid_tokens: Arc::new(crate::cache::DashMapCache::new()),
blacklisted_tokens: Arc::new(crate::cache::DashMapCache::new()),
expected_audience: self.audience,
expected_issuer: self.issuer,
})
}
}
pub fn generate_secure_jwt_secret() -> String {
use base64::Engine;
use rand::Rng;
let mut bytes = [0u8; 32];
rand::rng().fill_bytes(&mut bytes);
base64::engine::general_purpose::STANDARD.encode(bytes)
}
#[cfg(test)]
mod tests;