rust_grpc_lib/auth/layer.rs
1//! Server-side JWT validation layer for tonic gRPC services.
2//!
3//! Defines two types and one constructor:
4//!
5//! - [`JwtValidationService`] — the [`tonic::service::Interceptor`] that
6//! validates `Authorization: Bearer` headers on every incoming request.
7//! - [`JwtValidationLayer`] — a type alias for
8//! `InterceptorLayer<JwtValidationService<V>>`.
9//! - [`validator_into_layer`] — constructs a [`JwtValidationLayer`] from any
10//! [`TokenValidator`].
11//!
12//! On successful validation, [`KeycloakClaims`](crate::auth::KeycloakClaims) are inserted into the request's
13//! extensions so handler methods can retrieve them via
14//! `request.extensions().get::<KeycloakClaims>()`. On failure the request is
15//! rejected with [`tonic::Code::Unauthenticated`] before reaching any handler.
16
17use std::marker::PhantomData;
18use std::sync::Arc;
19
20use rust_auth_lib::{Claims, TokenValidator};
21use tonic::{
22 Request, Status,
23 service::{Interceptor, InterceptorLayer},
24};
25
26#[cfg(test)]
27mod tests;
28
29/// An [`InterceptorLayer`] that installs JWT validation on a tonic server.
30///
31/// Wrap your tonic server with this layer to enforce that every incoming gRPC
32/// request carries a valid `Authorization: Bearer <token>` header. Requests
33/// that fail validation are rejected with [`tonic::Code::Unauthenticated`]
34/// before reaching any handler.
35///
36/// `JwtValidationLayer` is a type alias for
37/// `tonic::service::InterceptorLayer<JwtValidationService<V>>`. Because it is
38/// a type alias, use the free function [`validator_into_layer`] to construct it.
39///
40/// # Example
41///
42/// ```rust,ignore
43/// use std::sync::Arc;
44/// use rust_grpc_lib::auth::{
45/// validator_into_layer, KeycloakClaims, KeyValidator, KeyValidatorConfig,
46/// };
47///
48/// #[tokio::main]
49/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
50/// // Reads AUTH_JWKS_FILE or AUTH_PEM_FILE; optionally AUTH_ISSUER
51/// let validator = Arc::new(KeyValidator::new(KeyValidatorConfig::from_env()?)?);
52///
53/// tonic::transport::Server::builder()
54/// .layer(validator_into_layer::<KeycloakClaims, _>(validator))
55/// .add_service(MyServiceServer::new(MyService))
56/// .serve("[::1]:50051".parse()?)
57/// .await?;
58///
59/// Ok(())
60/// }
61/// ```
62pub type JwtValidationLayer<C, V> = InterceptorLayer<JwtValidationService<C, V>>;
63
64pub fn validator_into_layer<C: Claims, V: TokenValidator>(
65 validator: Arc<V>,
66) -> JwtValidationLayer<C, V> {
67 InterceptorLayer::new(JwtValidationService {
68 _claims: PhantomData,
69 validator,
70 })
71}
72
73/// The server-side interceptor that validates `Authorization: Bearer <token>` on
74/// every incoming gRPC request.
75///
76/// `JwtValidationService` is the inner interceptor type used by
77/// [`JwtValidationLayer`]. You do not typically construct it directly; use
78/// [`validator_into_layer`] instead.
79///
80/// On success, the validated [`KeycloakClaims`](crate::auth::KeycloakClaims) are inserted into the request's
81/// extensions so that handler methods can retrieve them:
82///
83/// ```rust,ignore
84/// fn my_handler(&self, req: Request<MyRequest>) -> Result<Response<MyResponse>, Status> {
85/// let claims = req.extensions().get::<KeycloakClaims>()
86/// .ok_or_else(|| Status::unauthenticated("missing claims"))?;
87/// // claims.has_role("admin"), claims.subject(), etc.
88/// }
89/// ```
90///
91/// On failure (missing header, expired token, wrong issuer, bad signature),
92/// returns [`tonic::Status::unauthenticated`] immediately, which tonic converts
93/// into a proper gRPC error response before the handler is ever called.
94#[derive(Clone)]
95pub struct JwtValidationService<C, V> {
96 _claims: PhantomData<C>,
97 validator: Arc<V>,
98}
99
100impl<C: Claims + Clone, V: TokenValidator + Send + Sync + 'static> Interceptor
101 for JwtValidationService<C, V>
102{
103 fn call(&mut self, mut req: Request<()>) -> Result<Request<()>, Status> {
104 // Extract the Bearer token from the Authorization metadata entry.
105 let token = req
106 .metadata()
107 .get("authorization")
108 .and_then(|v| v.to_str().ok())
109 .and_then(|s| s.strip_prefix("Bearer "))
110 .map(|s| s.to_owned());
111
112 let token = match token {
113 Some(t) => t,
114 None => {
115 return Err(Status::unauthenticated("missing Authorization header"));
116 }
117 };
118
119 // The Interceptor trait is synchronous; drive the async validator with
120 // block_in_place so we don't block the executor thread.
121 let claims = self
122 .validator
123 .validate::<C>(&token)
124 .map_err(|e| Status::unauthenticated(e.to_string()))?;
125
126 req.extensions_mut().insert(claims);
127 Ok(req)
128 }
129}