From ae44a8e9615ea9d6b33e73a8c691429da00673f8 Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 6 Feb 2023 23:41:45 +0100 Subject: [PATCH] feat: integration tests --- Cargo.lock | 1 + demo-server/request.http | 6 +- demo-server/src/main.rs | 2 +- demo-server/src/oidc_provider/mod.rs | 39 +-- jwt-authorizer/Cargo.toml | 1 + jwt-authorizer/tests/integration_tests.rs | 312 ++++++++++++++++++++++ 6 files changed, 342 insertions(+), 19 deletions(-) create mode 100644 jwt-authorizer/tests/integration_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 6dd07eb..0c799e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -726,6 +726,7 @@ dependencies = [ "http", "hyper", "jsonwebtoken", + "lazy_static", "pin-project", "reqwest", "serde", diff --git a/demo-server/request.http b/demo-server/request.http index 59d3e9f..5b12401 100644 --- a/demo-server/request.http +++ b/demo-server/request.http @@ -4,17 +4,17 @@ GET http://localhost:3000/public ### Protected RSA GET http://localhost:3000/api/protected Content-Type: application/json -Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1yc2EifQ.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.K9QFjvVquRF2-Wt1QRfipOGwiYsmRs7SAwqKskHemFb9BRRZutpfV4oEoHaXMLomTUe8rH0TMjpKcweYK_H1I8D4r-mAN216oUfxCQiFWDB8T2VBI8um-efUg67i2myDZJr5VXdZH8ywj7bn9LyNS4I_xT-J3XvsngeCpuxVSRiYu4FkcUkLrPzbu2sDyBXFqYO9FOorZ8sl0Ninc93fWT2uUrEG8jRyWCa4xpoqbKbm7CN7T2tOKF7mx_xdSPTeSM-U9mUiHsMOrXi1S05IM0hvNJrBduLS6sMTFWrVhis6zqnuxDOirwZS-aN0_SgMDnZTFPsCh8dkqFde1Pv1IYjZfr5OOHjQ9QWj6UDjam6M1eWVPK6QLlxv5bU_gnlAiHm9wJX38-REwmVhIJIBzKxsgJAu1gnRBxe36OM3rkgYxpB86YvfDyOoFlqx8erdxYv38AtvJibe4HB6KLndp_QMm5XXQsbfyEXWGe8hzDwozdhGeQsJXz7PcI3KPlv19PrUM8njElFpOiyfAEXwbtp1EZTzMZ4ZNF6LLFy1fpLcosgyp05o_2YMvngltSnN3v0IPncJx50StdYsoxPN9Ac_nH8VbNlHfmPHMklD1plof0pYf5SiL8yCQP9Uiw9NrN2PeQzbveMKF1T1UNbn2tefxoxr3k6sgWiMH_g_kkk +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAxIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.pmm8Kdk-SvycXIGpWb1R0DuP5nlB7w4QQS7trhN_OjOpbk0A8F_lC4BdClz3rol2Pgo61lcFckJgjNBj34DQGeTGOtvxdiUXNgi1aKiXH4AyPzZeZx30PgFxa1fxhuZhBAj6xIZKBSBQvVyjeVQzAScINRCBX8zfCaXSU1ZCUkJl5vbD7zT-cYIFU76we9HcIYKRXwTiAyoNn3Lixa1H3_t5sbx3om2WlIB2x-sGpoDFDjorcuJT1yQx3grTRTBzHyRBRjZ3e8wrMbiacy-m3WoEFdkssQgYi_dSQH0hvxgacvGWayK0UqD7O5UL6EzTA2feXbgA_68o5gfvSnM8CUsPut5gZr-gwVbQKPbBdCQtl_wXIMot7UNKYEiFV38x5EmUr-ShzQcditW6fciguuY1Qav502UE1UMXvt5p8-kYxw2AaaVd6iTgQBzkBrtvywMYWzIwzGNA70RvUhI2rlgcn8GEU_51Tv_NMHjp6CjDbAxQVKa0PlcRE4pd6yk_IJSR4Nska_8BQZdPbsFn--z_XHEDoRZQ1C1M6m77xVndg3zX0sNQPXfWsttCbBmaHvMKTOp0cH9rlWB9r9nTo9fn8jcfqlak2O2IAzfzsOdVfUrES6T1UWkWobs9usGgqJuIkZHbDd4tmXyPRT4wrU7hxEyE9cuvuZPAi8GYt80 ### Protected EC GET http://localhost:3000/api/protected Content-Type: application/json -Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImtleS1lYyJ9.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.r0qaeYJWhjEybhNPgrrFwLBTeLMYtL4IJqOxZyxH9m-JYWqzV0R0WyYQIkf_tQ1UmzqHc9_xzUZnzSjTeEwDHw +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.YMQHWpGLJ3P59SvPX-RIW3uT5rfzShzcP1TNcaXr0VnsxCXYO0og0c3_O30no0D_ct0hOUJINY5tBsok-66Gzw ### Protected Ed GET http://localhost:3000/api/protected Content-Type: application/json -Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImtleS1lZCJ9.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.XAx9msioheXEH1XUEIWMHGBg25JOpBHqcgL_ou_S3fwVht2TbKRiDZ4G6ZyEtn57hCbOy250zTD_g0EbaMGwAg +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImVkMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.5bFOZqc-lBFy4gFifQ_CTx1A3R6Nry71gdi7KH2GGvTZQC_ZI1vNbqGnWQhpR6n_jUd9ICUc0pPI5iLCB6K1Bg ### 401 (no token) GET http://localhost:3000/api/protected diff --git a/demo-server/src/main.rs b/demo-server/src/main.rs index 3f1497d..a1e7bfe 100644 --- a/demo-server/src/main.rs +++ b/demo-server/src/main.rs @@ -19,7 +19,7 @@ struct User { async fn main() -> Result<(), InitError> { tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( - std::env::var("RUST_LOG").unwrap_or_else(|_| "info,axum_poc=debug,tower_http=debug".into()), + std::env::var("RUST_LOG").unwrap_or_else(|_| "info,jwt_authorizer=debug,tower_http=debug".into()), )) .with(tracing_subscriber::fmt::layer()) .init(); diff --git a/demo-server/src/oidc_provider/mod.rs b/demo-server/src/oidc_provider/mod.rs index a2ff480..6fc0662 100644 --- a/demo-server/src/oidc_provider/mod.rs +++ b/demo-server/src/oidc_provider/mod.rs @@ -37,42 +37,42 @@ async fn jwks() -> Json { let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap(); let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("key-rsa"); + 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/private_rsa_key_pkcs8.pem")).unwrap(); let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("rsa01"); + 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-private.pem"), Some(EcCurve::P256)).unwrap(); let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("key-ec"); + 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/private_ecdsa_key.pem"), Some(EcCurve::P256)).unwrap(); let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("ec01"); + pk.set_key_id("ec02"); pk.set_algorithm("ES256"); pk.set_key_use("sig"); kset.keys.push(pk); let keypair = EdKeyPair::from_pem(include_bytes!("../../../config/ed25519-private.pem")).unwrap(); let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("key-ed"); + 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/private_ed25519_key.pem")).unwrap(); let mut pk = keypair.to_jwk_public_key(); - pk.set_key_id("ed01"); + pk.set_key_id("ed02"); pk.set_algorithm("EdDSA"); pk.set_key_use("sig"); kset.keys.push(pk); @@ -114,18 +114,27 @@ pub async fn tokens() -> Json { nbf: 1516239022, // Jan 2018 }; - let rsa_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap(); - let ec_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec256-private.pem")).unwrap(); - let ed_key = EncodingKey::from_ed_pem(include_bytes!("../../../config/ed25519-private.pem")).unwrap(); + let rsa1_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap(); + let rsa2_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/private_rsa_key_pkcs8.pem")).unwrap(); + let ec1_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec256-private.pem")).unwrap(); + let ec2_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/private_ecdsa_key.pem")).unwrap(); + let ed1_key = EncodingKey::from_ed_pem(include_bytes!("../../../config/ed25519-private.pem")).unwrap(); + let ed2_key = EncodingKey::from_ed_pem(include_bytes!("../../../config/ed25519-private.pem")).unwrap(); - let rsa_token = encode(&build_header(Algorithm::RS256, "key-rsa"), &claims, &rsa_key).unwrap(); - let ec_token = encode(&build_header(Algorithm::ES256, "key-ec"), &claims, &ec_key).unwrap(); - let ed_token = encode(&build_header(Algorithm::EdDSA, "key-ed"), &claims, &ed_key).unwrap(); + let rsa1_token = encode(&build_header(Algorithm::RS256, "rsa01"), &claims, &rsa1_key).unwrap(); + let rsa2_token = encode(&build_header(Algorithm::RS256, "rsa02"), &claims, &rsa2_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 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!({ - "rsa": rsa_token, - "ec": ec_token, - "ed": ed_token + "rsa01": rsa1_token, + "rsa02": rsa2_token, + "ec01": ec1_token, + "ec02": ec2_token, + "ed01": ed1_token, + "ed02": ed2_token, })) } diff --git a/jwt-authorizer/Cargo.toml b/jwt-authorizer/Cargo.toml index 0512bf6..50f0c9a 100644 --- a/jwt-authorizer/Cargo.toml +++ b/jwt-authorizer/Cargo.toml @@ -30,5 +30,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } [dev-dependencies] hyper = { version = "0.14", features = ["full"] } +lazy_static = "1.4.0" tower = { version = "0.4", features = ["util"] } wiremock = "0.5" diff --git a/jwt-authorizer/tests/integration_tests.rs b/jwt-authorizer/tests/integration_tests.rs new file mode 100644 index 0000000..c9c50e7 --- /dev/null +++ b/jwt-authorizer/tests/integration_tests.rs @@ -0,0 +1,312 @@ +use std::{ + net::{SocketAddr, TcpListener}, + sync::{ + atomic::{AtomicI16, Ordering}, + Arc, Once, + }, + thread, + time::Duration, +}; + +use axum::{response::Response, routing::get, Json, Router}; +use http::{Request, StatusCode}; +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 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" + }] + }); +} + +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"; + +/// Static variable to ensure that logging is only initialized once. +pub static INITIALIZED: Once = Once::new(); + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct User { + sub: String, +} + +lazy_static! { + static ref DISCOVERY_COUNTER: Arc = Arc::new(AtomicI16::new(0)); + static ref JWKS_COUNTER: Arc = Arc::new(AtomicI16::new(0)); +} + +struct Stats {} + +impl Stats { + fn reset() { + Arc::clone(&DISCOVERY_COUNTER).store(0, Ordering::Relaxed); + Arc::clone(&JWKS_COUNTER).store(0, Ordering::Relaxed); + } + fn jwks_counter() -> i16 { + Arc::clone(&JWKS_COUNTER).load(Ordering::Relaxed) + } + fn discovery_counter() -> i16 { + Arc::clone(&DISCOVERY_COUNTER).load(Ordering::Relaxed) + } +} + +fn discovery(uri: &str) -> Json { + Arc::clone(&DISCOVERY_COUNTER).fetch_add(1, Ordering::Relaxed); + let d = serde_json::json!({ "jwks_uri": format!("{}/jwks", uri) }); + + Json(d) +} + +async fn jwks() -> Json { + Arc::clone(&JWKS_COUNTER).fetch_add(1, Ordering::Relaxed); + + Json(JWKS_RSA1.clone()) +} + +fn run_jwks_server() -> String { + let listener = TcpListener::bind("0.0.0.0:0".parse::().unwrap()).unwrap(); + let addr = listener.local_addr().unwrap(); + let url = format!("http://{}:{}", addr.ip(), addr.port()); + + let url2 = url.clone(); + + let app = Router::new() + .route("/.well-known/openid-configuration", get(|| async move { discovery(&url2) })) + .route("/jwks", get(jwks)); + + tokio::spawn(async move { + axum::Server::from_tcp(listener) + .unwrap() + .serve(app.into_make_service()) + .await + .unwrap(); + }); + + url +} + +async fn app(jwt_auth: JwtAuthorizer) -> Router { + async fn public_handler() -> &'static str { + "public" + } + + async fn protected_handler() -> &'static str { + "protected" + } + + async fn protected_with_user(JwtClaims(user): JwtClaims) -> Json { + Json(user) + } + + let pub_route: Router = Router::new().route("/public", get(public_handler)); + let protected_route: Router = Router::new() + .route("/protected", get(protected_handler)) + .route("/protected-with-user", get(protected_with_user)) + .layer(jwt_auth.layer().await.unwrap()); + + Router::new().merge(pub_route).merge(protected_route) +} + +fn init_test() { + INITIALIZED.call_once(|| { + 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(); + }); + // reset counters + Stats::reset(); +} + +async fn make_proteced_request(app: &mut Router, bearer: &str) -> Response { + app.ready() + .await + .unwrap() + .call( + Request::builder() + .uri("/protected") + .header("Authorization", bearer) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap() +} + +async fn make_public_request(app: &mut Router) -> Response { + app.ready() + .await + .unwrap() + .call(Request::builder().uri("/public").body(Body::empty()).unwrap()) + .await + .unwrap() +} + +#[tokio::test] +async fn sequential_tests() { + // these tests must be executed sequentially + scenario1().await; + scenario2().await; + scenario3().await; + scenario4().await; +} + +async fn scenario1() { + init_test(); + let url = run_jwks_server(); + let auth: JwtAuthorizer = JwtAuthorizer::from_oidc(&url); + let mut app = app(auth).await; + assert_eq!(1, Stats::discovery_counter()); + assert_eq!(0, Stats::jwks_counter()); + // NO LOADING when public request + let r = make_public_request(&mut app).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(0, Stats::jwks_counter(), "sc1: public -> no loading"); + // LOADING - first jwt check + let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(1, Stats::jwks_counter(), "sc1: 1st check -> loading"); + // NO RELOADING same kid with OK + let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(1, Stats::jwks_counter(), "sc1: 2st check -> no loading"); + // NO RELEOADING, invalid kid, 401 + let r = make_proteced_request(&mut app, JWT_RSA2_OK).await; + assert_eq!(StatusCode::UNAUTHORIZED, r.status()); + assert_eq!(1, Stats::jwks_counter(), "sc1: 3st check (invalid kid) -> no loading"); +} + +/// SCENARIO2 +/// +/// Refresh strategy: INTERVAL +async fn scenario2() { + init_test(); + let url = run_jwks_server(); + let refresh = Refresh { + minimal_refresh_interval: Duration::from_millis(20), + refresh_interval: Duration::from_millis(40), + retry_interval: Duration::from_millis(0), + strategy: RefreshStrategy::Interval, + }; + let auth: JwtAuthorizer = JwtAuthorizer::from_oidc(&url).refresh(refresh); + let mut app = app(auth).await; + assert_eq!(1, Stats::discovery_counter()); + assert_eq!(0, Stats::jwks_counter()); + let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(1, Stats::jwks_counter()); + // NO RELOADING same kid + let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(1, Stats::jwks_counter()); + // RELEOADING, same kid, refresh_interval elapsed + thread::sleep(Duration::from_millis(41)); + let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(2, Stats::jwks_counter()); +} + +/// SCENARIO3 +/// +/// Refresh strategy: KeyNotFound +async fn scenario3() { + init_test(); + let url = run_jwks_server(); + let refresh = Refresh { + strategy: RefreshStrategy::KeyNotFound, + minimal_refresh_interval: Duration::from_millis(20), + retry_interval: Duration::from_millis(0), + ..Default::default() + }; + let auth: JwtAuthorizer = JwtAuthorizer::from_oidc(&url).refresh(refresh); + let mut app = app(auth).await; + assert_eq!(1, Stats::discovery_counter()); + assert_eq!(0, Stats::jwks_counter()); + // RELOADING getting keys first time + let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(1, Stats::jwks_counter()); + thread::sleep(Duration::from_millis(21)); + // NO RELOADING refresh interval elapsed, kid OK + let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(1, Stats::jwks_counter()); + // RELEOADING, unknown kid, refresh_interval elapsed + thread::sleep(Duration::from_millis(41)); + let r = make_proteced_request(&mut app, JWT_RSA2_OK).await; + assert_eq!(StatusCode::UNAUTHORIZED, r.status()); + assert_eq!(2, Stats::jwks_counter()); +} + +/// SCENARIO4 +/// +/// Refresh strategy: NoRefresh +async fn scenario4() { + init_test(); + let url = run_jwks_server(); + let refresh = Refresh { + strategy: RefreshStrategy::NoRefresh, + minimal_refresh_interval: Duration::from_millis(0), + retry_interval: Duration::from_millis(0), + ..Default::default() + }; + let auth: JwtAuthorizer = JwtAuthorizer::from_oidc(&url).refresh(refresh); + let mut app = app(auth).await; + assert_eq!(1, Stats::discovery_counter()); + assert_eq!(0, Stats::jwks_counter()); + // RELOADING getting keys first time + let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(1, Stats::jwks_counter()); + thread::sleep(Duration::from_millis(21)); + // NO RELOADING kid OK + let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; + assert_eq!(StatusCode::OK, r.status()); + assert_eq!(1, Stats::jwks_counter()); + // NO RELEOADING, unknown kid + thread::sleep(Duration::from_millis(41)); + let r = make_proteced_request(&mut app, JWT_RSA2_OK).await; + assert_eq!(StatusCode::UNAUTHORIZED, r.status()); + assert_eq!(1, Stats::jwks_counter()); +}