use tonic::metadata::MetadataValue;
use tonic::service::Interceptor;
use tonic::service::interceptor::InterceptedService;
use tonic::transport::Channel;
use tonic::{Request, Status};
use crate::Result;
use crate::config::Auth;
pub type Intercepted = InterceptedService<Channel, AuthInterceptor>;
#[derive(Clone, Default)]
pub struct AuthInterceptor {
token: Option<String>,
}
impl std::fmt::Debug for AuthInterceptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthInterceptor")
.field(
"token",
match &self.token {
Some(_) => &"Some(<redacted>)",
None => &"None",
},
)
.finish()
}
}
impl AuthInterceptor {
#[must_use]
pub fn new(token: Option<String>) -> Self {
Self { token }
}
}
impl Interceptor for AuthInterceptor {
fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
if let Some(token) = &self.token {
let value: MetadataValue<_> = format!("Bearer {token}")
.parse()
.map_err(|_| Status::unauthenticated("invalid bearer token"))?;
request.metadata_mut().insert("authorization", value);
}
#[cfg(feature = "otel")]
crate::telemetry::otel::inject_trace_context_metadata(request.metadata_mut());
Ok(request)
}
}
pub async fn intercepted(channel: &Channel, auth: &Auth) -> Result<Intercepted> {
let token = auth.bearer().await?;
Ok(InterceptedService::new(
channel.clone(),
AuthInterceptor::new(token),
))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn injects_bearer_header_when_token_present() {
let mut interceptor = AuthInterceptor::new(Some("tok-123".to_string()));
let req = interceptor.call(Request::new(())).unwrap();
let value = req.metadata().get("authorization").unwrap();
assert_eq!(value.to_str().unwrap(), "Bearer tok-123");
}
#[test]
fn no_header_when_token_absent() {
let mut interceptor = AuthInterceptor::new(None);
let req = interceptor.call(Request::new(())).unwrap();
assert!(req.metadata().get("authorization").is_none());
}
#[test]
fn rejects_a_token_with_illegal_header_bytes() {
let mut interceptor = AuthInterceptor::new(Some("bad\ntoken".to_string()));
let result = interceptor.call(Request::new(()));
assert!(result.is_err(), "a token with a newline must be rejected");
}
#[test]
fn debug_redacts_the_bearer_token() {
let token = "secret-bearer-token";
let rendered = format!("{:?}", AuthInterceptor::new(Some(token.to_string())));
assert!(!rendered.contains(token), "{rendered}");
assert!(rendered.contains("redacted"), "{rendered}");
let rendered = format!("{:?}", AuthInterceptor::new(None));
assert!(rendered.contains("None"), "{rendered}");
}
}