From f0a7e96875874b395a79cbcbc8cccc7bc5ef197b Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 6 Jan 2022 10:04:43 +0100 Subject: [PATCH] Add tests for private_key_jwt client auth --- crates/config/src/oauth2.rs | 6 + crates/jose/src/keystore/static_keystore.rs | 114 +++++++++--------- crates/warp-utils/src/filters/client.rs | 123 +++++++++++++++++++- 3 files changed, 181 insertions(+), 62 deletions(-) diff --git a/crates/config/src/oauth2.rs b/crates/config/src/oauth2.rs index 3e57a290..094208a5 100644 --- a/crates/config/src/oauth2.rs +++ b/crates/config/src/oauth2.rs @@ -61,6 +61,12 @@ impl JwksOrJwksUri { } } +impl From for JwksOrJwksUri { + fn from(jwks: JsonWebKeySet) -> Self { + Self::Jwks(jwks) + } +} + #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] #[serde(tag = "client_auth_method", rename_all = "snake_case")] pub enum OAuth2ClientAuthMethodConfig { diff --git a/crates/jose/src/keystore/static_keystore.rs b/crates/jose/src/keystore/static_keystore.rs index 0d50a3d3..ae2df453 100644 --- a/crates/jose/src/keystore/static_keystore.rs +++ b/crates/jose/src/keystore/static_keystore.rs @@ -18,21 +18,59 @@ use anyhow::bail; use async_trait::async_trait; use base64ct::{Base64UrlUnpadded, Encoding}; use digest::Digest; -use ecdsa::VerifyingKey; +use ecdsa::{SigningKey, VerifyingKey}; use p256::{NistP256, PublicKey}; -use pkcs1::EncodeRsaPublicKey; -use pkcs8::EncodePublicKey; -use rsa::{PublicKey as _, RsaPublicKey}; +use pkcs1::{DecodeRsaPrivateKey, EncodeRsaPublicKey}; +use pkcs8::{DecodePrivateKey, EncodePublicKey}; +use rsa::{PublicKey as _, RsaPrivateKey, RsaPublicKey}; use sha2::{Sha256, Sha384, Sha512}; use signature::{Signature, Signer, Verifier}; use super::{ExportJwks, SigningKeystore, VerifyingKeystore}; use crate::{iana::JsonWebSignatureAlgorithm, JsonWebKey, JsonWebKeySet, JwtHeader}; +// Generate with +// openssl genrsa 2048 +const TEST_RSA_PKCS1_PEM: &str = "-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1j7Y2CH6Ss8tgaNvcQPaRJKnCZD8ABqNPyKDWLQLph6Zi7gZ +GqmRtTzMuevo2ezpkbCiQAPEp1ms022P92bB+uqG7xmzHTzbwLtnq3OAdjmrnaFV +I4v89WHUsTXX9hiYOK5dOM81bNZ6muxWZ0L/xw4jVWe7xkqnp2Lluq0HknlzP5yJ +UEikf5BkpX0iyIu2/X4r8YVp8uzG34l/8qBx6k3rO2VkOQOSybZj1oij5KZCusnu +QjJLKWXCqJToWE6iVn+Q0N6ySDLgmJ7Zq0Sou/9N/oWKn94FOsouQgET5NuzoIFR +qTb321fQ8gbqt/OupBbBKEo1qUU+cS77TD/AuQIDAQABAoIBAQDLSZzmD+93lnf+ +f36ZxOcRk/nNGPYUfx0xH+VzgHthJ73YFlozs1xflQ5JB/DM/4BsziZWCX1KsctM +XrRxMt6y4GAidcc/4eQ+T1RCGfl1tKkDi/bGIOloSGjRsV5208V0WvZ3lh2CZUy2 +vbQKjUc3sFGUkzZYI7RLHosPA2mg78IVuSnqvNaU0TgA2KkaxWs6Ecr/ys80cUvj +KKj04DmX5xaXwUKmz353i5gIt3aY3G5CAw5fU/ocDKR8nzVCpBAGbRRiUaVKIT06 +APSkLDTUnxSYtHtDJGHjgU/TsvAwTA92J3ue5Ysu9xTE+WyHA6Rgux7RQSD/wWHr +LdRPwxPFAoGBAOytMPh/f2zKmotanjho0QNfhAUHoQUfPudYT0nnDceOsi1jYWbQ +c/wPeQQC4Hp/pTUrkSIQPEz/hSxzZ6RPxxuGB8O94I0uLwQK4V1UwbgfsRa9zQzW +n0kgKZ8w8h8B7qyiKyIAnZzvKtNEnKrzrct4HsN3OEoXTwuAUYlvWtQTAoGBAOe8 +0liNaH9V6ecZiojkRR1tiQkr/dCV+a13eXXaRA/8y/3wKCQ4idYncclQJTLKsAwW +hHuDd4uLgtifREVIBD2jGdlznNr9HQNuZgwjuUoH+r1YLGgiMWeVYSr0m8lyDlQl +BJKTAphrqo6VJWDAnM18v+by//yRleSjVMqZ3zmDAoGBAMpA0rl5EyagGON/g/hG +sl8Ej+hQdazP38yJbfCEsATaD6+z3rei6Yr8mfjwkG5+iGrgmT0XzMAsF909ndMP +jeIabqY6rBtZ3TnCJobAeG9lPctmVUVkX2h5QLhWdoJC/3iteNis2AQVam5yksOQ +S/O16ew2BHdkZds5Q/SDoYXbAoGAK9tVZ8LjWu30hXMU/9FLr0USoTS9JWOszAKH +byFuriPmq1lvD2PP2kK+yx2q3JD1fmQokIOR9Uvi6IJD1mTJwKyEcN3reppailKz +Z2q/X15hOsJcLR0DgpoHuKxwa1B1m8Ehu2etHxGJRtC9MTFiu5T3cIrenXskBhBP +NMSoNWcCgYAD3u3zdeVo3gVoxneS7GNVI2WBhjtqgNIbINuxGZvfztm7+vNPE6sQ +VL8i+09uoM1H6sXbe2XXORmtW0j/6MmYhSoBXNdqWTNAiyNRhwEQtowqgl5R7PBu +//QZTF1z62R9IKDMRG3f5Wn8e1Dys6tXBuG603g+Dkkc/km476mrgw== +-----END RSA PRIVATE KEY-----"; + +// Generate with +// openssl ecparam -genkey -name prime256v1 | openssl pkcs8 -topk8 -nocrypt +const TEST_ECDSA_PKCS8_PEM: &str = "-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+tHxet7G+uar2Cef +iYPb7jv3uzncFtwJ7RhDOvEA0fChRANCAATCKn2AEqa9785k+TmwkeCvLub8XGrF +ezE6bA/blaPVE3nu4SUVYKULRJQxNjeOSra8TQrlIS8e5ItbMn8Tv9KV +-----END PRIVATE KEY-----"; + #[derive(Default)] pub struct StaticKeystore { rsa_keys: HashMap, - es256_keys: HashMap>, + es256_keys: HashMap>, } impl StaticKeystore { @@ -41,6 +79,18 @@ impl StaticKeystore { StaticKeystore::default() } + pub fn add_test_rsa_key(&mut self) -> anyhow::Result<()> { + let rsa = RsaPrivateKey::from_pkcs1_pem(TEST_RSA_PKCS1_PEM)?; + self.add_rsa_key(rsa)?; + Ok(()) + } + + pub fn add_test_ecdsa_key(&mut self) -> anyhow::Result<()> { + let ecdsa = SigningKey::from_pkcs8_pem(TEST_ECDSA_PKCS8_PEM)?; + self.add_ecdsa_key(ecdsa)?; + Ok(()) + } + pub fn add_rsa_key(&mut self, key: rsa::RsaPrivateKey) -> anyhow::Result<()> { let pubkey: &RsaPublicKey = &key; let der = pubkey.to_pkcs1_der()?; @@ -57,7 +107,7 @@ impl StaticKeystore { Ok(()) } - pub fn add_ecdsa_key(&mut self, key: ecdsa::SigningKey) -> anyhow::Result<()> { + pub fn add_ecdsa_key(&mut self, key: SigningKey) -> anyhow::Result<()> { let pubkey: PublicKey = key.verifying_key().into(); let der = EncodePublicKey::to_public_key_der(&pubkey)?; let digest = { @@ -297,63 +347,15 @@ impl ExportJwks for StaticKeystore { #[cfg(test)] mod tests { - use ecdsa::SigningKey; - use pkcs1::DecodeRsaPrivateKey; - use pkcs8::DecodePrivateKey; - use rsa::RsaPrivateKey; - use super::*; - // Generate with - // openssl genrsa 2048 - const RSA_PKCS1_PEM: &str = "-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA1j7Y2CH6Ss8tgaNvcQPaRJKnCZD8ABqNPyKDWLQLph6Zi7gZ -GqmRtTzMuevo2ezpkbCiQAPEp1ms022P92bB+uqG7xmzHTzbwLtnq3OAdjmrnaFV -I4v89WHUsTXX9hiYOK5dOM81bNZ6muxWZ0L/xw4jVWe7xkqnp2Lluq0HknlzP5yJ -UEikf5BkpX0iyIu2/X4r8YVp8uzG34l/8qBx6k3rO2VkOQOSybZj1oij5KZCusnu -QjJLKWXCqJToWE6iVn+Q0N6ySDLgmJ7Zq0Sou/9N/oWKn94FOsouQgET5NuzoIFR -qTb321fQ8gbqt/OupBbBKEo1qUU+cS77TD/AuQIDAQABAoIBAQDLSZzmD+93lnf+ -f36ZxOcRk/nNGPYUfx0xH+VzgHthJ73YFlozs1xflQ5JB/DM/4BsziZWCX1KsctM -XrRxMt6y4GAidcc/4eQ+T1RCGfl1tKkDi/bGIOloSGjRsV5208V0WvZ3lh2CZUy2 -vbQKjUc3sFGUkzZYI7RLHosPA2mg78IVuSnqvNaU0TgA2KkaxWs6Ecr/ys80cUvj -KKj04DmX5xaXwUKmz353i5gIt3aY3G5CAw5fU/ocDKR8nzVCpBAGbRRiUaVKIT06 -APSkLDTUnxSYtHtDJGHjgU/TsvAwTA92J3ue5Ysu9xTE+WyHA6Rgux7RQSD/wWHr -LdRPwxPFAoGBAOytMPh/f2zKmotanjho0QNfhAUHoQUfPudYT0nnDceOsi1jYWbQ -c/wPeQQC4Hp/pTUrkSIQPEz/hSxzZ6RPxxuGB8O94I0uLwQK4V1UwbgfsRa9zQzW -n0kgKZ8w8h8B7qyiKyIAnZzvKtNEnKrzrct4HsN3OEoXTwuAUYlvWtQTAoGBAOe8 -0liNaH9V6ecZiojkRR1tiQkr/dCV+a13eXXaRA/8y/3wKCQ4idYncclQJTLKsAwW -hHuDd4uLgtifREVIBD2jGdlznNr9HQNuZgwjuUoH+r1YLGgiMWeVYSr0m8lyDlQl -BJKTAphrqo6VJWDAnM18v+by//yRleSjVMqZ3zmDAoGBAMpA0rl5EyagGON/g/hG -sl8Ej+hQdazP38yJbfCEsATaD6+z3rei6Yr8mfjwkG5+iGrgmT0XzMAsF909ndMP -jeIabqY6rBtZ3TnCJobAeG9lPctmVUVkX2h5QLhWdoJC/3iteNis2AQVam5yksOQ -S/O16ew2BHdkZds5Q/SDoYXbAoGAK9tVZ8LjWu30hXMU/9FLr0USoTS9JWOszAKH -byFuriPmq1lvD2PP2kK+yx2q3JD1fmQokIOR9Uvi6IJD1mTJwKyEcN3reppailKz -Z2q/X15hOsJcLR0DgpoHuKxwa1B1m8Ehu2etHxGJRtC9MTFiu5T3cIrenXskBhBP -NMSoNWcCgYAD3u3zdeVo3gVoxneS7GNVI2WBhjtqgNIbINuxGZvfztm7+vNPE6sQ -VL8i+09uoM1H6sXbe2XXORmtW0j/6MmYhSoBXNdqWTNAiyNRhwEQtowqgl5R7PBu -//QZTF1z62R9IKDMRG3f5Wn8e1Dys6tXBuG603g+Dkkc/km476mrgw== ------END RSA PRIVATE KEY-----"; - - // Generate with - // openssl ecparam -genkey -name prime256v1 | openssl pkcs8 -topk8 -nocrypt - const EC_PKCS8_PEM: &str = "-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+tHxet7G+uar2Cef -iYPb7jv3uzncFtwJ7RhDOvEA0fChRANCAATCKn2AEqa9785k+TmwkeCvLub8XGrF -ezE6bA/blaPVE3nu4SUVYKULRJQxNjeOSra8TQrlIS8e5ItbMn8Tv9KV ------END PRIVATE KEY-----"; - #[tokio::test] async fn test_static_store() { let message = "this is the message to sign".as_bytes(); let store = { let mut s = StaticKeystore::new(); - - let rsa = RsaPrivateKey::from_pkcs1_pem(RSA_PKCS1_PEM).unwrap(); - s.add_rsa_key(rsa).unwrap(); - - let ecdsa = SigningKey::from_pkcs8_pem(EC_PKCS8_PEM).unwrap(); - s.add_ecdsa_key(ecdsa).unwrap(); - + s.add_test_rsa_key().unwrap(); + s.add_test_ecdsa_key().unwrap(); s }; diff --git a/crates/warp-utils/src/filters/client.rs b/crates/warp-utils/src/filters/client.rs index ee5c30e3..b18a7160 100644 --- a/crates/warp-utils/src/filters/client.rs +++ b/crates/warp-utils/src/filters/client.rs @@ -284,7 +284,7 @@ struct ClientAuthForm { mod tests { use headers::authorization::Credentials; use mas_config::{ConfigurationSection, OAuth2ClientAuthMethodConfig}; - use mas_jose::{JsonWebSignatureAlgorithm, SigningKeystore}; + use mas_jose::{ExportJwks, JsonWebSignatureAlgorithm, SigningKeystore, StaticKeystore}; use serde_json::json; use super::*; @@ -292,7 +292,14 @@ mod tests { // Long client_secret to support it as a HS512 key const CLIENT_SECRET: &str = "leek2zaeyeb8thai7piehea3vah6ool9oanin9aeraThuci9EeghaekaiD1upe4Quoh7xeMae2meitohj0Waaveiwaorah1yazohr6Vae7iebeiRaWene5IeWeeciezu"; - fn oauth2_config() -> OAuth2Config { + fn client_private_keystore() -> StaticKeystore { + let mut store = StaticKeystore::new(); + store.add_test_rsa_key().unwrap(); + store.add_test_ecdsa_key().unwrap(); + store + } + + async fn oauth2_config() -> OAuth2Config { let mut config = OAuth2Config::test(); config.clients.push(OAuth2ClientConfig { client_id: "public".to_string(), @@ -327,6 +334,19 @@ mod tests { }, redirect_uris: Vec::new(), }); + + let store = client_private_keystore(); + let jwks = store.export_jwks().await.unwrap(); + config.clients.push(OAuth2ClientConfig { + client_id: "private-key-jwt".to_string(), + client_auth_method: OAuth2ClientAuthMethodConfig::PrivateKeyJwt(jwks.clone().into()), + redirect_uris: Vec::new(), + }); + config.clients.push(OAuth2ClientConfig { + client_id: "private-key-jwt-2".to_string(), + client_auth_method: OAuth2ClientAuthMethodConfig::PrivateKeyJwt(jwks.into()), + redirect_uris: Vec::new(), + }); config } @@ -353,7 +373,7 @@ mod tests { async fn client_secret_jwt(alg: JsonWebSignatureAlgorithm) { let audience = "https://example.com/token".to_string(); - let filter = client_authentication::
(&oauth2_config(), audience.clone()); + let filter = client_authentication::(&oauth2_config().await, audience.clone()); let store = SharedSecret::new(&CLIENT_SECRET); let claims = ClientAssertionClaims { @@ -422,10 +442,101 @@ mod tests { assert!(res.is_err()); } + #[tokio::test] + async fn client_secret_jwt_rs256() { + private_key_jwt(JsonWebSignatureAlgorithm::Rs256).await; + } + + #[tokio::test] + async fn client_secret_jwt_rs384() { + private_key_jwt(JsonWebSignatureAlgorithm::Rs384).await; + } + + #[tokio::test] + async fn client_secret_jwt_rs512() { + private_key_jwt(JsonWebSignatureAlgorithm::Rs512).await; + } + + #[tokio::test] + async fn client_secret_jwt_es256() { + private_key_jwt(JsonWebSignatureAlgorithm::Es256).await; + } + + async fn private_key_jwt(alg: JsonWebSignatureAlgorithm) { + let audience = "https://example.com/token".to_string(); + let filter = client_authentication::(&oauth2_config().await, audience.clone()); + + let store = client_private_keystore(); + let claims = ClientAssertionClaims { + issuer: "private-key-jwt".to_string(), + subject: "private-key-jwt".to_string(), + audience, + jwt_id: None, + }; + let header = store.prepare_header(alg).await.expect("JWT header"); + let jwt = DecodedJsonWebToken::new(header, claims); + let jwt = jwt.sign(&store).await.expect("signed token"); + let jwt = jwt.serialize(); + + // TODO: test failing cases + // - expired token + // - "not before" in the future + // - subject/issuer mismatch + // - audience mismatch + // - wrong secret/signature + + let (auth, client, body) = warp::test::request() + .method("POST") + .header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string()) + .body(serde_urlencoded::to_string(json!({ + "client_id": "private-key-jwt", + "client_assertion": jwt, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "foo": "baz", + "bar": "foobar", + })).unwrap()) + .filter(&filter) + .await + .unwrap(); + + assert_eq!(auth, ClientAuthenticationMethod::PrivateKeyJwt); + assert_eq!(client.client_id, "private-key-jwt"); + assert_eq!(body.foo, "baz"); + assert_eq!(body.bar, "foobar"); + + // Without client_id + let res = warp::test::request() + .method("POST") + .header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string()) + .body(serde_urlencoded::to_string(json!({ + "client_assertion": jwt, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "foo": "baz", + "bar": "foobar", + })).unwrap()) + .filter(&filter) + .await; + assert!(res.is_ok()); + + // client_id mismatch + let res = warp::test::request() + .method("POST") + .body(serde_urlencoded::to_string(json!({ + "client_id": "private-key-jwt-2", + "client_assertion": jwt, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "foo": "baz", + "bar": "foobar", + })).unwrap()) + .filter(&filter) + .await; + assert!(res.is_err()); + } + #[tokio::test] async fn client_secret_post() { let filter = client_authentication::( - &oauth2_config(), + &oauth2_config().await, "https://example.com/token".to_string(), ); @@ -457,7 +568,7 @@ mod tests { #[tokio::test] async fn client_secret_basic() { let filter = client_authentication::( - &oauth2_config(), + &oauth2_config().await, "https://example.com/token".to_string(), ); @@ -489,7 +600,7 @@ mod tests { #[tokio::test] async fn none() { let filter = client_authentication::( - &oauth2_config(), + &oauth2_config().await, "https://example.com/token".to_string(), );