diff --git a/Cargo-1.45.lock b/Cargo-1.45.lock index 86c09b14..5d405782 100644 --- a/Cargo-1.45.lock +++ b/Cargo-1.45.lock @@ -221,6 +221,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "diff" version = "0.1.13" @@ -520,6 +555,12 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -774,7 +815,7 @@ checksum = "ac8b1a9b2518dc799a2271eff1688707eb315f0d4697aa6b0871369ca4c4da55" [[package]] name = "openidconnect" -version = "2.4.1" +version = "2.5.0" dependencies = [ "anyhow", "base64", @@ -797,6 +838,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_plain", + "serde_with", "subtle", "thiserror", "url", @@ -1079,6 +1121,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + [[package]] name = "ryu" version = "1.0.10" @@ -1199,6 +1247,29 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b827f2113224f3f19a665136f006709194bdfdcb1fdc1e4b2b5cbac8e0cced54" +dependencies = [ + "rustversion", + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha2" version = "0.10.2" @@ -1233,6 +1304,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index c9abe205..64df9398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openidconnect" -version = "2.4.1" +version = "2.5.0" authors = ["David A. Ramos "] description = "OpenID Connect library" license = "MIT" @@ -44,6 +44,7 @@ serde_derive = "1.0" serde_json = "1.0" serde_path_to_error = "0.1" serde_plain = "1.0" +serde_with = "1.13" serde-value = "0.7" url = { version = "2.1", features = ["serde"] } num-bigint = "0.4.3" diff --git a/src/core/jwk.rs b/src/core/jwk.rs index d6005ab3..478fc7b5 100644 --- a/src/core/jwk.rs +++ b/src/core/jwk.rs @@ -550,6 +550,7 @@ serialize_as_str!(CoreJsonWebKeyUse); #[cfg(test)] mod tests { + use crate::core::CoreJsonWebKeySet; use ring::test::rand::FixedByteRandom; use crate::jwt::tests::{TEST_EC_PUB_KEY_P256, TEST_EC_PUB_KEY_P384, TEST_RSA_PUB_KEY}; @@ -1287,4 +1288,59 @@ mod tests { assert_ne!(sig1, sig2); } + + // Tests that JsonWebKeySet ignores unsupported keys during deserialization so that clients can + // use providers that include unsupported keys as long as they only use supported ones to sign + // payloads. + #[test] + fn test_jwks_unsupported_key() { + let jwks_json = "{ + \"keys\": [ + { + \"kty\": \"RSA\", + \"use\": \"sig\", + \"kid\": \"2011-04-29\", + \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ + R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ + f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ + n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ + jF44-csFCur-kEgU8awapJzKnqDKgw\", + \"e\": \"AQAB\" + }, + { + \"kty\": \"MAGIC\", + \"use\": \"sig\", + \"kid\": \"2040-01-01\", + \"magic\": \"magic\" + }, + { + \"kty\": \"EC\", + \"use\": \"sig\", + \"kid\": \"2011-05-01\", + \"crv\": \"P-256\", + \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", + \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" + } + ] + }"; + let jwks = serde_json::from_str::(jwks_json) + .expect("deserialization should succeed"); + + assert_eq!(jwks.keys().len(), 2); + + assert_eq!(jwks.keys()[0].kty, CoreJsonWebKeyType::RSA); + assert_eq!(jwks.keys()[0].use_, Some(CoreJsonWebKeyUse::Signature)); + assert_eq!( + jwks.keys()[0].kid, + Some(JsonWebKeyId::new("2011-04-29".to_string())) + ); + + assert_eq!(jwks.keys()[1].kty, CoreJsonWebKeyType::EllipticCurve); + assert_eq!(jwks.keys()[1].use_, Some(CoreJsonWebKeyUse::Signature)); + assert_eq!( + jwks.keys()[1].kid, + Some(JsonWebKeyId::new("2011-05-01".to_string())) + ); + assert_eq!(jwks.keys()[1].crv, Some(CoreJsonCurveType::P256)); + } } diff --git a/src/discovery.rs b/src/discovery.rs index 511e83aa..99596f20 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -8,6 +8,7 @@ use http::status::StatusCode; use oauth2::{AuthUrl, Scope, TokenUrl}; use serde::de::DeserializeOwned; use serde::Serialize; +use serde_with::{serde_as, skip_serializing_none, VecSkipError}; use thiserror::Error; use super::http_utils::{check_content_type, MIME_TYPE_JSON}; @@ -38,6 +39,8 @@ impl AdditionalProviderMetadata for EmptyAdditionalProviderMetadata {} /// Provider metadata returned by [OpenID Connect Discovery]( /// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). /// +#[serde_as] +#[skip_serializing_none] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[allow(clippy::type_complexity)] pub struct ProviderMetadata @@ -60,117 +63,95 @@ where { issuer: IssuerUrl, authorization_endpoint: AuthUrl, - #[serde(skip_serializing_if = "Option::is_none")] token_endpoint: Option, - #[serde(skip_serializing_if = "Option::is_none")] userinfo_endpoint: Option, jwks_uri: JsonWebKeySetUrl, #[serde(default = "JsonWebKeySet::default", skip)] jwks: JsonWebKeySet, - #[serde(skip_serializing_if = "Option::is_none")] registration_endpoint: Option, - #[serde(skip_serializing_if = "Option::is_none")] scopes_supported: Option>, #[serde(bound(deserialize = "RT: ResponseType"))] response_types_supported: Vec>, - #[serde( - bound(deserialize = "RM: ResponseMode"), - skip_serializing_if = "Option::is_none" - )] + #[serde(bound(deserialize = "RM: ResponseMode"))] response_modes_supported: Option>, - #[serde( - bound(deserialize = "G: GrantType"), - skip_serializing_if = "Option::is_none" - )] + #[serde(bound(deserialize = "G: GrantType"))] grant_types_supported: Option>, - #[serde(skip_serializing_if = "Option::is_none")] acr_values_supported: Option>, #[serde(bound(deserialize = "S: SubjectIdentifierType"))] subject_types_supported: Vec, #[serde(bound(deserialize = "JS: JwsSigningAlgorithm"))] + #[serde_as(as = "VecSkipError<_>")] id_token_signing_alg_values_supported: Vec, #[serde( bound(deserialize = "JK: JweKeyManagementAlgorithm"), - skip_serializing_if = "Option::is_none" + default = "Option::default" )] + #[serde_as(as = "Option>")] id_token_encryption_alg_values_supported: Option>, #[serde( bound(deserialize = "JE: JweContentEncryptionAlgorithm"), - skip_serializing_if = "Option::is_none" + default = "Option::default" )] + #[serde_as(as = "Option>")] id_token_encryption_enc_values_supported: Option>, #[serde( bound(deserialize = "JS: JwsSigningAlgorithm"), - skip_serializing_if = "Option::is_none" + default = "Option::default" )] + #[serde_as(as = "Option>")] userinfo_signing_alg_values_supported: Option>, #[serde( bound(deserialize = "JK: JweKeyManagementAlgorithm"), - skip_serializing_if = "Option::is_none" + default = "Option::default" )] + #[serde_as(as = "Option>")] userinfo_encryption_alg_values_supported: Option>, #[serde( bound(deserialize = "JE: JweContentEncryptionAlgorithm"), - skip_serializing_if = "Option::is_none" + default = "Option::default" )] + #[serde_as(as = "Option>")] userinfo_encryption_enc_values_supported: Option>, #[serde( bound(deserialize = "JS: JwsSigningAlgorithm"), - skip_serializing_if = "Option::is_none" + default = "Option::default" )] + #[serde_as(as = "Option>")] request_object_signing_alg_values_supported: Option>, #[serde( bound(deserialize = "JK: JweKeyManagementAlgorithm"), - skip_serializing_if = "Option::is_none" + default = "Option::default" )] + #[serde_as(as = "Option>")] request_object_encryption_alg_values_supported: Option>, #[serde( bound(deserialize = "JE: JweContentEncryptionAlgorithm"), - skip_serializing_if = "Option::is_none" + default = "Option::default" )] + #[serde_as(as = "Option>")] request_object_encryption_enc_values_supported: Option>, - #[serde( - bound(deserialize = "CA: ClientAuthMethod"), - skip_serializing_if = "Option::is_none" - )] + #[serde(bound(deserialize = "CA: ClientAuthMethod"))] token_endpoint_auth_methods_supported: Option>, #[serde( bound(deserialize = "JS: JwsSigningAlgorithm"), - skip_serializing_if = "Option::is_none" + default = "Option::default" )] + #[serde_as(as = "Option>")] token_endpoint_auth_signing_alg_values_supported: Option>, - #[serde( - bound(deserialize = "AD: AuthDisplay"), - skip_serializing_if = "Option::is_none" - )] + #[serde(bound(deserialize = "AD: AuthDisplay"))] display_values_supported: Option>, - #[serde( - bound(deserialize = "CT: ClaimType"), - skip_serializing_if = "Option::is_none" - )] + #[serde(bound(deserialize = "CT: ClaimType"))] claim_types_supported: Option>, - #[serde( - bound(deserialize = "CN: ClaimName"), - skip_serializing_if = "Option::is_none" - )] + #[serde(bound(deserialize = "CN: ClaimName"))] claims_supported: Option>, - #[serde(skip_serializing_if = "Option::is_none")] service_documentation: Option, - #[serde(skip_serializing_if = "Option::is_none")] claims_locales_supported: Option>, - #[serde(skip_serializing_if = "Option::is_none")] ui_locales_supported: Option>, - #[serde(skip_serializing_if = "Option::is_none")] claims_parameter_supported: Option, - #[serde(skip_serializing_if = "Option::is_none")] request_parameter_supported: Option, - #[serde(skip_serializing_if = "Option::is_none")] request_uri_parameter_supported: Option, - #[serde(skip_serializing_if = "Option::is_none")] require_request_uri_registration: Option, - #[serde(skip_serializing_if = "Option::is_none")] op_policy_uri: Option, - #[serde(skip_serializing_if = "Option::is_none")] op_tos_uri: Option, #[serde(bound(deserialize = "A: AdditionalProviderMetadata"), flatten)] @@ -1445,4 +1426,193 @@ mod tests { serde_json::from_str(&serialized_json).unwrap(); assert_eq!(provider_metadata, redeserialized_metadata); } + + // Tests that we ignore enum values that the OIDC provider supports but that the client does + // not (which trigger serde deserialization errors while parsing the provider metadata). + #[test] + fn test_unsupported_enum_values() { + let json_response = "{\ + \"issuer\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\",\ + \"authorization_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\",\ + \"jwks_uri\":\"https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json\",\ + \"response_types_supported\":[\ + \"code\"\ + ],\ + \"subject_types_supported\":[\ + \"public\",\ + \"pairwise\"\ + ],\ + \"id_token_signing_alg_values_supported\":[\ + \"RS256\",\ + \"MAGIC\",\ + \"none\"\ + ],\ + \"id_token_encryption_alg_values_supported\":[\ + \"RSA1_5\",\ + \"MAGIC\"\ + ],\ + \"id_token_encryption_enc_values_supported\":[\ + \"A128CBC-HS256\",\ + \"MAGIC\"\ + ],\ + \"userinfo_signing_alg_values_supported\":[\ + \"RS256\",\ + \"MAGIC\",\ + \"none\"\ + ],\ + \"userinfo_encryption_alg_values_supported\":[\ + \"RSA1_5\",\ + \"MAGIC\"\ + ],\ + \"userinfo_encryption_enc_values_supported\":[\ + \"A128CBC-HS256\",\ + \"MAGIC\"\ + ],\ + \"request_object_signing_alg_values_supported\":[\ + \"RS256\",\ + \"MAGIC\",\ + \"none\"\ + ],\ + \"request_object_encryption_alg_values_supported\":[\ + \"RSA1_5\",\ + \"MAGIC\"\ + ],\ + \"request_object_encryption_enc_values_supported\":[\ + \"A128CBC-HS256\",\ + \"MAGIC\"\ + ],\ + \"token_endpoint_auth_signing_alg_values_supported\":[\ + \"RS256\",\ + \"MAGIC\",\ + \"none\"\ + ]\ + }"; + + let provider_metadata: CoreProviderMetadata = serde_json::from_str(json_response).unwrap(); + + assert_eq!( + IssuerUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" + .to_string() + ) + .unwrap(), + *provider_metadata.issuer() + ); + assert_eq!( + AuthUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ + /authorization" + .to_string() + ) + .unwrap(), + *provider_metadata.authorization_endpoint() + ); + assert_eq!(None, provider_metadata.token_endpoint()); + assert_eq!(None, provider_metadata.userinfo_endpoint()); + assert_eq!( + JsonWebKeySetUrl::new( + "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" + .to_string() + ) + .unwrap(), + *provider_metadata.jwks_uri() + ); + assert_eq!(None, provider_metadata.registration_endpoint()); + assert_eq!(None, provider_metadata.scopes_supported()); + assert_eq!( + vec![ResponseTypes::new(vec![CoreResponseType::Code])], + *provider_metadata.response_types_supported() + ); + assert_eq!(None, provider_metadata.response_modes_supported()); + assert_eq!(None, provider_metadata.grant_types_supported()); + assert_eq!(None, provider_metadata.acr_values_supported()); + assert_eq!( + vec![ + CoreSubjectIdentifierType::Public, + CoreSubjectIdentifierType::Pairwise, + ], + *provider_metadata.subject_types_supported() + ); + assert_eq!( + vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::None, + ], + *provider_metadata.id_token_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), + provider_metadata.id_token_encryption_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 + ]), + provider_metadata.id_token_encryption_enc_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::None, + ]), + provider_metadata.userinfo_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), + provider_metadata.userinfo_encryption_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 + ]), + provider_metadata.userinfo_encryption_enc_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::None, + ]), + provider_metadata.request_object_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), + provider_metadata.request_object_encryption_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 + ]), + provider_metadata.request_object_encryption_enc_values_supported() + ); + assert_eq!( + None, + provider_metadata.token_endpoint_auth_methods_supported() + ); + assert_eq!( + Some(&vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::None, + ]), + provider_metadata.token_endpoint_auth_signing_alg_values_supported() + ); + assert_eq!(None, provider_metadata.display_values_supported()); + assert_eq!(None, provider_metadata.claim_types_supported()); + assert_eq!(None, provider_metadata.claims_supported()); + + assert_eq!(None, provider_metadata.service_documentation()); + assert_eq!(None, provider_metadata.claims_locales_supported()); + assert_eq!(None, provider_metadata.ui_locales_supported()); + assert_eq!(None, provider_metadata.claims_parameter_supported()); + assert_eq!(None, provider_metadata.request_parameter_supported()); + assert_eq!(None, provider_metadata.request_uri_parameter_supported()); + assert_eq!(None, provider_metadata.require_request_uri_registration()); + assert_eq!(None, provider_metadata.op_policy_uri()); + assert_eq!(None, provider_metadata.op_tos_uri()); + + let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); + + let redeserialized_metadata: CoreProviderMetadata = + serde_json::from_str(&serialized_json).unwrap(); + assert_eq!(provider_metadata, redeserialized_metadata); + } } diff --git a/src/types.rs b/src/types.rs index 9c98f8ca..71abb9c3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -12,7 +12,8 @@ use http::status::StatusCode; use oauth2::helpers::deserialize_space_delimited_vec; use rand::{thread_rng, Rng}; use serde::de::DeserializeOwned; -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, VecSkipError}; use thiserror::Error; use url::Url; @@ -709,6 +710,7 @@ new_type![ /// /// JSON Web Key Set. /// +#[serde_as] #[derive(Debug, Deserialize, PartialEq, Serialize)] pub struct JsonWebKeySet where @@ -719,11 +721,10 @@ where { // FIXME: write a test that ensures duplicate object member names cause an error // (see https://tools.ietf.org/html/rfc7517#section-5) - // FIXME: add a deserializer that optionally ignores invalid keys rather than failing. That way, - // clients can function using the keys that they do understand, which is fine if they only ever - // get JWTs signed with those keys. See what other places we might want to be more tolerant of - // deserialization errors. #[serde(bound = "K: JsonWebKey")] + // Ignores invalid keys rather than failing. That way, clients can function using the keys that + // they do understand, which is fine if they only ever get JWTs signed with those keys. + #[serde_as(as = "VecSkipError<_>")] keys: Vec, #[serde(skip)] _phantom: PhantomData<(JS, JT, JU)>,