use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use acdp_primitives::AcdpError;
pub type AuthFuture<'a> =
Pin<Box<dyn Future<Output = Result<Option<BearerToken>, AcdpError>> + Send + 'a>>;
#[derive(Clone, PartialEq, Eq)]
pub struct BearerToken(Arc<str>);
impl BearerToken {
pub fn new(token: impl Into<String>) -> Result<Self, AcdpError> {
let token = token.into();
let core = token.trim_end_matches('=');
let valid = !core.is_empty()
&& core.bytes().all(|b| {
b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'+' | b'/')
});
if !valid {
return Err(AcdpError::SchemaViolation(
"bearer token must be a non-empty RFC 6750 b64token".into(),
));
}
Ok(Self(token.into()))
}
#[must_use]
pub fn expose(&self) -> &str {
&self.0
}
}
impl fmt::Debug for BearerToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BearerToken(<redacted>)")
}
}
pub trait AuthProvider: Send + Sync {
fn bearer_token<'a>(&'a self, authority: &'a str) -> AuthFuture<'a>;
fn token_rejected(&self, authority: &str) {
let _ = authority;
}
}
impl fmt::Debug for dyn AuthProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("dyn AuthProvider")
}
}
#[derive(Debug, Clone)]
pub struct StaticBearerToken {
authority: String,
token: BearerToken,
}
impl StaticBearerToken {
#[must_use]
pub fn new(authority: impl Into<String>, token: BearerToken) -> Self {
Self {
authority: authority.into().to_ascii_lowercase(),
token,
}
}
}
impl AuthProvider for StaticBearerToken {
fn bearer_token<'a>(&'a self, authority: &'a str) -> AuthFuture<'a> {
let token = authority
.eq_ignore_ascii_case(&self.authority)
.then(|| self.token.clone());
Box::pin(async move { Ok(token) })
}
}
#[derive(Debug, Clone, Default)]
pub struct PerAuthorityTokens {
tokens: Arc<RwLock<HashMap<String, BearerToken>>>,
}
impl PerAuthorityTokens {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(self, authority: impl Into<String>, token: BearerToken) -> Self {
self.set(authority, token);
self
}
pub fn set(&self, authority: impl Into<String>, token: BearerToken) {
self.tokens
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(authority.into().to_ascii_lowercase(), token);
}
pub fn remove(&self, authority: &str) {
self.tokens
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&authority.to_ascii_lowercase());
}
}
impl AuthProvider for PerAuthorityTokens {
fn bearer_token<'a>(&'a self, authority: &'a str) -> AuthFuture<'a> {
let token = self
.tokens
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&authority.to_ascii_lowercase())
.cloned();
Box::pin(async move { Ok(token) })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tok(s: &str) -> BearerToken {
BearerToken::new(s).unwrap()
}
#[test]
fn token_debug_is_redacted() {
let t = tok("eyJhbGciOiJFZERTQSJ9.e30.c2ln");
assert_eq!(format!("{t:?}"), "BearerToken(<redacted>)");
let p = StaticBearerToken::new("r.example", t);
assert!(!format!("{p:?}").contains("eyJ"));
}
#[test]
fn token_rejects_header_injection_and_empty() {
assert!(BearerToken::new("").is_err());
assert!(BearerToken::new("abc\r\nX-Evil: 1").is_err());
assert!(BearerToken::new("a b").is_err());
assert!(
BearerToken::new("a=b").is_err(),
"'=' only as trailing padding"
);
assert!(BearerToken::new("abc==").is_ok());
assert!(BearerToken::new("a-b.c_d~e+f/g").is_ok());
}
#[tokio::test]
async fn static_token_is_scoped_to_its_authority() {
let p = StaticBearerToken::new("Registry.Example", tok("t1"));
assert_eq!(
p.bearer_token("registry.example").await.unwrap(),
Some(tok("t1"))
);
assert_eq!(p.bearer_token("other.example").await.unwrap(), None);
assert_eq!(
p.bearer_token("registry.example:8443").await.unwrap(),
None,
"a different port is a different authority"
);
}
#[tokio::test]
async fn per_authority_tokens_are_shared_and_replaceable() {
let p = PerAuthorityTokens::new().with("a.example", tok("ta"));
let reader = p.clone();
assert_eq!(
reader.bearer_token("a.example").await.unwrap(),
Some(tok("ta"))
);
assert_eq!(reader.bearer_token("b.example").await.unwrap(), None);
p.set("A.example", tok("ta2"));
assert_eq!(
reader.bearer_token("a.example").await.unwrap(),
Some(tok("ta2"))
);
p.remove("a.example");
assert_eq!(reader.bearer_token("a.example").await.unwrap(), None);
}
}