From 453ec9e621f80337503b472c419c4766f28b4bef Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Fri, 28 Jun 2024 10:47:47 +0200 Subject: [PATCH 1/2] Implement `schemars::JsonSchema` for non-secret new types. Placed behind a `schemars` feature flag. --- Cargo.toml | 8 ++++++++ src/core/mod.rs | 2 ++ src/jwt/mod.rs | 2 ++ src/macros.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/types/jwk.rs | 1 + src/types/localized.rs | 1 + src/types/mod.rs | 27 +++++++++++++++++++++++++++ 7 files changed, 79 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 2cb0a61..482bc46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ reqwest-blocking = ["oauth2/reqwest-blocking"] rustls-tls = ["oauth2/rustls-tls"] timing-resistant-secret-traits = ["oauth2/timing-resistant-secret-traits"] ureq = ["oauth2/ureq"] +schemars = ["dep:schemars", "oauth2/schemars"] [[example]] name = "gitlab" @@ -70,6 +71,9 @@ url = { version = "2.4", features = ["serde"] } subtle = "2.4" ed25519-dalek = { version = "2.0.0", features = ["pem"] } +# Feature: schemars +schemars = { version = "0.8", optional = true} + [dev-dependencies] color-backtrace = { version = "0.5" } env_logger = "0.9" @@ -77,3 +81,7 @@ pretty_assertions = "1.0" reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false } retry = "1.0" anyhow = "1.0" + +# TEMP: https://github.com/ramosbugs/oauth2-rs/pull/279 +[patch.crates-io] +oauth2 = { git = "https://github.com/gibbz00/oauth2-rs", branch = "schemars" } diff --git a/src/core/mod.rs b/src/core/mod.rs index 7a3368e..0c6f5d7 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -344,6 +344,7 @@ impl Display for CoreAuthPrompt { new_type![ /// OpenID Connect Core claim name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] CoreClaimName(String) ]; impl ClaimName for CoreClaimName {} @@ -447,6 +448,7 @@ impl ClientAuthMethod for CoreClientAuthMethod {} new_type![ /// OpenID Connect Core gender claim. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] CoreGenderClaim(String) ]; impl GenderClaim for CoreGenderClaim {} diff --git a/src/jwt/mod.rs b/src/jwt/mod.rs index b3adfe0..777b123 100644 --- a/src/jwt/mod.rs +++ b/src/jwt/mod.rs @@ -17,6 +17,7 @@ pub(crate) mod tests; new_type![ #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] JsonWebTokenContentType(String) ]; @@ -34,6 +35,7 @@ new_type![ /// /// To compare two different JSON Web Token types, please use the normalized version via [`JsonWebTokenType::normalize`]. #[derive(Deserialize, Hash, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] JsonWebTokenType(String) impl { diff --git a/src/macros.rs b/src/macros.rs index 0f4793a..24b2234 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -424,6 +424,22 @@ macro_rules! new_url_type { } } impl Eq for $name {} + + #[cfg(feature = "schemars")] + impl schemars::JsonSchema for $name { + fn schema_name() -> String { + stringify!($name).to_owned() + } + + fn schema_id() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(concat!("openidconnect::", stringify!($name))) + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + // HELP(gibbz00): do we want to generate the schema for a URL or a String? + gen.subschema_for::() + } + } }; } @@ -951,3 +967,25 @@ macro_rules! serialize_as_str { } }; } + +#[cfg(test)] +mod tests { + #[cfg(feature = "schemars")] + mod json_schema { + use schemars::schema_for; + use serde_json::json; + + #[test] + fn generates_new_url_type_json_schema() { + let expected_schema = json!({ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ClientUrl", + "type": "string" + }); + + let schema = schema_for!(crate::ClientUrl); + let actual_schema = serde_json::to_value(&schema).unwrap(); + assert_eq!(expected_schema, actual_schema); + } + } +} diff --git a/src/types/jwk.rs b/src/types/jwk.rs index 6f0e590..f21047f 100644 --- a/src/types/jwk.rs +++ b/src/types/jwk.rs @@ -9,6 +9,7 @@ use std::hash::Hash; new_type![ /// ID of a JSON Web Key. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] JsonWebKeyId(String) ]; diff --git a/src/types/localized.rs b/src/types/localized.rs index ae51daa..acd4639 100644 --- a/src/types/localized.rs +++ b/src/types/localized.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; new_type![ /// Language tag adhering to RFC 5646 (e.g., `fr` or `fr-CA`). #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] LanguageTag(String) ]; impl AsRef for LanguageTag { diff --git a/src/types/mod.rs b/src/types/mod.rs index 3133963..1d0059d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -77,6 +77,7 @@ new_type![ /// Set of authentication methods or procedures that are considered to be equivalent to each /// other in a particular context. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] AuthenticationContextClass(String) ]; impl AsRef for AuthenticationContextClass { @@ -90,12 +91,14 @@ new_type![ /// /// Defining specific AMR identifiers is beyond the scope of the OpenID Connect Core spec. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] AuthenticationMethodReference(String) ]; new_type![ /// Access token hash. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] AccessTokenHash(String) impl { /// Initialize a new access token hash from an [`AccessToken`] and signature algorithm. @@ -117,36 +120,42 @@ new_type![ new_type![ /// Country portion of address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] AddressCountry(String) ]; new_type![ /// Locality portion of address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] AddressLocality(String) ]; new_type![ /// Postal code portion of address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] AddressPostalCode(String) ]; new_type![ /// Region portion of address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] AddressRegion(String) ]; new_type![ /// Audience claim value. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] Audience(String) ]; new_type![ /// Authorization code hash. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] AuthorizationCodeHash(String) impl { /// Initialize a new authorization code hash from an [`AuthorizationCode`] and signature @@ -169,6 +178,7 @@ new_type![ new_type![ /// OpenID Connect client name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] ClientName(String) ]; @@ -185,6 +195,7 @@ new_url_type![ new_type![ /// Client contact e-mail address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] ClientContactEmail(String) ]; @@ -203,60 +214,70 @@ new_type![ /// providing just year can result in varying month and day, so the implementers need to take /// this factor into account to correctly process the dates. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserBirthday(String) ]; new_type![ /// End user's e-mail address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserEmail(String) ]; new_type![ /// End user's family name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserFamilyName(String) ]; new_type![ /// End user's given name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserGivenName(String) ]; new_type![ /// End user's middle name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserMiddleName(String) ]; new_type![ /// End user's name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserName(String) ]; new_type![ /// End user's nickname. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserNickname(String) ]; new_type![ /// End user's phone number. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserPhoneNumber(String) ]; new_type![ /// URL of end user's profile picture. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserPictureUrl(String) ]; new_type![ /// URL of end user's profile page. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserProfileUrl(String) ]; @@ -264,18 +285,21 @@ new_type![ /// End user's time zone as a string from the /// [time zone database](https://www.iana.org/time-zones). #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserTimezone(String) ]; new_type![ /// URL of end user's website. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserWebsiteUrl(String) ]; new_type![ /// End user's username. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] EndUserUsername(String) ]; @@ -286,6 +310,7 @@ new_type![ /// either as a carriage return/line feed pair (`"\r\n"`) or as a single line feed character /// (`"\n"`). #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] FormattedAddress(String) ]; @@ -439,6 +464,7 @@ new_type![ /// separated by newlines. Newlines can be represented either as a carriage return/line feed /// pair (`\r\n`) or as a single line feed character (`\n`). #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] StreetAddress(String) ]; @@ -446,6 +472,7 @@ new_type![ /// Locally unique and never reassigned identifier within the Issuer for the End-User, which is /// intended to be consumed by the client application. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] SubjectIdentifier(String) ]; From 7c424113eead63146a33850356ea6814053dff90 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 5 Aug 2024 17:14:38 +0200 Subject: [PATCH 2/2] Implement `schemars::JsonSchema` for secret new types. --- src/macros.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/macros.rs b/src/macros.rs index 24b2234..4f21965 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -233,6 +233,7 @@ macro_rules! new_secret_type { #[$attr] )* #[cfg_attr(feature = "timing-resistant-secret-traits", derive(Eq))] + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct $name($type); impl $name { $($item)* @@ -987,5 +988,19 @@ mod tests { let actual_schema = serde_json::to_value(&schema).unwrap(); assert_eq!(expected_schema, actual_schema); } + + #[test] + fn generates_new_secret_type_json_schema() { + let expected_schema = json!({ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Nonce", + "description": "String value used to associate a client session with an ID Token, and to mitigate replay attacks.", + "type": "string" + }); + + let schema = schema_for!(crate::Nonce); + let actual_schema = serde_json::to_value(&schema).unwrap(); + assert_eq!(expected_schema, actual_schema); + } } }