1use std::future::Future;
21use std::marker::PhantomData;
22use std::pin::Pin;
23use std::task::{Context, Poll};
24
25use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
26use serde::{Deserialize, Serialize};
27use tower_layer::Layer;
28use tower_service::Service;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Claims {
35 pub sub: String,
37 pub exp: u64,
39 pub roles: Vec<String>,
41}
42
43#[derive(Debug, thiserror::Error)]
47pub enum AuthError {
48 #[error("missing authorization token")]
50 MissingToken,
51 #[error("invalid token: {0}")]
53 InvalidToken(String),
54 #[error("token has expired")]
56 Expired,
57 #[error("unauthorized")]
59 Unauthorized,
60}
61
62impl From<AuthError> for tonic::Status {
63 fn from(e: AuthError) -> tonic::Status {
64 tonic::Status::unauthenticated(e.to_string())
65 }
66}
67
68pub trait AuthValidator: Send + Sync {
80 fn validate<'a>(
85 &'a self,
86 token: &'a str,
87 ) -> Pin<Box<dyn Future<Output = Result<Claims, AuthError>> + Send + 'a>>;
88}
89
90#[derive(Clone)]
99pub struct BearerTokenValidator {
100 decoding_key: DecodingKey,
101 validation: Validation,
102}
103
104impl BearerTokenValidator {
105 pub fn new(secret: &[u8]) -> Self {
110 let mut validation = Validation::new(Algorithm::HS256);
111 validation.validate_aud = false;
114 validation.leeway = 0;
116 Self {
117 decoding_key: DecodingKey::from_secret(secret),
118 validation,
119 }
120 }
121}
122
123impl AuthValidator for BearerTokenValidator {
124 fn validate<'a>(
125 &'a self,
126 token: &'a str,
127 ) -> Pin<Box<dyn Future<Output = Result<Claims, AuthError>> + Send + 'a>> {
128 let jwt = match token.strip_prefix("Bearer ") {
130 Some(t) => t.to_owned(),
131 None => {
132 return Box::pin(async {
133 Err(AuthError::InvalidToken("not a Bearer token".to_string()))
134 });
135 }
136 };
137
138 let decoding_key = self.decoding_key.clone();
139 let validation = self.validation.clone();
140
141 Box::pin(async move {
142 match decode::<Claims>(&jwt, &decoding_key, &validation) {
143 Ok(token_data) => Ok(token_data.claims),
144 Err(e) => {
145 use jsonwebtoken::errors::ErrorKind;
146 match e.kind() {
147 ErrorKind::ExpiredSignature => Err(AuthError::Expired),
148 _ => Err(AuthError::InvalidToken(e.to_string())),
149 }
150 }
151 }
152 })
153 }
154}
155
156#[derive(Clone)]
163pub struct AuthMiddlewareLayer<V>
164where
165 V: AuthValidator + Clone + Send + Sync + 'static,
166{
167 validator: V,
168}
169
170impl<V> AuthMiddlewareLayer<V>
171where
172 V: AuthValidator + Clone + Send + Sync + 'static,
173{
174 pub fn new(validator: V) -> Self {
176 Self { validator }
177 }
178}
179
180impl<S, V> Layer<S> for AuthMiddlewareLayer<V>
181where
182 V: AuthValidator + Clone + Send + Sync + 'static,
183{
184 type Service = AuthMiddleware<S, V>;
185
186 fn layer(&self, inner: S) -> Self::Service {
187 AuthMiddleware {
188 inner,
189 validator: self.validator.clone(),
190 }
191 }
192}
193
194#[derive(Clone)]
206pub struct AuthMiddleware<S, V>
207where
208 V: AuthValidator + Clone + Send + Sync + 'static,
209{
210 inner: S,
211 validator: V,
212}
213
214impl<S, B, ResBody, V> Service<http::Request<B>> for AuthMiddleware<S, V>
215where
216 S: Service<http::Request<B>, Response = http::Response<ResBody>> + Clone + Send + 'static,
217 S::Future: Send + 'static,
218 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
219 B: Send + 'static,
220 ResBody: Default + Send + 'static,
221 V: AuthValidator + Clone + Send + Sync + 'static,
222{
223 type Response = http::Response<ResBody>;
224 type Error = S::Error;
225 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
226
227 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
228 self.inner.poll_ready(cx)
229 }
230
231 fn call(&mut self, mut req: http::Request<B>) -> Self::Future {
232 let token_result = req
234 .headers()
235 .get(http::header::AUTHORIZATION)
236 .and_then(|v| v.to_str().ok())
237 .map(|s| s.to_owned())
238 .ok_or(AuthError::MissingToken);
239
240 let validator = self.validator.clone();
241 let mut inner = self.inner.clone();
245 std::mem::swap(&mut self.inner, &mut inner);
246
247 Box::pin(async move {
248 let token = match token_result {
249 Ok(t) => t,
250 Err(e) => {
251 return Ok(build_unauthenticated_response(e.to_string()));
252 }
253 };
254
255 match validator.validate(&token).await {
256 Ok(claims) => {
257 req.extensions_mut().insert(claims);
258 inner.call(req).await
259 }
260 Err(e) => Ok(build_unauthenticated_response(e.to_string())),
261 }
262 })
263 }
264}
265
266fn build_unauthenticated_response<ResBody: Default>(message: String) -> http::Response<ResBody> {
271 let status = tonic::Status::unauthenticated(message);
272 let (mut parts, _body) = http::Response::new(ResBody::default()).into_parts();
273 parts.status = http::StatusCode::OK; parts.headers.insert(
276 "grpc-status",
277 http::HeaderValue::from_str(&(status.code() as i32).to_string())
278 .unwrap_or_else(|_| http::HeaderValue::from_static("16")),
279 );
280 if let Ok(v) = http::HeaderValue::from_str(status.message()) {
281 parts.headers.insert("grpc-message", v);
282 }
283 http::Response::from_parts(parts, ResBody::default())
284}
285
286#[cfg(test)]
289mod tests {
290 use super::*;
291 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
292 use std::time::{SystemTime, UNIX_EPOCH};
293 use tower_service::Service as _;
294
295 fn make_jwt(secret: &[u8], exp_offset_secs: i64) -> String {
298 let now = SystemTime::now()
299 .duration_since(UNIX_EPOCH)
300 .expect("system clock before UNIX epoch")
301 .as_secs();
302 let exp = if exp_offset_secs >= 0 {
303 now + exp_offset_secs as u64
304 } else {
305 now.saturating_sub(exp_offset_secs.unsigned_abs())
306 };
307 let claims = Claims {
308 sub: "test-user".to_string(),
309 exp,
310 roles: vec!["admin".to_string()],
311 };
312 encode(
313 &Header::new(Algorithm::HS256),
314 &claims,
315 &EncodingKey::from_secret(secret),
316 )
317 .expect("JWT encoding should not fail in tests")
318 }
319
320 #[derive(Clone)]
323 struct AlwaysOkValidator;
324
325 impl AuthValidator for AlwaysOkValidator {
326 fn validate<'a>(
327 &'a self,
328 _token: &'a str,
329 ) -> Pin<Box<dyn Future<Output = Result<Claims, AuthError>> + Send + 'a>> {
330 Box::pin(async {
331 Ok(Claims {
332 sub: "always-ok".to_string(),
333 exp: u64::MAX,
334 roles: vec![],
335 })
336 })
337 }
338 }
339
340 #[derive(Clone)]
343 struct EchoService;
344
345 impl Service<http::Request<String>> for EchoService {
346 type Response = http::Response<String>;
347 type Error = Box<dyn std::error::Error + Send + Sync>;
348 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
349
350 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
351 Poll::Ready(Ok(()))
352 }
353
354 fn call(&mut self, req: http::Request<String>) -> Self::Future {
355 let claims = req.extensions().get::<Claims>().cloned();
356 Box::pin(async move {
357 let body = match claims {
358 Some(c) => format!("sub={}", c.sub),
359 None => "no-claims".to_string(),
360 };
361 Ok(http::Response::new(body))
362 })
363 }
364 }
365
366 #[tokio::test]
369 async fn test_auth_rejects_missing_token() {
370 let layer = AuthMiddlewareLayer::new(AlwaysOkValidator);
371 let mut svc = layer.layer(EchoService);
372
373 let req = http::Request::builder()
374 .body(String::new())
375 .expect("request builder should not fail");
376
377 let resp = svc.call(req).await.expect("service call should not error");
378
379 let grpc_status = resp
381 .headers()
382 .get("grpc-status")
383 .and_then(|v| v.to_str().ok())
384 .unwrap_or("missing");
385 assert_eq!(
386 grpc_status, "16",
387 "expected grpc-status=16 (UNAUTHENTICATED)"
388 );
389 }
390
391 #[tokio::test]
392 async fn test_auth_accepts_valid_token() {
393 let secret = b"supersecret";
394 let jwt = make_jwt(secret, 3600);
395 let bearer = format!("Bearer {jwt}");
396
397 let layer = AuthMiddlewareLayer::new(BearerTokenValidator::new(secret));
398 let mut svc = layer.layer(EchoService);
399
400 let req = http::Request::builder()
401 .header(http::header::AUTHORIZATION, &bearer)
402 .body(String::new())
403 .expect("request builder should not fail");
404
405 let resp = svc.call(req).await.expect("service call should not error");
406
407 assert_eq!(resp.body(), "sub=test-user");
409 assert!(
411 resp.headers().get("grpc-status").is_none(),
412 "should not have grpc-status on success"
413 );
414 }
415
416 #[tokio::test]
417 async fn test_auth_custom_validator() {
418 let layer = AuthMiddlewareLayer::new(AlwaysOkValidator);
419 let mut svc = layer.layer(EchoService);
420
421 let req = http::Request::builder()
422 .header(http::header::AUTHORIZATION, "Bearer anything")
423 .body(String::new())
424 .expect("request builder should not fail");
425
426 let resp = svc.call(req).await.expect("service call should not error");
427
428 assert_eq!(resp.body(), "sub=always-ok");
429 }
430
431 #[tokio::test]
433 async fn test_bearer_validator_rejects_non_bearer_prefix() {
434 let validator = BearerTokenValidator::new(b"secret");
435 let result = validator.validate("Token abc123").await;
436 assert!(
437 matches!(result, Err(AuthError::InvalidToken(_))),
438 "expected InvalidToken for non-Bearer scheme"
439 );
440 }
441
442 #[tokio::test]
444 async fn test_bearer_validator_rejects_expired() {
445 let secret = b"expiry-test-secret";
446 let jwt = make_jwt(secret, -1); let bearer = format!("Bearer {jwt}");
448
449 let validator = BearerTokenValidator::new(secret);
450 let result = validator.validate(&bearer).await;
451 assert!(
452 matches!(result, Err(AuthError::Expired)),
453 "expected Expired error for expired JWT, got: {:?}",
454 result
455 );
456 }
457
458 #[test]
460 fn test_auth_validator_is_object_safe() {
461 let validator = BearerTokenValidator::new(b"test");
463 let _dyn_ref: &dyn AuthValidator = &validator;
464 }
465
466 #[test]
469 fn test_layer_construction() {
470 let _layer: AuthMiddlewareLayer<AlwaysOkValidator> =
471 AuthMiddlewareLayer::new(AlwaysOkValidator);
472 }
473}