Crate axum_jose

Crate axum_jose 

Source
Expand description

Lightweight authorization middleware for axum, following JSON Object Signing and Encryption (JOSE) standards.

§Overview

This crate provides a tower-based AuthorizationLayer that integrates with axum’s middleware system to add JWT-based authorization to your application. The middleware validates JWTs from incoming requests against JWK (JSON Web Key) sets, which can be either provided locally or fetched from remote identity providers.

§Quickstart

This example illustrates how to validate JWTs against a remote JWK set provided e.g. by your OpenID Connect provider.

It also shows how to enable caching using the RemoteJwkSetBuilder::with_cache method to avoid fetching the JWK set on every request. Choose a TTL that balances responsiveness to key rotation with provider load. Shorter TTLs react faster to key rotation, while longer TTLs reduce requests to your identity provider.

Note though, that the cache is not only invalidated when reaching its TTL but also when a JWT with an unknown kid (key ID) is encountered. Therefore, in addition to caching, consider configuring rate limiting using RemoteJwkSetBuilder::with_rate_limit to prevent running into your identity provider’s server-side rate limits.

use axum::{routing::get, Router};
use axum_jose::{AuthorizationLayer, RemoteJwkSet};
use std::num::NonZero;
use std::time::Duration;
use url::Url;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Configure a `RemoteJwkSet` to fetch the JWK set e.g. from your OpenID
    // Connect provider which typically exposes JWK sets at a
    // `.well-known/jwks.json` endpoint
    let remote_jwk_set = RemoteJwkSet::builder(Url::parse(
        "https://your.oidc.provider/.well-known/jwks.json",
    )?)
    // Use caching to avoid fetching the JWK set on every request
    .with_cache(Duration::from_secs(300))
    // Configure rate limiting to avoid hitting provider limits
    .with_rate_limit(NonZero::new(10).unwrap(), Duration::from_secs(60))
    .build();

    // Create an authorization layer using the remote JWK set to validate JWTs.
    let authorization_layer = AuthorizationLayer::with_remote_jwk_set(
        remote_jwk_set,
        Url::parse("https://your.jwt.issuer")?,
        "your.jwt.audience".to_string(),
    );

    // Protect your `axum` routes with the authorization layer.
    let router = Router::new()
        .route("/protected", get(|| async { "Hello, authorized user!" }))
        .layer(authorization_layer);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, router).await?;
    Ok(())
}

§Accessing JWT Claims

After successful authorization, the JWT claims are available via axum’s request extensions:

use axum::{Extension, routing::get};
use axum_jose::authorization::Claims;
use serde_json::Value;

async fn protected_handler(Extension(Claims(claims)): Extension<Claims>) -> String {
    // Access standard claims
    let subject = claims.get("sub").and_then(|v| v.as_str()).unwrap_or("unknown");

    // Access custom claims
    let role = claims.get("role").and_then(|v| v.as_str()).unwrap_or("user");

    format!("Hello, {}! Your role is: {}", subject, role)
}

For type-safe claim extraction, deserialize into a custom struct:

use serde::Deserialize;
use axum_jose::authorization::Claims;
use axum::Extension;

#[derive(Deserialize)]
struct MyClaims {
    sub: String,
    role: String,
}

async fn protected_handler(Extension(Claims(claims)): Extension<Claims>) -> String {
    match serde_json::from_value::<MyClaims>(claims) {
        Ok(my_claims) => format!("Hello, {}! Your role is: {}", my_claims.sub, my_claims.role),
        Err(_) => "Invalid claims".to_string(),
    }
}

§Custom HTTP Client

Provide a custom reqwest::Client for specific requirements like timeouts, proxies, etc.:


let http_client = reqwest::Client::builder()
    .timeout(Duration::from_secs(10))
    .build()?;

let jwk_set = RemoteJwkSet::builder(
    Url::parse("https://example.com/.well-known/jwks.json")?
)
.with_http_client(http_client)
.build();

§Using a Local JWK Set

For testing purposes or scenarios where you manage keys locally you can use a jsonwebtoken::jwk::JwkSet directly instead of fetching one from a remote URL:

use axum_jose::AuthorizationLayer;
use jsonwebtoken::jwk::JwkSet;

// Load or construct your JWK set
let jwk_set: JwkSet = serde_json::from_str(r#"{"keys": [...]}"#)?;

let auth_layer = AuthorizationLayer::with_local_jwk_set(
    jwk_set,
    Url::parse("https://your.jwt.issuer")?,
    "your.jwt.audience".to_string(),
);

let router = Router::new()
    .route("/protected", get(|| async { "Authorized!" }))
    .layer(auth_layer);

§Error Handling

When authorization fails, the middleware returns an Error instance that translates into a 401 Unauthorized response with a JSON body containing a description of what went wrong:

{
  "error": "JWT validation failed"
}

See Error for more details on possible error cases.

Re-exports§

pub use authorization::AuthorizationLayer;

Modules§

authorization

Structs§

RemoteJwkSet
RemoteJwkSetBuilder
Builder for configuring a RemoteJwkSet with optional caching and rate limiting.

Enums§

Error