You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Add tests for private_key_jwt client auth
This commit is contained in:
@ -61,6 +61,12 @@ impl JwksOrJwksUri {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonWebKeySet> 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 {
|
||||
|
@ -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<String, rsa::RsaPrivateKey>,
|
||||
es256_keys: HashMap<String, ecdsa::SigningKey<NistP256>>,
|
||||
es256_keys: HashMap<String, SigningKey<NistP256>>,
|
||||
}
|
||||
|
||||
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<NistP256>) -> anyhow::Result<()> {
|
||||
pub fn add_ecdsa_key(&mut self, key: SigningKey<NistP256>) -> 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
|
||||
};
|
||||
|
||||
|
@ -284,7 +284,7 @@ struct ClientAuthForm<T> {
|
||||
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::<Form>(&oauth2_config(), audience.clone());
|
||||
let filter = client_authentication::<Form>(&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::<Form>(&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::<Form>(
|
||||
&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::<Form>(
|
||||
&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::<Form>(
|
||||
&oauth2_config(),
|
||||
&oauth2_config().await,
|
||||
"https://example.com/token".to_string(),
|
||||
);
|
||||
|
||||
|
Reference in New Issue
Block a user