1use tonic::metadata::MetadataValue;
9use tonic::service::Interceptor;
10use tonic::service::interceptor::InterceptedService;
11use tonic::transport::Channel;
12use tonic::{Request, Status};
13
14use crate::Result;
15use crate::config::Auth;
16
17pub type Intercepted = InterceptedService<Channel, AuthInterceptor>;
19
20#[derive(Clone, Default)]
22pub struct AuthInterceptor {
23 token: Option<String>,
24}
25
26impl std::fmt::Debug for AuthInterceptor {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("AuthInterceptor")
35 .field(
36 "token",
37 match &self.token {
38 Some(_) => &"Some(<redacted>)",
39 None => &"None",
40 },
41 )
42 .finish()
43 }
44}
45
46impl AuthInterceptor {
47 #[must_use]
49 pub fn new(token: Option<String>) -> Self {
50 Self { token }
51 }
52}
53
54impl Interceptor for AuthInterceptor {
55 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
56 if let Some(token) = &self.token {
57 let value: MetadataValue<_> = format!("Bearer {token}")
58 .parse()
59 .map_err(|_| Status::unauthenticated("invalid bearer token"))?;
60 request.metadata_mut().insert("authorization", value);
61 }
62 #[cfg(feature = "otel")]
64 crate::telemetry::otel::inject_trace_context_metadata(request.metadata_mut());
65 Ok(request)
66 }
67}
68
69pub async fn intercepted(channel: &Channel, auth: &Auth) -> Result<Intercepted> {
75 let token = auth.bearer().await?;
76 Ok(InterceptedService::new(
77 channel.clone(),
78 AuthInterceptor::new(token),
79 ))
80}
81
82#[cfg(test)]
83#[allow(clippy::unwrap_used)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn injects_bearer_header_when_token_present() {
89 let mut interceptor = AuthInterceptor::new(Some("tok-123".to_string()));
90 let req = interceptor.call(Request::new(())).unwrap();
91 let value = req.metadata().get("authorization").unwrap();
92 assert_eq!(value.to_str().unwrap(), "Bearer tok-123");
93 }
94
95 #[test]
96 fn no_header_when_token_absent() {
97 let mut interceptor = AuthInterceptor::new(None);
98 let req = interceptor.call(Request::new(())).unwrap();
99 assert!(req.metadata().get("authorization").is_none());
100 }
101
102 #[test]
103 fn rejects_a_token_with_illegal_header_bytes() {
104 let mut interceptor = AuthInterceptor::new(Some("bad\ntoken".to_string()));
105 let result = interceptor.call(Request::new(()));
106 assert!(result.is_err(), "a token with a newline must be rejected");
107 }
108
109 #[test]
112 fn debug_redacts_the_bearer_token() {
113 let token = "secret-bearer-token";
114 let rendered = format!("{:?}", AuthInterceptor::new(Some(token.to_string())));
115 assert!(!rendered.contains(token), "{rendered}");
116 assert!(rendered.contains("redacted"), "{rendered}");
117
118 let rendered = format!("{:?}", AuthInterceptor::new(None));
121 assert!(rendered.contains("None"), "{rendered}");
122 }
123}