diff --git a/Cargo.toml b/Cargo.toml index 3e15fde..3b23385 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" members = [ - "demo-server", - "jwt-authorizer", + #"demo-server", + "jwt-authorizer" ] diff --git a/demo-server/Cargo.toml b/demo-server/Cargo.toml deleted file mode 100644 index 5b5b198..0000000 --- a/demo-server/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "demo-server" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = "1.0.83" -axum = { version = "0.7.5" } -headers = "0.4" -josekit = "0.8.6" -jsonwebtoken = "9.3.0" -once_cell = "1.19.0" -reqwest = { version = "0.12.4", features = ["json"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -thiserror = "1.0.60" -tokio = { version = "1.37.0", features = ["full"] } -tower-http = { version = "0.5.2", features = ["trace"] } -tracing = "0.1.40" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -jwt-authorizer = { path = "../jwt-authorizer" } diff --git a/demo-server/bruno-e2e/401 Invalid Token.bru b/demo-server/bruno-e2e/401 Invalid Token.bru deleted file mode 100644 index b56b98d..0000000 --- a/demo-server/bruno-e2e/401 Invalid Token.bru +++ /dev/null @@ -1,20 +0,0 @@ -meta { - name: 401 Invalid Token - type: http - seq: 6 -} - -get { - url: http://localhost:3000/api/protected - body: none - auth: bearer -} - -auth:bearer { - token: blablabla.xxxx.xxxx -} - -assert { - res.status: eq 401 - res.headers['www-authenticate']: eq Bearer error="invalid_token" -} diff --git a/demo-server/bruno-e2e/401 No Token.bru b/demo-server/bruno-e2e/401 No Token.bru deleted file mode 100644 index 5f6bac2..0000000 --- a/demo-server/bruno-e2e/401 No Token.bru +++ /dev/null @@ -1,16 +0,0 @@ -meta { - name: 401 No Token - type: http - seq: 5 -} - -get { - url: http://localhost:3000/api/protected - body: none - auth: none -} - -assert { - res.status: eq 401 - res.headers['www-authenticate']: eq Bearer -} diff --git a/demo-server/bruno-e2e/Protected EC.bru b/demo-server/bruno-e2e/Protected EC.bru deleted file mode 100644 index fa1047e..0000000 --- a/demo-server/bruno-e2e/Protected EC.bru +++ /dev/null @@ -1,20 +0,0 @@ -meta { - name: Protected EC - type: http - seq: 2 -} - -get { - url: http://localhost:3000/api/protected - body: none - auth: bearer -} - -auth:bearer { - token: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.YMQHWpGLJ3P59SvPX-RIW3uT5rfzShzcP1TNcaXr0VnsxCXYO0og0c3_O30no0D_ct0hOUJINY5tBsok-66Gzw -} - -assert { - res.status: eq 200 - res.body: contains b@b.com -} diff --git a/demo-server/bruno-e2e/Protected ED.bru b/demo-server/bruno-e2e/Protected ED.bru deleted file mode 100644 index e9c82ae..0000000 --- a/demo-server/bruno-e2e/Protected ED.bru +++ /dev/null @@ -1,20 +0,0 @@ -meta { - name: Protected ED - type: http - seq: 3 -} - -get { - url: http://localhost:3000/api/protected - body: none - auth: bearer -} - -auth:bearer { - token: eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImVkMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.5bFOZqc-lBFy4gFifQ_CTx1A3R6Nry71gdi7KH2GGvTZQC_ZI1vNbqGnWQhpR6n_jUd9ICUc0pPI5iLCB6K1Bg -} - -assert { - res.status: eq 200 - res.body: contains b@b.com -} diff --git a/demo-server/bruno-e2e/Protected RSA.bru b/demo-server/bruno-e2e/Protected RSA.bru deleted file mode 100644 index 36ed575..0000000 --- a/demo-server/bruno-e2e/Protected RSA.bru +++ /dev/null @@ -1,20 +0,0 @@ -meta { - name: Protected RSA - type: http - seq: 1 -} - -get { - url: http://localhost:3000/api/protected - body: none - auth: bearer -} - -auth:bearer { - token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAxIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.pmm8Kdk-SvycXIGpWb1R0DuP5nlB7w4QQS7trhN_OjOpbk0A8F_lC4BdClz3rol2Pgo61lcFckJgjNBj34DQGeTGOtvxdiUXNgi1aKiXH4AyPzZeZx30PgFxa1fxhuZhBAj6xIZKBSBQvVyjeVQzAScINRCBX8zfCaXSU1ZCUkJl5vbD7zT-cYIFU76we9HcIYKRXwTiAyoNn3Lixa1H3_t5sbx3om2WlIB2x-sGpoDFDjorcuJT1yQx3grTRTBzHyRBRjZ3e8wrMbiacy-m3WoEFdkssQgYi_dSQH0hvxgacvGWayK0UqD7O5UL6EzTA2feXbgA_68o5gfvSnM8CUsPut5gZr-gwVbQKPbBdCQtl_wXIMot7UNKYEiFV38x5EmUr-ShzQcditW6fciguuY1Qav502UE1UMXvt5p8-kYxw2AaaVd6iTgQBzkBrtvywMYWzIwzGNA70RvUhI2rlgcn8GEU_51Tv_NMHjp6CjDbAxQVKa0PlcRE4pd6yk_IJSR4Nska_8BQZdPbsFn--z_XHEDoRZQ1C1M6m77xVndg3zX0sNQPXfWsttCbBmaHvMKTOp0cH9rlWB9r9nTo9fn8jcfqlak2O2IAzfzsOdVfUrES6T1UWkWobs9usGgqJuIkZHbDd4tmXyPRT4wrU7hxEyE9cuvuZPAi8GYt80 -} - -assert { - res.status: eq 200 - res.body: contains b@b.com -} diff --git a/demo-server/bruno-e2e/Public URL.bru b/demo-server/bruno-e2e/Public URL.bru deleted file mode 100644 index f341ec8..0000000 --- a/demo-server/bruno-e2e/Public URL.bru +++ /dev/null @@ -1,15 +0,0 @@ -meta { - name: Public URL - type: http - seq: 4 -} - -get { - url: http://localhost:3000/public - body: none - auth: none -} - -assert { - res.status: eq 200 -} diff --git a/demo-server/bruno-e2e/bruno.json b/demo-server/bruno-e2e/bruno.json deleted file mode 100644 index 84ba4df..0000000 --- a/demo-server/bruno-e2e/bruno.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "version": "1", - "name": "jwt-authorizer E2E", - "type": "collection", - "ignore": [ - "node_modules", - ".git" - ] -} \ No newline at end of file diff --git a/demo-server/bruno-e2e/demo-server/Discovery.bru b/demo-server/bruno-e2e/demo-server/Discovery.bru deleted file mode 100644 index 79bf5e6..0000000 --- a/demo-server/bruno-e2e/demo-server/Discovery.bru +++ /dev/null @@ -1,15 +0,0 @@ -meta { - name: Discovery - type: http - seq: 1 -} - -get { - url: http://localhost:3001/.well-known/openid-configuration - body: none - auth: none -} - -assert { - res.status: eq 200 -} diff --git a/demo-server/bruno-e2e/demo-server/JWKS Endpoint.bru b/demo-server/bruno-e2e/demo-server/JWKS Endpoint.bru deleted file mode 100644 index f34a79d..0000000 --- a/demo-server/bruno-e2e/demo-server/JWKS Endpoint.bru +++ /dev/null @@ -1,15 +0,0 @@ -meta { - name: JWKS Endpoint - type: http - seq: 2 -} - -get { - url: http://localhost:3001/jwks - body: none - auth: none -} - -assert { - res.status: eq 200 -} diff --git a/demo-server/bruno-e2e/demo-server/Test Tokens.bru b/demo-server/bruno-e2e/demo-server/Test Tokens.bru deleted file mode 100644 index 874c10c..0000000 --- a/demo-server/bruno-e2e/demo-server/Test Tokens.bru +++ /dev/null @@ -1,15 +0,0 @@ -meta { - name: Test Tokens - type: http - seq: 3 -} - -get { - url: http://localhost:3001/tokens - body: none - auth: none -} - -assert { - res.status: eq 200 -} diff --git a/demo-server/src/main.rs b/demo-server/src/main.rs deleted file mode 100644 index be7f7e2..0000000 --- a/demo-server/src/main.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Demo server - for demo and testing purposes - -use axum::{routing::get, Router}; -use jwt_authorizer::{ - error::InitError, AuthError, Authorizer, IntoLayer, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy, -}; -use serde::Deserialize; -use tokio::net::TcpListener; -use tower_http::trace::TraceLayer; -use tracing::info; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -mod oidc_provider; - -/// Object representing claims -/// (a subset of deserialized claims) -#[derive(Debug, Deserialize, Clone)] -struct User { - sub: String, -} - -#[tokio::main] -async fn main() -> Result<(), InitError> { - tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::new( - std::env::var("RUST_LOG").unwrap_or_else(|_| "info,jwt_authorizer=debug,tower_http=debug".into()), - )) - .with(tracing_subscriber::fmt::layer()) - .init(); - - // claims checker function - fn claim_checker(u: &User) -> bool { - info!("checking claims: {} -> {}", u.sub, u.sub.contains('@')); - - u.sub.contains('@') // must be an email - } - - // starting oidc provider (discovery is needed by from_oidc()) - let issuer_uri = oidc_provider::run_server(); - - // First let's create an authorizer builder from a Oidc Discovery - // User is a struct deserializable from JWT claims representing the authorized user - // let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc("https://accounts.google.com/") - let auth: Authorizer = JwtAuthorizer::from_oidc(issuer_uri) - // .no_refresh() - .refresh(Refresh { - strategy: RefreshStrategy::Interval, - ..Default::default() - }) - .check(claim_checker) - .build() - .await?; - - // actual router demo - let api = Router::new() - .route("/protected", get(protected)) - // adding the authorizer layer - .layer(auth.into_layer()); - - let app = Router::new() - // public endpoint - .route("/public", get(public_handler)) - // protected APIs - .nest("/api", api) - .layer(TraceLayer::new_for_http()); - - let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap(); - tracing::info!("listening on {:?}", listener.local_addr()); - - axum::serve(listener, app.into_make_service()).await.unwrap(); - - Ok(()) -} - -/// handler with injected claims object -async fn protected(JwtClaims(user): JwtClaims) -> Result { - // Send the protected data to the user - Ok(format!("Welcome: {}", user.sub)) -} - -// public url handler -async fn public_handler() -> &'static str { - "Public URL!" -} diff --git a/demo-server/src/oidc_provider/mod.rs b/demo-server/src/oidc_provider/mod.rs deleted file mode 100644 index 3ee41ab..0000000 --- a/demo-server/src/oidc_provider/mod.rs +++ /dev/null @@ -1,183 +0,0 @@ -use axum::{routing::get, Json, Router}; -use josekit::jwk::{ - alg::{ec::EcCurve, ec::EcKeyPair, ed::EdKeyPair, rsa::RsaKeyPair}, - Jwk, -}; -use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; -use jwt_authorizer::{NumericDate, OneOrArray, RegisteredClaims}; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::{thread, time::Duration}; -use tokio::net::TcpListener; - -const ISSUER_URI: &str = "http://localhost:3001"; - -/// OpenId Connect discovery (simplified for test purposes) -#[derive(Serialize, Clone)] -struct OidcDiscovery { - issuer: String, - jwks_uri: String, -} - -/// discovery url handler -async fn discovery() -> Json { - let d = OidcDiscovery { - issuer: ISSUER_URI.to_owned(), - jwks_uri: format!("{ISSUER_URI}/jwks"), - }; - Json(json!(d)) -} - -#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] -struct JwkSet { - keys: Vec, -} - -/// jwk set endpoint handler -async fn jwks() -> Json { - let mut kset = JwkSet { keys: Vec::::new() }; - - let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/rsa-private1.pem")).unwrap(); - let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("rsa01"); - pk.set_algorithm("RS256"); - pk.set_key_use("sig"); - kset.keys.push(pk); - - let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/rsa-private2.pem")).unwrap(); - let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("rsa02"); - pk.set_algorithm("RS256"); - pk.set_key_use("sig"); - kset.keys.push(pk); - - let keypair = EcKeyPair::from_pem(include_bytes!("../../../config/ec256-private1.pem"), Some(EcCurve::P256)).unwrap(); - let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("ec01"); - pk.set_algorithm("ES256"); - pk.set_key_use("sig"); - kset.keys.push(pk); - - let keypair = EcKeyPair::from_pem(include_bytes!("../../../config/ec256-private2.pem"), Some(EcCurve::P256)).unwrap(); - let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("ec02"); - pk.set_algorithm("ES256"); - pk.set_key_use("sig"); - kset.keys.push(pk); - - let keypair = EcKeyPair::from_pem(include_bytes!("../../../config/ec384-private1.pem"), Some(EcCurve::P384)).unwrap(); - let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("ec01-es384"); - pk.set_algorithm("ES384"); - pk.set_key_use("sig"); - kset.keys.push(pk); - - let keypair = EdKeyPair::from_pem(include_bytes!("../../../config/ed25519-private1.pem")).unwrap(); - let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("ed01"); - pk.set_algorithm("EdDSA"); - pk.set_key_use("sig"); - kset.keys.push(pk); - - let keypair = EdKeyPair::from_pem(include_bytes!("../../../config/ed25519-private2.pem")).unwrap(); - let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("ed02"); - pk.set_algorithm("EdDSA"); - pk.set_key_use("sig"); - kset.keys.push(pk); - - Json(json!(kset)) -} - -/// build a minimal JWT header -fn build_header(alg: Algorithm, kid: &str) -> Header { - Header { - typ: Some("JWT".to_string()), - alg, - kid: Some(kid.to_owned()), - cty: None, - jku: None, - jwk: None, - x5u: None, - x5c: None, - x5t: None, - x5t_s256: None, - } -} - -/// token claims -#[derive(Debug, Serialize, Deserialize)] -struct Claims { - iss: &'static str, - sub: &'static str, - exp: usize, - nbf: usize, -} - -/// handler issuing test tokens (this is not a standard endpoint) -pub async fn tokens() -> Json { - let claims = Claims { - iss: ISSUER_URI, - sub: "b@b.com", - exp: 2000000000, // May 2033 - nbf: 1516239022, // Jan 2018 - }; - - let claims_with_aud = RegisteredClaims { - iss: Some(ISSUER_URI.to_owned()), - sub: Some("b@b.com".to_owned()), - aud: Some(OneOrArray::Array(vec!["aud1".to_owned(), "aud2".to_owned()])), - exp: Some(NumericDate(2000000000)), // May 2033 - nbf: Some(NumericDate(1516239022)), // Jan 2018 - iat: None, - jti: None, - }; - - let rsa1_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/rsa-private1.pem")).unwrap(); - let rsa2_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/rsa-private2.pem")).unwrap(); - let ec1_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec256-private1.pem")).unwrap(); - let ec2_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec256-private2.pem")).unwrap(); - let ec1_es384_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec384-private1.pem")).unwrap(); - let ed1_key = EncodingKey::from_ed_pem(include_bytes!("../../../config/ed25519-private1.pem")).unwrap(); - let ed2_key = EncodingKey::from_ed_pem(include_bytes!("../../../config/ed25519-private2.pem")).unwrap(); - - let rsa1_token = encode(&build_header(Algorithm::RS256, "rsa01"), &claims, &rsa1_key).unwrap(); - let rsa1_token_aud = encode(&build_header(Algorithm::RS256, "rsa01"), &claims_with_aud, &rsa1_key).unwrap(); - let rsa2_token = encode(&build_header(Algorithm::RS256, "rsa02"), &claims, &rsa2_key).unwrap(); - let ec1_token_aud = encode(&build_header(Algorithm::ES256, "ec01"), &claims_with_aud, &ec1_key).unwrap(); - let ec1_token = encode(&build_header(Algorithm::ES256, "ec01"), &claims, &ec1_key).unwrap(); - let ec2_token = encode(&build_header(Algorithm::ES256, "ec02"), &claims, &ec2_key).unwrap(); - let ec1_es384_token = encode(&build_header(Algorithm::ES384, "ec01-es384"), &claims, &ec1_es384_key).unwrap(); - let ed1_token = encode(&build_header(Algorithm::EdDSA, "ed01"), &claims, &ed1_key).unwrap(); - let ed2_token = encode(&build_header(Algorithm::EdDSA, "ed02"), &claims, &ed2_key).unwrap(); - - Json(json!({ - "rsa01": rsa1_token, - "rsa01_aud": rsa1_token_aud, - "rsa02": rsa2_token, - "ec01": ec1_token, - "ec01_aud": ec1_token_aud, - "ec02": ec2_token, - "ec01_es384": ec1_es384_token, - "ed01": ed1_token, - "ed02": ed2_token, - })) -} - -/// exposes some oidc "like" endpoints for test purposes -pub fn run_server() -> &'static str { - let app = Router::new() - .route("/.well-known/openid-configuration", get(discovery)) - .route("/jwks", get(jwks)) - .route("/tokens", get(tokens)); - - tokio::spawn(async move { - let listener = TcpListener::bind("127.0.0.1:3001").await.unwrap(); - tracing::info!("oidc provider starting on: {:?}", listener.local_addr()); - axum::serve(listener, app.into_make_service()).await.unwrap(); - }); - - thread::sleep(Duration::from_millis(200)); // waiting oidc to start - - ISSUER_URI -} diff --git a/jwt-authorizer/Cargo.toml b/jwt-authorizer/Cargo.toml index ba6421c..3dc0b26 100644 --- a/jwt-authorizer/Cargo.toml +++ b/jwt-authorizer/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/cduvray/jwt-authorizer" keywords = ["jwt", "axum", "authorisation", "jwks"] [dependencies] -axum = { version = "0.7" } +axum = { version = "0.8" } chrono = { version = "0.4", optional = true } futures-util = "0.3" futures-core = "0.3" diff --git a/jwt-authorizer/src/lib.rs b/jwt-authorizer/src/lib.rs index 86878bc..52a1863 100644 --- a/jwt-authorizer/src/lib.rs +++ b/jwt-authorizer/src/lib.rs @@ -1,6 +1,9 @@ #![doc = include_str!("../docs/README.md")] -use axum::{async_trait, extract::FromRequestParts, http::request::Parts}; +use axum::{ + extract::{FromRequestParts, OptionalFromRequestParts}, + http::request::Parts, +}; use jsonwebtoken::TokenData; use serde::de::DeserializeOwned; @@ -24,7 +27,6 @@ pub mod validation; #[derive(Debug, Clone, Copy, Default)] pub struct JwtClaims(pub T); -#[async_trait] impl FromRequestParts for JwtClaims where T: DeserializeOwned + Send + Sync + Clone + 'static, @@ -40,3 +42,19 @@ where } } } + +impl OptionalFromRequestParts for JwtClaims +where + T: DeserializeOwned + Send + Sync + Clone + 'static, + S: Send + Sync, +{ + type Rejection = AuthError; + + async fn from_request_parts(parts: &mut Parts, _: &S) -> Result, Self::Rejection> { + if let Some(claims) = parts.extensions.get::>() { + Ok(Some(JwtClaims(claims.claims.clone()))) + } else { + Ok(None) + } + } +}