Skip to content

Commit

Permalink
Merge pull request #1 from shuttle-hq/feat/more_algorithms
Browse files Browse the repository at this point in the history
feat: more algorithms
  • Loading branch information
chesedo committed Mar 23, 2024
2 parents 94ad6ad + 4c12fc9 commit 928eb59
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 128 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "tower-jwt"
description = "Tower middleware to parse JWTs on Authorization Bearers"
version = "0.1.0"
version = "0.1.1"
license = "Apache-2.0"
repository = "https://github.com/shuttle-hq/tower-jwt"
documentation = "https://docs.rs/tower-jwt"
Expand All @@ -21,7 +21,7 @@ tracing = "0.1.40"

[dev-dependencies]
axum = "0.6.13"
chrono = { version = "0.4.23", features = ["clock"] }
chrono = { version = "0.4.23", features = ["clock", "serde"] }
hyper = "^0.14.0"
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] }
ring = "0.17.8"
125 changes: 115 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
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](https://docs.rs/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.

# Example
# Symmetric example using Hyper

``` rust
use http::{Request, Response, StatusCode};
use chrono::{DateTime, Utc};
use http::{header::AUTHORIZATION, Request, Response, StatusCode};
use hyper::Body;
use jsonwebtoken::{DecodingKey, Validation};
use serde::Deserialize;
use std::{convert::Infallible, iter::once};
use std::convert::Infallible;
use tower::{Service, ServiceBuilder, ServiceExt};
use tower_jwt::{JwtLayer, RequestClaim};

// Setup your claim with the fields you want to extract
#[derive(Clone, Deserialize, Debug)]
struct Claim {
/// Subject (whom token refers to).
/// 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]
Expand All @@ -26,6 +38,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

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(Body::empty()))
} else {
Expand All @@ -37,15 +55,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}

// Make a new JWT layer which will validate the token were issued by "issuer"
let jwt_layer = JwtLayer::<Claim, _>::new("issuer", || {
// Something to get the public key
async { Vec::new() }
});
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
// Call the service without a claim
let request = Request::builder().uri("/").body(Body::empty())?;

let status = service.ready().await?.call(request).await?.status();
Expand All @@ -56,6 +78,89 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
"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(Body::empty())?;

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
```rust
use axum::{routing::get, Extension, Router};
use http::{Request, StatusCode};
use hyper::Body;
use jsonwebtoken::{DecodingKey, Validation};
use ring::{
rand,
signature::{self, Ed25519KeyPair, KeyPair},
};
use serde::Deserialize;
use tower::ServiceExt;
use tower_jwt::{JwtLayer, RequestClaim};

// 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(Body::empty()).unwrap())
.await
.unwrap();

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

Ok(())
}
```
Loading

0 comments on commit 928eb59

Please sign in to comment.