axum_jose/lib.rs
1//! Lightweight authorization [middleware for `axum`](https://docs.rs/axum/latest/axum/middleware/index.html),
2//! following [JSON Object Signing and Encryption (JOSE)](https://datatracker.ietf.org/wg/jose/charter/) standards.
3//!
4//! # Overview
5//!
6//! This crate provides a [`tower`](https://docs.rs/tower)-based [`AuthorizationLayer`] that integrates with `axum`'s
7//! middleware system to add JWT-based authorization to your application. The middleware validates JWTs from incoming
8//! requests against JWK (JSON Web Key) sets, which can be either provided locally or fetched from remote identity providers.
9//!
10//! # Quickstart
11//!
12//! This example illustrates how to validate JWTs against a remote JWK set provided e.g. by your OpenID Connect
13//! provider.
14//!
15//! It also shows how to enable caching using the [`RemoteJwkSetBuilder::with_cache`] method to avoid fetching the JWK
16//! set on every request. Choose a TTL that balances responsiveness to key rotation with provider
17//! load. Shorter TTLs react faster to key rotation, while longer TTLs reduce requests to your identity provider.
18//!
19//! Note though, that the cache is not only invalidated when reaching its TTL but also when a JWT with an unknown `kid`
20//! (key ID) is encountered. Therefore, in addition to caching, consider configuring rate limiting using
21//! [`RemoteJwkSetBuilder::with_rate_limit`] to prevent running into your identity provider's server-side rate limits.
22//!
23//! ```rust,no_run
24//! use axum::{routing::get, Router};
25//! use axum_jose::{AuthorizationLayer, RemoteJwkSet};
26//! use std::num::NonZero;
27//! use std::time::Duration;
28//! use url::Url;
29//!
30//! #[tokio::main]
31//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
32//! // Configure a `RemoteJwkSet` to fetch the JWK set e.g. from your OpenID
33//! // Connect provider which typically exposes JWK sets at a
34//! // `.well-known/jwks.json` endpoint
35//! let remote_jwk_set = RemoteJwkSet::builder(Url::parse(
36//! "https://your.oidc.provider/.well-known/jwks.json",
37//! )?)
38//! // Use caching to avoid fetching the JWK set on every request
39//! .with_cache(Duration::from_secs(300))
40//! // Configure rate limiting to avoid hitting provider limits
41//! .with_rate_limit(NonZero::new(10).unwrap(), Duration::from_secs(60))
42//! .build();
43//!
44//! // Create an authorization layer using the remote JWK set to validate JWTs.
45//! let authorization_layer = AuthorizationLayer::with_remote_jwk_set(
46//! remote_jwk_set,
47//! Url::parse("https://your.jwt.issuer")?,
48//! "your.jwt.audience".to_string(),
49//! );
50//!
51//! // Protect your `axum` routes with the authorization layer.
52//! let router = Router::new()
53//! .route("/protected", get(|| async { "Hello, authorized user!" }))
54//! .layer(authorization_layer);
55//!
56//! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
57//! axum::serve(listener, router).await?;
58//! Ok(())
59//! }
60//! ```
61//!
62//! # Accessing JWT Claims
63//!
64//! After successful authorization, the JWT claims are available via axum's request extensions:
65//!
66//! ```rust,no_run
67//! use axum::{Extension, routing::get};
68//! use axum_jose::authorization::Claims;
69//! use serde_json::Value;
70//!
71//! async fn protected_handler(Extension(Claims(claims)): Extension<Claims>) -> String {
72//! // Access standard claims
73//! let subject = claims.get("sub").and_then(|v| v.as_str()).unwrap_or("unknown");
74//!
75//! // Access custom claims
76//! let role = claims.get("role").and_then(|v| v.as_str()).unwrap_or("user");
77//!
78//! format!("Hello, {}! Your role is: {}", subject, role)
79//! }
80//! ```
81//!
82//! For type-safe claim extraction, deserialize into a custom struct:
83//!
84//! ```rust,no_run
85//! use serde::Deserialize;
86//! use axum_jose::authorization::Claims;
87//! use axum::Extension;
88//!
89//! #[derive(Deserialize)]
90//! struct MyClaims {
91//! sub: String,
92//! role: String,
93//! }
94//!
95//! async fn protected_handler(Extension(Claims(claims)): Extension<Claims>) -> String {
96//! match serde_json::from_value::<MyClaims>(claims) {
97//! Ok(my_claims) => format!("Hello, {}! Your role is: {}", my_claims.sub, my_claims.role),
98//! Err(_) => "Invalid claims".to_string(),
99//! }
100//! }
101//! ```
102//!
103//! # Custom HTTP Client
104//!
105//! Provide a custom `reqwest::Client` for specific requirements like timeouts, proxies, etc.:
106//!
107//! ```rust,no_run
108//! # use axum_jose::RemoteJwkSet;
109//! # use url::Url;
110//! # use std::time::Duration;
111//!
112//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
113//! let http_client = reqwest::Client::builder()
114//! .timeout(Duration::from_secs(10))
115//! .build()?;
116//!
117//! let jwk_set = RemoteJwkSet::builder(
118//! Url::parse("https://example.com/.well-known/jwks.json")?
119//! )
120//! .with_http_client(http_client)
121//! .build();
122//! # Ok(())
123//! # }
124//! ```
125//!
126//! # Using a Local JWK Set
127//!
128//! For testing purposes or scenarios where you manage keys locally you can use a `jsonwebtoken::jwk::JwkSet` directly
129//! instead of fetching one from a remote URL:
130//!
131//! ```rust,no_run
132//! # use axum::{routing::get, Router};
133//! use axum_jose::AuthorizationLayer;
134//! use jsonwebtoken::jwk::JwkSet;
135//! # use url::Url;
136//!
137//! # #[tokio::main]
138//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
139//! // Load or construct your JWK set
140//! let jwk_set: JwkSet = serde_json::from_str(r#"{"keys": [...]}"#)?;
141//!
142//! let auth_layer = AuthorizationLayer::with_local_jwk_set(
143//! jwk_set,
144//! Url::parse("https://your.jwt.issuer")?,
145//! "your.jwt.audience".to_string(),
146//! );
147//!
148//! let router = Router::new()
149//! .route("/protected", get(|| async { "Authorized!" }))
150//! .layer(auth_layer);
151//!
152//! # let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
153//! # axum::serve(listener, router).await?;
154//! # Ok(())
155//! # }
156//! ```
157//!
158//! # Error Handling
159//!
160//! When authorization fails, the middleware returns an [`Error`] instance that translates into a `401 Unauthorized`
161//! response with a JSON body containing a description of what went wrong:
162//!
163//! ```json
164//! {
165//! "error": "JWT validation failed"
166//! }
167//! ```
168//!
169//! See [`Error`] for more details on possible error cases.
170
171pub mod authorization;
172pub use authorization::AuthorizationLayer;
173pub use error::Error;
174pub use remote_jwk_set::{RemoteJwkSet, RemoteJwkSetBuilder};
175
176mod error;
177mod jwk_set;
178mod remote_jwk_set;