use subtle::ConstantTimeEq as _;
use rusty_gasket::auth::backend::{AuthBackend, extract_bearer_token};
use rusty_gasket::auth::error::AuthError;
use rusty_gasket::auth::identity::Identity;
pub struct StaticBearerBackend {
token: secrecy::SecretString,
subject: &'static str,
service_account: bool,
privileged: bool,
}
impl std::fmt::Debug for StaticBearerBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StaticBearerBackend")
.field("subject", &self.subject)
.field("service_account", &self.service_account)
.field("privileged", &self.privileged)
.finish_non_exhaustive()
}
}
impl StaticBearerBackend {
const DEFAULT_SUBJECT: &'static str = "static-bearer";
const AUTH_METHOD: &'static str = "static-bearer";
#[must_use]
pub fn new(token: impl Into<String>) -> Self {
Self {
token: secrecy::SecretString::from(token.into()),
subject: Self::DEFAULT_SUBJECT,
service_account: false,
privileged: false,
}
}
#[must_use]
pub const fn subject(mut self, subject: &'static str) -> Self {
self.subject = subject;
self
}
#[must_use]
pub const fn service_account(mut self, service_account: bool) -> Self {
self.service_account = service_account;
self
}
#[must_use]
pub const fn privileged(mut self, privileged: bool) -> Self {
self.privileged = privileged;
self
}
fn identity(&self) -> Identity {
Identity::builder(self.subject, Self::AUTH_METHOD)
.service_account(self.service_account)
.privileged(self.privileged)
.build()
}
}
impl AuthBackend for StaticBearerBackend {
fn name(&self) -> &'static str {
"static-bearer"
}
async fn authenticate(
&self,
headers: &http::HeaderMap,
_uri: &http::Uri,
) -> Result<Option<Identity>, AuthError> {
let Some(header) = headers.get(http::header::AUTHORIZATION) else {
return Ok(None);
};
let Some(token) = header.to_str().ok().and_then(extract_bearer_token) else {
return Ok(None);
};
use secrecy::ExposeSecret as _;
let expected = self.token.expose_secret().as_bytes();
let presented = token.as_bytes();
let matches = presented.len() == expected.len() && presented.ct_eq(expected).into();
if matches {
Ok(Some(self.identity()))
} else {
Err(AuthError::InvalidCredentials(
"static bearer token mismatch".to_string(),
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bearer(value: &str) -> http::HeaderMap {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::AUTHORIZATION,
format!("Bearer {value}")
.parse()
.expect("valid header value"),
);
headers
}
fn uri() -> http::Uri {
"/test".parse().expect("valid uri")
}
#[tokio::test]
async fn correct_token_authenticates() {
let backend = StaticBearerBackend::new("correct-horse");
let identity = backend
.authenticate(&bearer("correct-horse"), &uri())
.await
.expect("authentication should not error")
.expect("a matching token yields an identity");
assert_eq!(identity.subject(), "static-bearer");
assert_eq!(identity.auth_method(), "static-bearer");
assert!(!identity.is_service_account());
assert!(!identity.is_privileged());
}
#[tokio::test]
async fn flags_and_subject_propagate() {
let backend = StaticBearerBackend::new("token")
.subject("push-bot")
.service_account(true)
.privileged(true);
let identity = backend
.authenticate(&bearer("token"), &uri())
.await
.expect("authentication should not error")
.expect("a matching token yields an identity");
assert_eq!(identity.subject(), "push-bot");
assert_eq!(identity.auth_method(), "static-bearer");
assert!(identity.is_service_account());
assert!(identity.is_privileged());
}
#[tokio::test]
async fn wrong_token_is_invalid_credentials() {
let backend = StaticBearerBackend::new("correct");
let err = backend
.authenticate(&bearer("wrong"), &uri())
.await
.expect_err("a non-matching token must be a definitive failure");
assert!(matches!(err, AuthError::InvalidCredentials(_)));
}
#[tokio::test]
async fn missing_authorization_defers() {
let backend = StaticBearerBackend::new("correct");
let result = backend
.authenticate(&http::HeaderMap::new(), &uri())
.await
.expect("a missing header must not error");
assert!(result.is_none(), "no header → defer to next backend");
}
#[tokio::test]
async fn non_bearer_scheme_defers() {
let backend = StaticBearerBackend::new("correct");
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::AUTHORIZATION,
"Basic correct".parse().expect("valid header value"),
);
let result = backend
.authenticate(&headers, &uri())
.await
.expect("a non-Bearer scheme must not error");
assert!(
result.is_none(),
"non-Bearer scheme → defer to next backend"
);
}
#[tokio::test]
async fn empty_presented_token_is_invalid() {
let backend = StaticBearerBackend::new("correct");
let err = backend
.authenticate(&bearer(""), &uri())
.await
.expect_err("empty presented token cannot match a non-empty secret");
assert!(matches!(err, AuthError::InvalidCredentials(_)));
}
#[tokio::test]
async fn empty_secret_matches_empty_presented_token() {
let backend = StaticBearerBackend::new("");
let identity = backend
.authenticate(&bearer(""), &uri())
.await
.expect("authentication should not error")
.expect("empty secret matches empty presented token");
assert_eq!(identity.subject(), "static-bearer");
}
#[tokio::test]
async fn garbage_token_is_invalid_without_panic() {
let backend = StaticBearerBackend::new("correct");
let err = backend
.authenticate(&bearer("!@#$%^&*()_+-=[]{};':\""), &uri())
.await
.expect_err("garbage token must be a definitive failure");
assert!(matches!(err, AuthError::InvalidCredentials(_)));
}
#[tokio::test]
async fn non_ascii_header_defers_without_panic() {
let backend = StaticBearerBackend::new("correct");
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::AUTHORIZATION,
http::HeaderValue::from_bytes("Bearer 日本語トークン".as_bytes())
.expect("non-ASCII bytes are a valid header value"),
);
let result = backend
.authenticate(&headers, &uri())
.await
.expect("a non-ASCII header must not error");
assert!(result.is_none(), "non-ASCII header → defer to next backend");
}
#[tokio::test]
async fn long_ascii_secret_matches_itself() {
let secret = "x".repeat(512);
let backend = StaticBearerBackend::new(secret.clone());
let identity = backend
.authenticate(&bearer(&secret), &uri())
.await
.expect("authentication should not error")
.expect("a matching long token yields an identity");
assert_eq!(identity.auth_method(), "static-bearer");
}
#[tokio::test]
async fn prefix_of_secret_is_invalid() {
let backend = StaticBearerBackend::new("correct-token");
let err = backend
.authenticate(&bearer("correct"), &uri())
.await
.expect_err("a prefix of the secret must not authenticate");
assert!(matches!(err, AuthError::InvalidCredentials(_)));
}
#[test]
fn name_is_stable() {
assert_eq!(StaticBearerBackend::new("x").name(), "static-bearer");
}
#[test]
fn debug_does_not_leak_secret() {
let backend = StaticBearerBackend::new("super-secret-value");
let rendered = format!("{backend:?}");
assert!(
!rendered.contains("super-secret-value"),
"Debug output must not contain the secret: {rendered}"
);
}
}