Skip to main content

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}