Crates.io | tower-jwt |
lib.rs | tower-jwt |
version | 0.2.1 |
source | src |
created_at | 2024-03-20 12:15:56.134397 |
updated_at | 2024-03-25 09:25:43.25759 |
description | Tower middleware to parse JWTs on Authorization Bearers |
homepage | |
repository | https://github.com/shuttle-hq/tower-jwt |
max_upload_size | |
id | 1180430 |
size | 25,410 |
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.
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(())
}
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(())
}