use std::sync::Arc;
use tokio::sync::Semaphore;
use tonic::{Status, metadata::MetadataMap};
use super::{AuthMode, Principal, TokenStore};
const AUTHORIZATION_HEADER: &str = "authorization";
const BEARER_PREFIX: &str = "Bearer ";
#[derive(Clone)]
pub struct AuthInterceptor {
mode: AuthMode,
tokens: TokenStore,
verification_semaphore: Arc<Semaphore>,
}
impl AuthInterceptor {
pub fn new(mode: AuthMode, tokens: TokenStore) -> Self {
if matches!(mode, AuthMode::BasicBearer { .. }) {
super::handshake::warm_unknown_user_timing_decoy();
}
let verification_semaphore = Arc::new(Semaphore::new(super::handshake::verification_permits()));
Self { mode, tokens, verification_semaphore }
}
pub fn mode(&self) -> &AuthMode {
&self.mode
}
pub fn tokens(&self) -> &TokenStore {
&self.tokens
}
pub(crate) fn verification_semaphore(&self) -> &Semaphore {
&self.verification_semaphore
}
pub fn check(&self, metadata: &MetadataMap) -> Result<Option<Principal>, Status> {
match &self.mode {
AuthMode::Disabled => Ok(None),
AuthMode::BasicBearer { .. } => {
let token = extract_bearer_token(metadata)?;
let principal = self
.tokens
.validate(&token)
.ok_or_else(|| Status::unauthenticated("invalid or expired bearer token"))?;
Ok(Some(principal))
},
}
}
}
fn extract_bearer_token(metadata: &MetadataMap) -> Result<String, Status> {
let header =
metadata.get(AUTHORIZATION_HEADER).ok_or_else(|| Status::unauthenticated("missing authorization header"))?;
let header = header.to_str().map_err(|_| Status::unauthenticated("authorization header is not ASCII"))?;
let token = header
.strip_prefix(BEARER_PREFIX)
.ok_or_else(|| Status::unauthenticated("authorization header must use Bearer scheme"))?;
if token.is_empty() {
return Err(Status::unauthenticated("bearer token is empty"));
}
Ok(token.to_string())
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use tonic::metadata::AsciiMetadataValue;
use super::*;
fn principal(name: &str) -> Principal {
Principal { username: name.to_string() }
}
fn metadata_with(value: Option<&str>) -> MetadataMap {
let mut md = MetadataMap::new();
if let Some(value) = value {
md.insert(AUTHORIZATION_HEADER, AsciiMetadataValue::try_from(value).expect("ascii"));
}
md
}
#[test]
fn disabled_mode_returns_no_principal() {
let interceptor = AuthInterceptor::new(AuthMode::Disabled, TokenStore::new());
assert!(interceptor.check(&metadata_with(None)).expect("disabled").is_none());
}
#[test]
fn valid_bearer_returns_principal() {
let tokens = TokenStore::new();
let token = tokens.issue(principal("alice"), Some(Duration::from_secs(60))).expect("issue under capacity");
let mode = AuthMode::BasicBearer { users: vec![], token_ttl_secs: 60 };
let interceptor = AuthInterceptor::new(mode, tokens);
let md = metadata_with(Some(&format!("Bearer {token}")));
let p = interceptor.check(&md).expect("valid token").expect("principal");
assert_eq!(p.username, "alice");
}
#[test]
fn invalid_bearer_rejects() {
let mode = AuthMode::BasicBearer { users: vec![], token_ttl_secs: 60 };
let interceptor = AuthInterceptor::new(mode, TokenStore::new());
let md = metadata_with(Some("Bearer not-a-real-token"));
let status = interceptor.check(&md).unwrap_err();
assert_eq!(status.code(), tonic::Code::Unauthenticated);
}
#[test]
fn missing_authorization_rejects() {
let mode = AuthMode::BasicBearer { users: vec![], token_ttl_secs: 60 };
let interceptor = AuthInterceptor::new(mode, TokenStore::new());
let status = interceptor.check(&metadata_with(None)).unwrap_err();
assert_eq!(status.code(), tonic::Code::Unauthenticated);
}
#[test]
fn wrong_scheme_rejects() {
let mode = AuthMode::BasicBearer { users: vec![], token_ttl_secs: 60 };
let interceptor = AuthInterceptor::new(mode, TokenStore::new());
let md = metadata_with(Some("Basic dXNlcjpwYXNz"));
let status = interceptor.check(&md).unwrap_err();
assert_eq!(status.code(), tonic::Code::Unauthenticated);
}
#[test]
fn empty_bearer_rejects() {
let mode = AuthMode::BasicBearer { users: vec![], token_ttl_secs: 60 };
let interceptor = AuthInterceptor::new(mode, TokenStore::new());
let md = metadata_with(Some("Bearer "));
let status = interceptor.check(&md).unwrap_err();
assert_eq!(status.code(), tonic::Code::Unauthenticated);
}
}