4 releases

0.2.1 Mar 25, 2024
0.2.0 Mar 23, 2024
0.1.1 Mar 23, 2024
0.1.0 Mar 20, 2024

#539 in Authentication

Apache-2.0

25KB
387 lines

Tower middleware to parse JWT tokens off the Authorization Bearer of requests and store the deserialized claims on the request extension.

This is build on top of the jsonwebtoken crate and support all the algorithms supported by that crate.

Since this is a Tower middleware it can be used on any framework like Axum, Tonic, etc.

Symmetric example using Hyper

This example show how to use the re-exported DecodingKey and Validation from jsonwebtoken to parse and validate a JWT setup with a simple symmetric secret. It also show how to customize the default validator and how to extract both registered and public fields on a claim.

use chrono::{DateTime, Utc};
use http::{header::AUTHORIZATION, Request, Response, StatusCode};
use http_body_util::Empty;
use serde::Deserialize;
use std::convert::Infallible;
use tower::{Service, ServiceBuilder, ServiceExt};
use tower_jwt::{DecodingKey, JwtLayer, RequestClaim, Validation};

// Setup your claim with the fields you want to extract
#[derive(Clone, Deserialize, Debug)]
struct Claim {
    /// Subject (whom the token refers to)
    pub sub: String,

    /// Name of the claim owner
    pub name: String,

    #[serde(with = "chrono::serde::ts_seconds")]
    /// Issued at (timestamp)
    pub iat: DateTime<Utc>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    async fn handle(req: Request<Empty<()>>) -> Result<Response<Empty<()>>, Infallible> {
        let claim = req.extensions().get::<RequestClaim<Claim>>();

        if let Some(claim) = claim {
            // Use the claim here...
            assert_eq!(claim.claim.sub, "1234567890");
            assert_eq!(claim.claim.name, "John Doe");
            assert_eq!(
                claim.claim.iat,
                DateTime::parse_from_rfc3339("2018-01-18T01:30:00Z").unwrap()
            );

            Ok(Response::new(Empty::new()))
        } else {
            // Claim was not set so this request is unauthorized
            Ok(Response::builder()
                .status(StatusCode::UNAUTHORIZED)
                .body(Empty::new())
                .unwrap())
        }
    }

    let mut validation = Validation::default();
    validation.validate_exp = false;
    validation.required_spec_claims.clear();

    // Make a new JWT layer which will validate the tokens on requests
    let jwt_layer = JwtLayer::<Claim>::new(
        validation,
        DecodingKey::from_secret("symmetric secret".as_bytes()),
    );

    let mut service = ServiceBuilder::new().layer(jwt_layer).service_fn(handle);

    // Call the service without a claim
    let request = Request::builder().uri("/").body(Empty::new())?;

    let status = service.ready().await?.call(request).await?.status();

    assert_eq!(
        status,
        StatusCode::UNAUTHORIZED,
        "request did not have a token while endpoint expected one"
    );

    // Call the service with a claim
    let request = Request::builder()
        .uri("/")
        .header(AUTHORIZATION, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDAwfQ.CHiQ0VbodaR55aiN_0JJB7nWJBO__rt_7ur1WO-jZxg")
        .body(Empty::new())?;

    let status = service.ready().await?.call(request).await?.status();

    assert_eq!(
        status,
        StatusCode::OK,
        "request should extract the token correctly"
    );

    Ok(())
}

Assymmetric example using Axum

use axum::{routing::get, Extension, Router};
use http::{Request, StatusCode};
use http_body_util::Empty;
use ring::{
    rand,
    signature::{self, Ed25519KeyPair, KeyPair},
};
use serde::Deserialize;
use tower::ServiceExt;
use tower_jwt::{DecodingKey, JwtLayer, RequestClaim, Validation};

// Setup your claim with the fields you want to extract
#[derive(Deserialize, Clone)]
pub struct Claim {
    /// Subject (whom token refers to).
    pub sub: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Make a asymmetric key pair
    // This will mostly be done outside of the code using like `openssl` to generate the key pair
    let doc = signature::Ed25519KeyPair::generate_pkcs8(&rand::SystemRandom::new()).unwrap();
    // let encoding_key = EncodingKey::from_ed_der(doc.as_ref());
    let pair = Ed25519KeyPair::from_pkcs8(doc.as_ref()).unwrap();
    let public_key = pair.public_key().as_ref().to_vec();
    let decoding_key = DecodingKey::from_ed_der(&public_key);

    let mut validation = Validation::new(jsonwebtoken::Algorithm::EdDSA);

    // Only allow tokens from the test-issuer
    validation.set_issuer(&["test-issuer"]);

    let router = Router::new()
        .route(
            "/",
            get(|claim: Option<Extension<RequestClaim<Claim>>>| async move {
                if let Some(Extension(claim)) = claim {
                    (StatusCode::OK, format!("Hello, {}", claim.claim.sub))
                } else {
                    (StatusCode::UNAUTHORIZED, "Not authorized".to_string())
                }
            }),
        )
        .layer(JwtLayer::<Claim, _>::new(validation, move || {
            let decoding_key = decoding_key.clone();

            async {
                // In practice a network call will happen here to get the public key
                decoding_key
            }
        }));

    // Call the service without a claim
    let response = router
        .clone()
        .oneshot(Request::builder().uri("/").body(Empty::new()).unwrap())
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);

    Ok(())
}

Dependencies

~3–12MB
~163K SLoC