diff --git a/README.md b/README.md index 99b5296..7666ed0 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,15 @@ JWT authorizer Layer for Axum. ## Features - JWT token verification (Bearer) - - Algoritms: ECDSA, RSA, EdDSA, HS + - Algoritms: ECDSA, RSA, EdDSA, HMAC - JWKS endpoint support - Configurable refresh + - OpenId Connect Discovery +- Validation + - exp, nbf, iss, aud - Claims extraction - Claims checker +- Tracing support (error logging) ## Usage @@ -17,7 +21,7 @@ See documentation of the [`jwt-authorizer`](./jwt-authorizer/docs/README.md) mod ## Development -... +Minimum supported Rust version is 1.65. ## Contributing diff --git a/config/README.md b/config/README.md index bcd2490..e21e48c 100644 --- a/config/README.md +++ b/config/README.md @@ -1,5 +1,10 @@ # Key generation +## RSA + +> openssl genrsa -out rsa-private2.pem 1024 +> openssl rsa -in rsa-private2.pem -out rsa-public2.pem -pubout -outform PEM + ## EC (ECDSA) - (algorigthm ES256 - ECDSA using SHA-256) curve name: prime256v1 (secp256r1, secp384r1) diff --git a/config/rsa-public2.pem b/config/rsa-public2.pem new file mode 100644 index 0000000..5a7ffd9 --- /dev/null +++ b/config/rsa-public2.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRE6rHuNR0QbHO3H3Kt2 +pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5/CYYi/cvI+SXVT9kPWSKXxJXB +Xd/4LkvcPuUakBoAkfh+eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHR +yIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG/AtH89BIE9jDBHZ9dLelK9a184zAf8Lw +oPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xq +i+yUod+j8MtvIj812dkS4QMiRVN/by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5T +dQIDAQAB +-----END PUBLIC KEY----- diff --git a/demo-server/src/oidc_provider/mod.rs b/demo-server/src/oidc_provider/mod.rs index c343dc8..70848a4 100644 --- a/demo-server/src/oidc_provider/mod.rs +++ b/demo-server/src/oidc_provider/mod.rs @@ -101,6 +101,7 @@ fn build_header(alg: Algorithm, kid: &str) -> Header { struct Claims { iss: &'static str, sub: &'static str, + aud: &'static str, exp: usize, nbf: usize, } @@ -110,6 +111,7 @@ pub async fn tokens() -> Json { let claims = Claims { iss: ISSUER_URI, sub: "b@b.com", + aud: "aud1", exp: 2000000000, // May 2033 nbf: 1516239022, // Jan 2018 }; diff --git a/jwt-authorizer/docs/README.md b/jwt-authorizer/docs/README.md index 996d638..3537922 100644 --- a/jwt-authorizer/docs/README.md +++ b/jwt-authorizer/docs/README.md @@ -5,13 +5,15 @@ JWT authoriser Layer for Axum. ## Features - JWT token verification (Bearer) - - Algoritms: ECDSA, RSA, EdDSA, HS + - Algoritms: ECDSA, RSA, EdDSA, HMAC - JWKS endpoint support - Configurable refresh - OpenId Connect Discovery +- Validation + - exp, nbf, iss, aud - Claims extraction - Claims checker -- tracing support (error logging) +- Tracing support (error logging) ## Usage Example @@ -48,6 +50,30 @@ JWT authoriser Layer for Axum. # }; ``` +## Validation + +Validation configuration object. + +If no validation configuration is provided default values will be applyed. + +docs: [`jwt-authorizer::Validation`] + +```rust +# use jwt_authorizer::{JwtAuthorizer, Validation}; +# use serde_json::Value; + +let validation = Validation::new() + .iss(&["https://issuer1", "https://issuer2"]) + .aud(&["audience1"]) + .nbf(true) + .leeway(20); + +let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc("https://accounts.google.com") + .validation(validation); + +``` + + ## ClaimsChecker A check function (mapping deserialized claims to boolean) can be added to the authorizer. diff --git a/jwt-authorizer/src/authorizer.rs b/jwt-authorizer/src/authorizer.rs index 1c24989..0af9907 100644 --- a/jwt-authorizer/src/authorizer.rs +++ b/jwt-authorizer/src/authorizer.rs @@ -1,6 +1,6 @@ use std::io::Read; -use jsonwebtoken::{decode, decode_header, jwk::JwkSet, DecodingKey, TokenData, Validation}; +use jsonwebtoken::{decode, decode_header, jwk::JwkSet, DecodingKey, TokenData}; use reqwest::Url; use serde::de::DeserializeOwned; @@ -37,6 +37,7 @@ where { pub key_source: KeySource, pub claims_checker: Option>, + pub validation: crate::validation::Validation, } fn read_data(path: &str) -> Result, InitError> { @@ -64,6 +65,7 @@ where key_source_type: &KeySourceType, claims_checker: Option>, refresh: Option, + validation: crate::validation::Validation, ) -> Result, InitError> { Ok(match key_source_type { KeySourceType::RSA(path) => { @@ -71,6 +73,7 @@ where Authorizer { key_source: KeySource::DecodingKeySource(key), claims_checker, + validation, } } KeySourceType::EC(path) => { @@ -78,6 +81,7 @@ where Authorizer { key_source: KeySource::DecodingKeySource(key), claims_checker, + validation, } } KeySourceType::ED(path) => { @@ -85,6 +89,7 @@ where Authorizer { key_source: KeySource::DecodingKeySource(key), claims_checker, + validation, } } KeySourceType::Secret(secret) => { @@ -92,6 +97,7 @@ where Authorizer { key_source: KeySource::DecodingKeySource(key), claims_checker, + validation, } } KeySourceType::JwksString(jwks_str) => { @@ -102,6 +108,7 @@ where Authorizer { key_source: KeySource::DecodingKeySource(k), claims_checker, + validation, } } KeySourceType::Jwks(url) => { @@ -110,6 +117,7 @@ where Authorizer { key_source: KeySource::KeyStoreSource(key_store_manager), claims_checker, + validation, } } KeySourceType::Discovery(issuer_url) => { @@ -120,6 +128,7 @@ where Authorizer { key_source: KeySource::KeyStoreSource(key_store_manager), claims_checker, + validation, } } }) @@ -127,9 +136,11 @@ where pub async fn check_auth(&self, token: &str) -> Result, AuthError> { let header = decode_header(token)?; - let validation = Validation::new(header.alg); + // TODO: build validation only once or cache it (store it in key_source?) + // (problem: alg family is checked in jsonwebtoken but may change with store refresh) + let jwt_validation = &self.validation.to_jwt_validation(header.alg); let decoding_key = self.key_source.get_key(header).await?; - let token_data = decode::(token, &decoding_key, &validation)?; + let token_data = decode::(token, &decoding_key, jwt_validation)?; if let Some(ref checker) = self.claims_checker { if !checker.check(&token_data.claims) { @@ -147,12 +158,14 @@ mod tests { use jsonwebtoken::{Algorithm, Header}; use serde_json::Value; + use crate::validation::Validation; + use super::{Authorizer, KeySourceType}; #[tokio::test] async fn build_from_secret() { let h = Header::new(Algorithm::HS256); - let a = Authorizer::::build(&KeySourceType::Secret("xxxxxx"), None, None) + let a = Authorizer::::build(&KeySourceType::Secret("xxxxxx"), None, None, Validation::new()) .await .unwrap(); let k = a.key_source.get_key(h); @@ -171,7 +184,7 @@ mod tests { "e": "AQAB" }]} "#; - let a = Authorizer::::build(&KeySourceType::JwksString(jwks.to_owned()), None, None) + let a = Authorizer::::build(&KeySourceType::JwksString(jwks.to_owned()), None, None, Validation::new()) .await .unwrap(); let k = a.key_source.get_key(Header::new(Algorithm::RS256)); @@ -180,42 +193,70 @@ mod tests { #[tokio::test] async fn build_from_file() { - let a = Authorizer::::build(&KeySourceType::RSA("../config/rsa-public1.pem".to_owned()), None, None) - .await - .unwrap(); + let a = Authorizer::::build( + &KeySourceType::RSA("../config/rsa-public1.pem".to_owned()), + None, + None, + Validation::new(), + ) + .await + .unwrap(); let k = a.key_source.get_key(Header::new(Algorithm::RS256)); assert!(k.await.is_ok()); - let a = Authorizer::::build(&KeySourceType::EC("../config/ecdsa-public1.pem".to_owned()), None, None) - .await - .unwrap(); + let a = Authorizer::::build( + &KeySourceType::EC("../config/ecdsa-public1.pem".to_owned()), + None, + None, + Validation::new(), + ) + .await + .unwrap(); let k = a.key_source.get_key(Header::new(Algorithm::ES256)); assert!(k.await.is_ok()); - let a = Authorizer::::build(&KeySourceType::ED("../config/ed25519-public1.pem".to_owned()), None, None) - .await - .unwrap(); + let a = Authorizer::::build( + &KeySourceType::ED("../config/ed25519-public1.pem".to_owned()), + None, + None, + Validation::new(), + ) + .await + .unwrap(); let k = a.key_source.get_key(Header::new(Algorithm::EdDSA)); assert!(k.await.is_ok()); } #[tokio::test] async fn build_file_errors() { - let a = Authorizer::::build(&KeySourceType::RSA("./config/does-not-exist.pem".to_owned()), None, None).await; + let a = Authorizer::::build( + &KeySourceType::RSA("./config/does-not-exist.pem".to_owned()), + None, + None, + Validation::new(), + ) + .await; println!("{:?}", a.as_ref().err()); assert!(a.is_err()); } #[tokio::test] async fn build_jwks_url_error() { - let a = Authorizer::::build(&KeySourceType::Jwks("://xxxx".to_owned()), None, None).await; + let a = + Authorizer::::build(&KeySourceType::Jwks("://xxxx".to_owned()), None, None, Validation::default()).await; println!("{:?}", a.as_ref().err()); assert!(a.is_err()); } #[tokio::test] async fn build_discovery_url_error() { - let a = Authorizer::::build(&KeySourceType::Discovery("://xxxx".to_owned()), None, None).await; + let a = Authorizer::::build( + &KeySourceType::Discovery("://xxxx".to_owned()), + None, + None, + Validation::default(), + ) + .await; println!("{:?}", a.as_ref().err()); assert!(a.is_err()); } diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index 13d2418..5f3dc6c 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -17,6 +17,7 @@ use tower_service::Service; use crate::authorizer::{Authorizer, FnClaimsChecker, KeySourceType}; use crate::error::InitError; use crate::jwks::key_store_manager::Refresh; +use crate::validation::Validation; use crate::{AuthError, RefreshStrategy}; /// Authorizer Layer builder @@ -30,6 +31,7 @@ where key_source_type: KeySourceType, refresh: Option, claims_checker: Option>, + validation: Option, } /// authorization layer builder @@ -43,6 +45,7 @@ where key_source_type: KeySourceType::Discovery(issuer.to_string()), refresh: Default::default(), claims_checker: None, + validation: None, } } @@ -52,6 +55,7 @@ where key_source_type: KeySourceType::Jwks(url.to_owned()), refresh: Default::default(), claims_checker: None, + validation: None, } } @@ -61,6 +65,7 @@ where key_source_type: KeySourceType::RSA(path.to_owned()), refresh: Default::default(), claims_checker: None, + validation: None, } } @@ -70,6 +75,7 @@ where key_source_type: KeySourceType::EC(path.to_owned()), refresh: Default::default(), claims_checker: None, + validation: None, } } @@ -79,6 +85,7 @@ where key_source_type: KeySourceType::ED(path.to_owned()), refresh: Default::default(), claims_checker: None, + validation: None, } } @@ -88,6 +95,7 @@ where key_source_type: KeySourceType::Secret(secret), refresh: Default::default(), claims_checker: None, + validation: None, } } @@ -120,9 +128,16 @@ where self } + pub fn validation(mut self, validation: Validation) -> JwtAuthorizer { + self.validation = Some(validation); + + self + } + /// Build axum layer - pub async fn layer(&self) -> Result, InitError> { - let auth = Arc::new(Authorizer::build(&self.key_source_type, self.claims_checker.clone(), self.refresh).await?); + pub async fn layer(self) -> Result, InitError> { + let val = self.validation.unwrap_or_default(); + let auth = Arc::new(Authorizer::build(&self.key_source_type, self.claims_checker, self.refresh, val).await?); Ok(AsyncAuthorizationLayer::new(auth)) } } diff --git a/jwt-authorizer/src/lib.rs b/jwt-authorizer/src/lib.rs index ec6cfe5..3c1a88f 100644 --- a/jwt-authorizer/src/lib.rs +++ b/jwt-authorizer/src/lib.rs @@ -8,12 +8,14 @@ use serde::de::DeserializeOwned; pub use self::error::AuthError; pub use jwks::key_store_manager::{Refresh, RefreshStrategy}; pub use layer::JwtAuthorizer; +pub use validation::Validation; pub mod authorizer; pub mod error; pub mod jwks; pub mod layer; mod oidc; +pub mod validation; /// Claims serialized using T #[derive(Debug, Clone, Copy, Default)] diff --git a/jwt-authorizer/src/validation.rs b/jwt-authorizer/src/validation.rs new file mode 100644 index 0000000..d354708 --- /dev/null +++ b/jwt-authorizer/src/validation.rs @@ -0,0 +1,129 @@ +use std::collections::HashSet; + +use jsonwebtoken::Algorithm; + +/// Defines the jwt validation parameters (with defaults simplifying configuration). +pub struct Validation { + /// Add some leeway (in seconds) to the `exp` and `nbf` validation to + /// account for clock skew. + /// + /// Defaults to `60`. + pub leeway: u64, + /// Whether to validate the `exp` field. + /// + /// Defaults to `true`. + pub validate_exp: bool, + /// Whether to validate the `nbf` field. + /// + /// Defaults to `false`. + pub validate_nbf: bool, + /// If it contains a value, the validation will check that the `aud` claim value is in the values provided. + /// + /// Defaults to `None`. + pub aud: Option>, + /// If it contains a value, the validation will check that the `iss` claim value is in the values provided. + /// + /// Defaults to `None`. + pub iss: Option>, + + /// Whether to validate the JWT signature. Very insecure to turn that off! + /// + /// Defaults to true. + pub validate_signature: bool, +} + +impl Validation { + /// new Validation with default values + pub fn new() -> Self { + Default::default() + } + + /// check that the `iss` claim is a member of the values provided + pub fn iss(mut self, items: &[T]) -> Self { + self.iss = Some(items.iter().map(|x| x.to_string()).collect()); + + self + } + + /// check that the `aud` claim is a member of the items provided + pub fn aud(mut self, items: &[T]) -> Self { + self.aud = Some(items.iter().map(|x| x.to_string()).collect()); + + self + } + + /// enables or disables exp validation + pub fn exp(mut self, val: bool) -> Self { + self.validate_exp = val; + + self + } + + /// enables or disables nbf validation + pub fn nbf(mut self, val: bool) -> Self { + self.validate_nbf = val; + + self + } + + /// Add some leeway (in seconds) to the `exp` and `nbf` validation to + /// account for clock skew. + pub fn leeway(mut self, value: u64) -> Self { + self.leeway = value; + + self + } + + /// Whether to validate the JWT cryptographic signature + /// Very insecure to turn that off, only do it if you know what you're doing. + pub fn disable_validation(mut self) -> Self { + self.validate_signature = false; + + self + } + + pub(crate) fn to_jwt_validation(&self, alg: Algorithm) -> jsonwebtoken::Validation { + let required_claims = if self.validate_exp { + let mut claims = HashSet::with_capacity(1); + claims.insert("exp".to_owned()); + claims + } else { + HashSet::with_capacity(0) + }; + + let aud = self.aud.clone().map(|v| HashSet::from_iter(v.into_iter())); + let iss = self.iss.clone().map(|v| HashSet::from_iter(v.into_iter())); + + let mut jwt_validation = jsonwebtoken::Validation::default(); + + jwt_validation.required_spec_claims = required_claims; + jwt_validation.leeway = self.leeway; + jwt_validation.validate_exp = self.validate_exp; + jwt_validation.validate_nbf = self.validate_nbf; + jwt_validation.iss = iss; + jwt_validation.aud = aud; + jwt_validation.sub = None; + jwt_validation.algorithms = vec![alg]; + if !self.validate_signature { + jwt_validation.insecure_disable_signature_validation(); + } + + jwt_validation + } +} + +impl Default for Validation { + fn default() -> Self { + Validation { + leeway: 60, + + validate_exp: true, + validate_nbf: false, + + iss: None, + aud: None, + + validate_signature: true, + } + } +} diff --git a/jwt-authorizer/tests/common/mod.rs b/jwt-authorizer/tests/common/mod.rs new file mode 100644 index 0000000..ac27db5 --- /dev/null +++ b/jwt-authorizer/tests/common/mod.rs @@ -0,0 +1,47 @@ +#![allow(dead_code)] + +use lazy_static::lazy_static; +use serde_json::{json, Value}; + +lazy_static! { + pub static ref JWKS_RSA1: Value = json!({ + "keys": [{ + "kty": "RSA", + "n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s", + "e": "AQAB", + "kid": "rsa01", + "alg": "RS256", + "use": "sig" + }] + }); + pub static ref JWKS_RSA2: Value = json!({ + "keys": [{ + "kty": "RSA", + "n": "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ", + "e": "AQAB", + "kid": "rsa02", + "alg": "RS256", + "use": "sig" + }] + }); + pub static ref JWKS_EC1: Value = json!({ + "keys": [{ + "kty": "EC", + "crv": "P-256", + "x": "MZiwc5EVP_E3vkd2oKedr4lWVMN9vgdyBBpBIVFJjwY", + "y": "1npLU75B6M0mb01zUAVoeYJSDOlQJmvjBdqLPjJvy3Y", + "kid": "ec01", + "alg": "ES256", + "use": "sig" + }] + }); +} + +pub const JWT_RSA1_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAxIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiYXVkIjoiYXVkMSIsImV4cCI6MjAwMDAwMDAwMCwibmJmIjoxNTE2MjM5MDIyfQ.d29YS5U-Cfur1DDxOeiYBxlEzRFQrVovuFdyIlrMpAfLLWudtpqiMxHKfTEM0ohHrk4ahf7nWhMamuiQUOEZSYpx7ze-4f47FGU4RxFLVSZw7VUWFrNYKkmRlgFsCbpdRLb5in6YDIaqrUnr2tqF9c3vUo_15lLgNn__xDG9_49A8UvbNFGvDm_z53aYGBPdgWVmwrRU5lmHH0tYcLMyiqQKfnM4jr__klIeVGBpJ2V_2qZyHvvevEtiiV7EGWZxaA-cYzeaO-_24nVBvPYrcdib84pz-a4JWhmnUobfzvtbdKEy12abxB2TvpzikBbX5etiDx92cPP_9Kf_51BncmwC_anRIwCSEe5TEgduihYS9yucOGgjP09sjlPPvdGAE6vcl35eR2fizJo7KU6Ol8DoUSDMhuPS-KQ_bFpCDK1iPtsXw514WZQZL1qXF61yd5QZ3wvckR8s_pV0XcFHWg_TpupNC3Yn6zlYU9l8NLkWiIudJVAM2pe-MSu292FyR2ytLISrNqHtk-e4_MIoviqyswvmtHZivoFWkq_CE2V9RyLX4WXaVEJLf4FihfCMFGZVfON2B8N2PfoPMuAlE1otQerbKwwR_TYjOFJRG1HdIDqvNDQ-LeJDKKX0NzCHwoJIqC-X7m6F1QIcaupOWnXyoSndvsi6g1jAlP_fTCI"; +pub const JWT_RSA2_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAyIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.tWyA4ve2CY6GruBch_qIf8f1PgCEhqmrZ1J5XBuwO_v-P-PSLe3MWpkPAMdIDE5QE19ItUcGdJblhiyPb0tJJtrDHVYER7q8X4fOjQjY_NlFK6Bd1GtZS2DCA5EPxIX8l7Jpn8fPvbyamagLwnB_waQaYBteTGnOkLmz3F3sqC8KdO9lyu5v7BknC1f56ZOvr_DiInkTiAsTWqX4nS2KYRjcz4HcxcPO7O0CFXqcOTF_e3ntmq4rQV9LHCaEnuXj2WZtnX423CMkcG0uYzsnmWAMPB6IlDKejPnAJThMjjuJhze1gGbP1U8c53UbEhfHEZgJ2N634YEXMfsojZ5VzQ"; +pub const JWT_EC1_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiYXVkIjoiYXVkMSIsImV4cCI6MjAwMDAwMDAwMCwibmJmIjoxNTE2MjM5MDIyfQ.orTVTRdQnktCg1Ar_mo9IvN__5-s-q5oCZaUUWaL2I8HAkIq68GV6ACqvhrxQMB-OInX0hY9pBWGYbrFJjCwKA"; +pub const JWT_EC2_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDIifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiYXVkIjoiYXVkMSIsImV4cCI6MjAwMDAwMDAwMCwibmJmIjoxNTE2MjM5MDIyfQ.QnsVM9CA11VpwDr6e_aHzxlLSXTQ7yVH5oxTR1yIWBPKnosjk1EIIBMcSjD81fZCrON2kX4TNkfSCxSCL8GI3g"; +pub const JWT_EC1_EXP_KO: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJib2IiLCJleHAiOjE1MTYyMzkwMjIsIm5iZiI6MTUxNjIzOTAyMn0.MNmY66S3NgSAbWwZP0hfC5pme3SM7B3yvFhBFLQH-cU3enP0G8bBzDOhpjmli9uKQitkIQxffwu2Au9wTUraTQ"; +pub const JWT_EC1_NBF_KO: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJib2IiLCJleHAiOjIwMDAwMDAwMDAsIm5iZiI6MjAwMDAwMDAwMH0.d5MRfwcToMxR7O7NEt3qUj-MUKKpG9BZW1w6ihyfN95ZULoMajr7mtYY2R2LS96oBYgp3OdlR4tkHmdqDpvCSA"; +pub const JWT_ED1_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImVkMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiYXVkIjoiYXVkMSIsImV4cCI6MjAwMDAwMDAwMCwibmJmIjoxNTE2MjM5MDIyfQ.U2eaP1EzRiLDRRPJTVjOMy4y40uAiW8MeryWJAjU-QPxU_PnuzatvrRjntTcdW7hXx0EWIezecJuXzp2UrBqAw"; +pub const JWT_ED2_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImVkMDIifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiYXVkIjoiYXVkMSIsImV4cCI6MjAwMDAwMDAwMCwibmJmIjoxNTE2MjM5MDIyfQ.xFrGhImKI1irksznuU9DoLk24bbdHhurbVRoUdZSb_FNlav1Jw49eMyKfeJUPy8IdMCtnG33K9xHuCRjm5IcAA"; diff --git a/jwt-authorizer/tests/integration_tests.rs b/jwt-authorizer/tests/integration_tests.rs index 2494852..de8885a 100644 --- a/jwt-authorizer/tests/integration_tests.rs +++ b/jwt-authorizer/tests/integration_tests.rs @@ -14,52 +14,15 @@ use hyper::Body; use jwt_authorizer::{JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde_json::Value; use tower::Service; use tower::ServiceExt; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -lazy_static! { - static ref JWKS_RSA1: Value = json!({ - "keys": [{ - "kty": "RSA", - "n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s", - "e": "AQAB", - "kid": "rsa01", - "alg": "RS256", - "use": "sig" - }] - }); - static ref JWKS_RSA2: Value = json!({ - "keys": [{ - "kty": "RSA", - "n": "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ", - "e": "AQAB", - "kid": "rsa02", - "alg": "RS256", - "use": "sig" - }] - }); - static ref JWKS_EC1: Value = json!({ - "keys": [{ - "kty": "EC", - "crv": "P-256", - "x": "MZiwc5EVP_E3vkd2oKedr4lWVMN9vgdyBBpBIVFJjwY", - "y": "1npLU75B6M0mb01zUAVoeYJSDOlQJmvjBdqLPjJvy3Y", - "kid": "ec01", - "alg": "ES256", - "use": "sig" - }] - }); -} +use crate::common::{JWT_RSA1_OK, JWT_RSA2_OK}; -const JWT_RSA1_OK: &str = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAxIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.pmm8Kdk-SvycXIGpWb1R0DuP5nlB7w4QQS7trhN_OjOpbk0A8F_lC4BdClz3rol2Pgo61lcFckJgjNBj34DQGeTGOtvxdiUXNgi1aKiXH4AyPzZeZx30PgFxa1fxhuZhBAj6xIZKBSBQvVyjeVQzAScINRCBX8zfCaXSU1ZCUkJl5vbD7zT-cYIFU76we9HcIYKRXwTiAyoNn3Lixa1H3_t5sbx3om2WlIB2x-sGpoDFDjorcuJT1yQx3grTRTBzHyRBRjZ3e8wrMbiacy-m3WoEFdkssQgYi_dSQH0hvxgacvGWayK0UqD7O5UL6EzTA2feXbgA_68o5gfvSnM8CUsPut5gZr-gwVbQKPbBdCQtl_wXIMot7UNKYEiFV38x5EmUr-ShzQcditW6fciguuY1Qav502UE1UMXvt5p8-kYxw2AaaVd6iTgQBzkBrtvywMYWzIwzGNA70RvUhI2rlgcn8GEU_51Tv_NMHjp6CjDbAxQVKa0PlcRE4pd6yk_IJSR4Nska_8BQZdPbsFn--z_XHEDoRZQ1C1M6m77xVndg3zX0sNQPXfWsttCbBmaHvMKTOp0cH9rlWB9r9nTo9fn8jcfqlak2O2IAzfzsOdVfUrES6T1UWkWobs9usGgqJuIkZHbDd4tmXyPRT4wrU7hxEyE9cuvuZPAi8GYt80"; -const JWT_RSA2_OK: &str = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAyIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.tWyA4ve2CY6GruBch_qIf8f1PgCEhqmrZ1J5XBuwO_v-P-PSLe3MWpkPAMdIDE5QE19ItUcGdJblhiyPb0tJJtrDHVYER7q8X4fOjQjY_NlFK6Bd1GtZS2DCA5EPxIX8l7Jpn8fPvbyamagLwnB_waQaYBteTGnOkLmz3F3sqC8KdO9lyu5v7BknC1f56ZOvr_DiInkTiAsTWqX4nS2KYRjcz4HcxcPO7O0CFXqcOTF_e3ntmq4rQV9LHCaEnuXj2WZtnX423CMkcG0uYzsnmWAMPB6IlDKejPnAJThMjjuJhze1gGbP1U8c53UbEhfHEZgJ2N634YEXMfsojZ5VzQ"; -// const JWT_EC1_OK: &str = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.AsAX8XQdsQMI7NGNJOPE8LFFaKJ_nYXeKBwl2NZACbPhCiRj7FgxIw0UVcpmRVzK0BNbb9S4lFocaTLo9DsCeQ"; -// const JWT_EC2_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDIifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.DJFNPyfuL5-ifcAxRCvneo7SdtDu0cfJyYmv2Gl4rmJOjKlzDx3GDamYa0cGLy8zcYYdpDMJ-s1WKzlGC_Hiyw"; -// const JWT_ED1_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImVkMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.5bFOZqc-lBFy4gFifQ_CTx1A3R6Nry71gdi7KH2GGvTZQC_ZI1vNbqGnWQhpR6n_jUd9ICUc0pPI5iLCB6K1Bg"; -// const JWT_ED2_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImVkMDIifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.Yfe88E26UEJ8x13h8xv2XtBrQ7O5E5UtS9t6-hRbo_pMSxKui13X0uNleRPHaZFfzK4AO033m8gHYHxQDLkTCg"; +mod common; /// Static variable to ensure that logging is only initialized once. pub static INITIALIZED: Once = Once::new(); @@ -99,7 +62,7 @@ fn discovery(uri: &str) -> Json { async fn jwks() -> Json { Arc::clone(&JWKS_COUNTER).fetch_add(1, Ordering::Relaxed); - Json(JWKS_RSA1.clone()) + Json(common::JWKS_RSA1.clone()) } fn run_jwks_server() -> String { @@ -166,7 +129,7 @@ async fn make_proteced_request(app: &mut Router, bearer: &str) -> Response { .call( Request::builder() .uri("/protected") - .header("Authorization", bearer) + .header("Authorization", format!("Bearer {bearer}")) .body(Body::empty()) .unwrap(), ) diff --git a/jwt-authorizer/tests/tests.rs b/jwt-authorizer/tests/tests.rs index 235f8bf..6575ccf 100644 --- a/jwt-authorizer/tests/tests.rs +++ b/jwt-authorizer/tests/tests.rs @@ -1,3 +1,5 @@ +mod common; + #[cfg(test)] mod tests { use axum::{ @@ -8,11 +10,11 @@ mod tests { Router, }; use http::{header, HeaderValue}; - use jwt_authorizer::{JwtAuthorizer, JwtClaims}; + use jwt_authorizer::{validation::Validation, JwtAuthorizer, JwtClaims}; use serde::Deserialize; use tower::ServiceExt; - const JWT_RSA_OK: &str = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1yc2EifQ.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.K9QFjvVquRF2-Wt1QRfipOGwiYsmRs7SAwqKskHemFb9BRRZutpfV4oEoHaXMLomTUe8rH0TMjpKcweYK_H1I8D4r-mAN216oUfxCQiFWDB8T2VBI8um-efUg67i2myDZJr5VXdZH8ywj7bn9LyNS4I_xT-J3XvsngeCpuxVSRiYu4FkcUkLrPzbu2sDyBXFqYO9FOorZ8sl0Ninc93fWT2uUrEG8jRyWCa4xpoqbKbm7CN7T2tOKF7mx_xdSPTeSM-U9mUiHsMOrXi1S05IM0hvNJrBduLS6sMTFWrVhis6zqnuxDOirwZS-aN0_SgMDnZTFPsCh8dkqFde1Pv1IYjZfr5OOHjQ9QWj6UDjam6M1eWVPK6QLlxv5bU_gnlAiHm9wJX38-REwmVhIJIBzKxsgJAu1gnRBxe36OM3rkgYxpB86YvfDyOoFlqx8erdxYv38AtvJibe4HB6KLndp_QMm5XXQsbfyEXWGe8hzDwozdhGeQsJXz7PcI3KPlv19PrUM8njElFpOiyfAEXwbtp1EZTzMZ4ZNF6LLFy1fpLcosgyp05o_2YMvngltSnN3v0IPncJx50StdYsoxPN9Ac_nH8VbNlHfmPHMklD1plof0pYf5SiL8yCQP9Uiw9NrN2PeQzbveMKF1T1UNbn2tefxoxr3k6sgWiMH_g_kkk"; + use crate::common; #[derive(Debug, Deserialize, Clone)] struct User { @@ -33,7 +35,7 @@ mod tests { .oneshot( Request::builder() .uri("/protected") - .header("Authorization", bearer) + .header("Authorization", format!("Bearer {bearer}")) .body(Body::empty()) .unwrap(), ) @@ -63,10 +65,24 @@ mod tests { #[tokio::test] async fn protected_with_jwt() { - let response = make_proteced_request(JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem"), JWT_RSA_OK).await; - + let response = make_proteced_request( + JwtAuthorizer::from_ed_pem("../config/ed25519-public2.pem"), + common::JWT_ED2_OK, + ) + .await; assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert_eq!(&body[..], b"hello: b@b.com"); + let response = + make_proteced_request(JwtAuthorizer::from_ec_pem("../config/ecdsa-public2.pem"), common::JWT_EC2_OK).await; + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert_eq!(&body[..], b"hello: b@b.com"); + + let response = + make_proteced_request(JwtAuthorizer::from_rsa_pem("../config/rsa-public2.pem"), common::JWT_RSA2_OK).await; + assert_eq!(response.status(), StatusCode::OK); let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); assert_eq!(&body[..], b"hello: b@b.com"); } @@ -82,16 +98,16 @@ mod tests { #[tokio::test] async fn protected_with_claims_check() { let rsp_ok = make_proteced_request( - JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").check(|_| true), - JWT_RSA_OK, + JwtAuthorizer::from_rsa_pem("../config/rsa-public2.pem").check(|_| true), + common::JWT_RSA2_OK, ) .await; assert_eq!(rsp_ok.status(), StatusCode::OK); let rsp_ko = make_proteced_request( - JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").check(|_| false), - JWT_RSA_OK, + JwtAuthorizer::from_rsa_pem("../config/rsa-public2.pem").check(|_| false), + common::JWT_RSA2_OK, ) .await; @@ -110,7 +126,8 @@ mod tests { // but should be 500 when checking. #[tokio::test] async fn protected_with_bad_jwks_url() { - let response = make_proteced_request(JwtAuthorizer::from_jwks_url("http://bad-url/xxx/yyy"), JWT_RSA_OK).await; + let response = + make_proteced_request(JwtAuthorizer::from_jwks_url("http://bad-url/xxx/yyy"), common::JWT_RSA1_OK).await; assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } @@ -128,4 +145,140 @@ mod tests { assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } + + // -------------------- + // VALIDATION + // --------------------- + #[tokio::test] + async fn validate_signature() { + let response = make_proteced_request( + JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").validation(Validation::new().disable_validation()), + common::JWT_EC2_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + + let response = make_proteced_request( + JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").validation(Validation::new()), + common::JWT_EC2_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn validate_iss() { + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().iss(&["bad-iss"])), + common::JWT_EC1_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new()), + common::JWT_EC1_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem") + .validation(Validation::new().iss(&["http://localhost:3001"])), + common::JWT_EC1_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn validate_aud() { + let response = make_proteced_request( + JwtAuthorizer::from_ed_pem("../config/ed25519-public1.pem").validation(Validation::new().aud(&["bad-aud"])), + common::JWT_ED1_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + let response = make_proteced_request( + JwtAuthorizer::from_ed_pem("../config/ed25519-public1.pem").validation(Validation::new()), + common::JWT_ED1_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + + let response = make_proteced_request( + JwtAuthorizer::from_ed_pem("../config/ed25519-public1.pem").validation(Validation::new().aud(&["aud1"])), + common::JWT_ED1_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn validate_exp() { + // DEFAULT -> ENABLED + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new()), + common::JWT_EC1_EXP_KO, + ) + .await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + // DISABLED + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().exp(false)), + common::JWT_EC1_EXP_KO, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + + // ENABLED + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().exp(true)), + common::JWT_EC1_EXP_KO, + ) + .await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().exp(true)), + common::JWT_EC1_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn validate_nbf() { + // DEFAULT -> DISABLED + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new()), + common::JWT_EC1_NBF_KO, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + + // DISABLED + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().nbf(false)), + common::JWT_EC1_NBF_KO, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + + // ENABLED + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().nbf(true)), + common::JWT_EC1_NBF_KO, + ) + .await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + let response = make_proteced_request( + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().nbf(true)), + common::JWT_EC1_OK, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + } }