From 586c6da13143682582b0bd740595d0c75d8a0ae5 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Fri, 14 Apr 2023 15:18:21 +0200 Subject: [PATCH 01/30] Add support for Request by reference Add tests for RequestUrl Add missing request parameters Add sphereon demo website test Update documentation with new RequestUrl Remove sphereon demo example Add validate_request method to Provider struct Add preoper Ser and De for SiopRequest and RequestBuilder Add skeptic for Markdown code testing Add support for Request by reference fix: fix rebase conflicts Add comments and fix some tests fix: Move `derivative` to dev-dependencies Refactor Provider and Subject improve tests and example using wiremock Improve struct field serde fix: remove claims from lib.rs style: fix arguments order Add did:key DID method Add support for Request by reference fix: Remove lifetime annotations Add preoper Ser and De for SiopRequest and RequestBuilder Add Scope and Claim fix: fix rebase conflicts --- README.md | 1 + src/claim.rs | 29 +++++++++++ src/provider.rs | 19 ++++++++ src/request.rs | 127 ++++++++++++++++++++++++++++++++++++++++++++++++ src/response.rs | 4 ++ 5 files changed, 180 insertions(+) create mode 100644 src/claim.rs diff --git a/README.md b/README.md index fa81771d..d45bfb0f 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ use wiremock::{ matchers::{method, path}, Mock, MockServer, ResponseTemplate, }; +use std::str::FromStr; lazy_static! { pub static ref MOCK_KEYPAIR: Keypair = Keypair::generate(&mut OsRng); diff --git a/src/claim.rs b/src/claim.rs new file mode 100644 index 00000000..25551001 --- /dev/null +++ b/src/claim.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Debug, PartialEq, Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Claim { + // Profile Scope + Name, + FamilyName, + GivenName, + MiddleName, + Nickname, + PreferredUsername, + Profile, + Picture, + Website, + Gender, + Birthdate, + Zoneinfo, + Locale, + UpdatedAt, + // Email Scope + Email, + EmailVerified, + // Address Scope + Address, + // Phone Scope + PhoneNumber, + PhoneNumberVerified, +} diff --git a/src/provider.rs b/src/provider.rs index b9b37977..a7de31d9 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -81,6 +81,25 @@ where Ok(SiopResponse::new(request.redirect_uri().clone(), jwt)) } + // TODO: needs refactoring. + /// Generates a [`SiopResponse`] in response to a [`SiopRequest`]. The [`SiopResponse`] contains an [`IdToken`], + /// which is signed by the [`Subject`] of the [`Provider`]. + pub async fn generate_response(&self, request: SiopRequest) -> Result { + let subject_did = self.subject.did()?; + let id_token = IdToken::new( + subject_did.to_string(), + subject_did.to_string(), + request.client_id().clone(), + request.nonce().clone(), + (Utc::now() + Duration::minutes(10)).timestamp(), + ) + .state(request.state().clone()); + + let jwt = self.subject.encode(id_token).await?; + + Ok(SiopResponse::new(jwt, request.redirect_uri().clone())) + } + pub async fn send_response(&self, response: SiopResponse) -> Result<()> { let client = reqwest::Client::new(); let builder = client.post(response.redirect_uri()).form(&response); diff --git a/src/request.rs b/src/request.rs index f8591567..21378f6f 100644 --- a/src/request.rs +++ b/src/request.rs @@ -270,3 +270,130 @@ mod tests { assert!(request_url.is_err(),); } } + +#[derive(Deserialize, Getters, Debug)] +pub struct Registration { + #[getset(get = "pub")] + subject_syntax_types_supported: Option>, + id_token_signing_alg_values_supported: Option>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_registration() { + let request_url = RequestUrl::from_str( + "\ + siopv2://idtoken?\ + scope=openid\ + &response_type=id_token\ + &client_id=did%3Aexample%3AEiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA\ + &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb\ + &response_mode=post\ + ®istration=%7B%22subject_syntax_types_supported%22%3A\ + %5B%22did%3Amock%22%5D%2C%0A%20%20%20%20\ + %22id_token_signing_alg_values_supported%22%3A%5B%22EdDSA%22%5D%7D\ + &nonce=n-0S6_WzA2Mj\ + ", + ) + .unwrap(); + + assert_eq!( + RequestUrl::from_str(&RequestUrl::to_string(&request_url)).unwrap(), + request_url + ); + } + + #[test] + fn test_valid_request_uri() { + // A form urlencoded string with a `request_uri` parameter should deserialize into the `RequestUrl::RequestUri` variant. + let request_url = RequestUrl::from_str("siopv2://idtoken?request_uri=https://example.com/request_uri").unwrap(); + assert_eq!( + request_url, + RequestUrl::RequestUri { + request_uri: "https://example.com/request_uri".to_owned() + } + ); + } + + #[test] + fn test_valid_request() { + // A form urlencoded string without a `request_uri` parameter should deserialize into the `RequestUrl::Request` variant. + let request_url = RequestUrl::from_str( + "\ + siopv2://idtoken?\ + scope=openid\ + &response_type=id_token\ + &client_id=did%3Aexample%3AEiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA\ + &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb\ + &response_mode=post\ + ®istration=%7B%22subject_syntax_types_supported%22%3A\ + %5B%22did%3Amock%22%5D%2C%0A%20%20%20%20\ + %22id_token_signing_alg_values_supported%22%3A%5B%22EdDSA%22%5D%7D\ + &nonce=n-0S6_WzA2Mj\ + ", + ) + .unwrap(); + assert_eq!( + request_url.clone(), + RequestUrl::Request(Box::new(SiopRequest { + response_type: ResponseType::IdToken, + response_mode: Some("post".to_owned()), + client_id: "did:example:\ + EiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA" + .to_owned(), + scope: "openid".to_owned(), + claims: None, + redirect_uri: "https://client.example.org/cb".to_owned(), + nonce: "n-0S6_WzA2Mj".to_owned(), + registration: Some(Registration { + subject_syntax_types_supported: Some(vec!["did:mock".to_owned()]), + id_token_signing_alg_values_supported: Some(vec!["EdDSA".to_owned()]), + }), + iss: None, + iat: None, + exp: None, + nbf: None, + jti: None, + state: None, + })) + ); + + assert_eq!( + request_url, + RequestUrl::from_str(&RequestUrl::to_string(&request_url)).unwrap() + ); + } + + #[test] + fn test_invalid_request() { + // A form urlencoded string with an otherwise valid request is invalid when the `request_uri` parameter is also + // present. + let request_url = RequestUrl::from_str( + "\ + siopv2://idtoken?\ + scope=openid\ + &response_type=id_token\ + &client_id=did%3Aexample%3AEiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA\ + &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb\ + &response_mode=post\ + ®istration=%7B%22subject_syntax_types_supported%22%3A\ + %5B%22did%3Amock%22%5D%2C%0A%20%20%20%20\ + %22id_token_signing_alg_values_supported%22%3A%5B%22EdDSA%22%5D%7D\ + &nonce=n-0S6_WzA2Mj\ + &request_uri=https://example.com/request_uri\ + ", + ); + assert!(request_url.is_err()) + } + + #[test] + fn test_invalid_request_uri() { + // A form urlencoded string with a `request_uri` should not have any other parameters. + let request_url = + RequestUrl::from_str("siopv2://idtoken?request_uri=https://example.com/request_uri&scope=openid"); + assert!(request_url.is_err(),); + } +} diff --git a/src/response.rs b/src/response.rs index a38c19a7..2fafb5a3 100644 --- a/src/response.rs +++ b/src/response.rs @@ -8,6 +8,10 @@ pub struct SiopResponse { #[getset(get = "pub")] redirect_uri: String, pub id_token: String, + #[serde(skip)] + #[getset(get = "pub")] + redirect_uri: String, + pub id_token: String, } impl SiopResponse { From 0bd23b7ce1264e97ff826458a5be05244ca0782a Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Mon, 17 Apr 2023 09:06:25 +0200 Subject: [PATCH 02/30] Improve struct field serde --- src/response.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/response.rs b/src/response.rs index 2fafb5a3..a38c19a7 100644 --- a/src/response.rs +++ b/src/response.rs @@ -8,10 +8,6 @@ pub struct SiopResponse { #[getset(get = "pub")] redirect_uri: String, pub id_token: String, - #[serde(skip)] - #[getset(get = "pub")] - redirect_uri: String, - pub id_token: String, } impl SiopResponse { From 6c67a90a895fc45be82d647ff82c5f3721992bb5 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 19 Apr 2023 10:10:29 +0200 Subject: [PATCH 03/30] fix: remove custom serde --- Cargo.toml | 1 + src/lib.rs | 1 + src/request.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index cadbf3ba..48c587dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ url = { version = "2.3.1", features = ["serde"] } is_empty = "0.2.0" serde_urlencoded = "0.7.1" derive_more = "0.99.16" +merge = "0.1.0" [dev-dependencies] ed25519-dalek = "1.0.1" diff --git a/src/lib.rs b/src/lib.rs index e7916759..26da3a70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod request; pub mod request_builder; pub mod response; pub mod scope; +pub mod storage; pub mod subject; pub mod validator; diff --git a/src/request.rs b/src/request.rs index 21378f6f..99d603e7 100644 --- a/src/request.rs +++ b/src/request.rs @@ -2,6 +2,7 @@ use crate::{claims::ClaimRequests, Registration, RequestUrlBuilder, Scope, Stand use anyhow::{anyhow, Result}; use derive_more::Display; use getset::Getters; +use merge::Merge; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::convert::TryInto; From eb5990d05881b244a30d9e9ffe69d90762660bc1 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 19 Apr 2023 10:13:07 +0200 Subject: [PATCH 04/30] Add claims and scope parameters --- src/claim.rs | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 src/claim.rs diff --git a/src/claim.rs b/src/claim.rs deleted file mode 100644 index 25551001..00000000 --- a/src/claim.rs +++ /dev/null @@ -1,29 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Debug, PartialEq, Serialize, Clone)] -#[serde(rename_all = "snake_case")] -pub enum Claim { - // Profile Scope - Name, - FamilyName, - GivenName, - MiddleName, - Nickname, - PreferredUsername, - Profile, - Picture, - Website, - Gender, - Birthdate, - Zoneinfo, - Locale, - UpdatedAt, - // Email Scope - Email, - EmailVerified, - // Address Scope - Address, - // Phone Scope - PhoneNumber, - PhoneNumberVerified, -} From c4e78b610a16f22f65d1490ac161d1bd41b4c587 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 19 Apr 2023 10:13:48 +0200 Subject: [PATCH 05/30] Add Storage and RelyingParty test improvement --- src/key_method.rs | 4 ++-- src/provider.rs | 21 ++++++++++++----- src/relying_party.rs | 2 +- src/storage.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 src/storage.rs diff --git a/src/key_method.rs b/src/key_method.rs index 6af076b9..0ea43afb 100644 --- a/src/key_method.rs +++ b/src/key_method.rs @@ -91,7 +91,7 @@ async fn resolve_public_key(kid: &str) -> Result> { #[cfg(test)] mod tests { use super::*; - use crate::{IdToken, Provider, RelyingParty}; + use crate::{IdToken, MemoryStorage, Provider, RelyingParty}; use chrono::{Duration, Utc}; #[tokio::test] @@ -100,7 +100,7 @@ mod tests { let subject = KeySubject::new(); // Create a new provider. - let provider = Provider::new(subject).await.unwrap(); + let provider = Provider::new(subject, MemoryStorage::default()).await.unwrap(); // Get a new SIOP request with response mode `post` for cross-device communication. let request_url = "\ diff --git a/src/provider.rs b/src/provider.rs index a7de31d9..1026d444 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -6,20 +6,23 @@ use chrono::{Duration, Utc}; /// [`SiopRequest`]'s from [crate::relying_party::RelyingParty]'s (RPs). The [`Provider`] acts as a trusted intermediary between the RPs and /// the user who is trying to authenticate. #[derive(Default)] -pub struct Provider +pub struct Provider where S: Subject + Validator, + T: Storage, { pub subject: S, + pub storage: T, } -impl Provider +impl Provider where S: Subject + Validator, + T: Storage, { // TODO: Use ProviderBuilder instead. - pub async fn new(subject: S) -> Result { - Ok(Provider { subject }) + pub async fn new(subject: S, storage: T) -> Result { + Ok(Provider { subject, storage }) } pub fn subject_syntax_types_supported(&self) -> Result> { @@ -76,6 +79,11 @@ where id_token }; + // Fetch the user's claims from the storage. + if let Some(id_token_request_claims) = request.id_token_request_claims() { + id_token.standard_claims = self.storage.fetch_claims(&id_token_request_claims); + } + let jwt = self.subject.encode(id_token).await?; Ok(SiopResponse::new(request.redirect_uri().clone(), jwt)) @@ -111,6 +119,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::storage::MemoryStorage; use crate::test_utils::MockSubject; #[tokio::test] @@ -119,7 +128,7 @@ mod tests { let subject = MockSubject::new("did:mock:123".to_string(), "key_identifier".to_string()).unwrap(); // Create a new provider. - let provider = Provider::new(subject).await.unwrap(); + let provider = Provider::new(subject, MemoryStorage::default()).await.unwrap(); // Get a new SIOP request with response mode `post` for cross-device communication. let request_url = "\ @@ -145,7 +154,7 @@ mod tests { #[tokio::test] async fn test_provider_subject_syntax_types_supported() { // Create a new provider. - let provider = Provider::::default(); + let provider = Provider::::default(); // Test whether the provider returns the correct subject syntax types. assert_eq!( diff --git a/src/relying_party.rs b/src/relying_party.rs index b64dd400..87e61608 100644 --- a/src/relying_party.rs +++ b/src/relying_party.rs @@ -136,7 +136,7 @@ mod tests { let storage = MemoryStorage::new(serde_json::from_value(USER_CLAIMS.clone()).unwrap()); // Create a new provider. - let provider = Provider::new(subject).await.unwrap(); + let provider = Provider::new(subject, storage).await.unwrap(); // Create a new RequestUrl which includes a `request_uri` pointing to the mock server's `request_uri` endpoint. let request_url = RequestUrl::builder() diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 00000000..ca33bf79 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,55 @@ +use crate::{claims::Claim, StandardClaims}; + +pub trait Storage { + fn fetch_claims(&self, request_claims: &StandardClaims) -> StandardClaims; +} + +#[derive(Default, Debug)] +pub struct MemoryStorage { + data: StandardClaims, +} + +impl MemoryStorage { + pub fn new(data: StandardClaims) -> Self { + MemoryStorage { data } + } +} + +impl Storage for MemoryStorage { + fn fetch_claims(&self, request_claims: &StandardClaims) -> StandardClaims { + let mut present = StandardClaims::default(); + + macro_rules! present_if { + ($claim:ident) => { + if let Some(claim) = &request_claims.$claim { + match claim { + Claim::Request(_) | Claim::Default => present.$claim = self.data.$claim.clone(), + _ => {} + } + } + }; + } + + present_if!(name); + present_if!(family_name); + present_if!(given_name); + present_if!(middle_name); + present_if!(nickname); + present_if!(preferred_username); + present_if!(profile); + present_if!(picture); + present_if!(website); + present_if!(gender); + present_if!(birthdate); + present_if!(zoneinfo); + present_if!(locale); + present_if!(updated_at); + present_if!(email); + present_if!(email_verified); + present_if!(address); + present_if!(phone_number); + present_if!(phone_number_verified); + + present + } +} From cbf35ac866f3e091a2eb15c7346c1c2d0ce85345 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 19 Apr 2023 10:51:08 +0200 Subject: [PATCH 06/30] Update README example --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d45bfb0f..b1fc25d6 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ use chrono::{Duration, Utc}; use ed25519_dalek::{Keypair, Signature, Signer}; use lazy_static::lazy_static; use rand::rngs::OsRng; +use serde_json::{json, Value}; use siopv2::{ claims::{Claim, ClaimRequests}, request::ResponseType, StandardClaim, @@ -145,7 +146,7 @@ async fn main() { let subject = MySubject::default(); // Create a new provider. - let provider = Provider::new(subject).await.unwrap(); + let provider = Provider::new(subject, MemoryStorage::default()).await.unwrap(); // Create a new RequestUrl which includes a `request_uri` pointing to the mock server's `request_uri` endpoint. let request_url = RequestUrl::builder() From db8e0b601a607591ecfc103acf5e99768d57c7e4 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 19 Apr 2023 14:43:13 +0200 Subject: [PATCH 07/30] fix: Add standard_claims to test IdToken --- src/subject.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subject.rs b/src/subject.rs index 98dc5ffc..c3722440 100644 --- a/src/subject.rs +++ b/src/subject.rs @@ -37,7 +37,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{test_utils::MockSubject, IdToken, Validator}; + use crate::{test_utils::MockSubject, IdToken, StandardClaims, Validator}; use serde_json::json; #[tokio::test] From ba19bf978e08919fe0c1407f9ddf10669609d8e6 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 19 Apr 2023 17:45:33 +0200 Subject: [PATCH 08/30] Move Storage trait to test_utils --- Cargo.toml | 3 +-- README.md | 3 +-- src/key_method.rs | 4 ++-- src/lib.rs | 1 - src/provider.rs | 22 ++++++++-------------- src/relying_party.rs | 2 +- 6 files changed, 13 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 48c587dc..311de555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,7 @@ is_empty = "0.2.0" serde_urlencoded = "0.7.1" derive_more = "0.99.16" merge = "0.1.0" - -[dev-dependencies] +# [dev-dependencies] ed25519-dalek = "1.0.1" rand = "0.7" lazy_static = "1.4.0" diff --git a/README.md b/README.md index b1fc25d6..d45bfb0f 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ use chrono::{Duration, Utc}; use ed25519_dalek::{Keypair, Signature, Signer}; use lazy_static::lazy_static; use rand::rngs::OsRng; -use serde_json::{json, Value}; use siopv2::{ claims::{Claim, ClaimRequests}, request::ResponseType, StandardClaim, @@ -146,7 +145,7 @@ async fn main() { let subject = MySubject::default(); // Create a new provider. - let provider = Provider::new(subject, MemoryStorage::default()).await.unwrap(); + let provider = Provider::new(subject).await.unwrap(); // Create a new RequestUrl which includes a `request_uri` pointing to the mock server's `request_uri` endpoint. let request_url = RequestUrl::builder() diff --git a/src/key_method.rs b/src/key_method.rs index 0ea43afb..cb67fcf2 100644 --- a/src/key_method.rs +++ b/src/key_method.rs @@ -91,7 +91,7 @@ async fn resolve_public_key(kid: &str) -> Result> { #[cfg(test)] mod tests { use super::*; - use crate::{IdToken, MemoryStorage, Provider, RelyingParty}; + use crate::{IdToken, Provider, RelyingParty, StandardClaims}; use chrono::{Duration, Utc}; #[tokio::test] @@ -100,7 +100,7 @@ mod tests { let subject = KeySubject::new(); // Create a new provider. - let provider = Provider::new(subject, MemoryStorage::default()).await.unwrap(); + let provider = Provider::new(subject).await.unwrap(); // Get a new SIOP request with response mode `post` for cross-device communication. let request_url = "\ diff --git a/src/lib.rs b/src/lib.rs index 26da3a70..e7916759 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ pub mod request; pub mod request_builder; pub mod response; pub mod scope; -pub mod storage; pub mod subject; pub mod validator; diff --git a/src/provider.rs b/src/provider.rs index 1026d444..baf97695 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -6,23 +6,20 @@ use chrono::{Duration, Utc}; /// [`SiopRequest`]'s from [crate::relying_party::RelyingParty]'s (RPs). The [`Provider`] acts as a trusted intermediary between the RPs and /// the user who is trying to authenticate. #[derive(Default)] -pub struct Provider +pub struct Provider where S: Subject + Validator, - T: Storage, { pub subject: S, - pub storage: T, } -impl Provider +impl Provider where S: Subject + Validator, - T: Storage, { // TODO: Use ProviderBuilder instead. - pub async fn new(subject: S, storage: T) -> Result { - Ok(Provider { subject, storage }) + pub async fn new(subject: S) -> Result { + Ok(Provider { subject }) } pub fn subject_syntax_types_supported(&self) -> Result> { @@ -79,10 +76,8 @@ where id_token }; - // Fetch the user's claims from the storage. - if let Some(id_token_request_claims) = request.id_token_request_claims() { - id_token.standard_claims = self.storage.fetch_claims(&id_token_request_claims); - } + // Include the user claims in the id token. + id_token.standard_claims = user_claims; let jwt = self.subject.encode(id_token).await?; @@ -119,7 +114,6 @@ where #[cfg(test)] mod tests { use super::*; - use crate::storage::MemoryStorage; use crate::test_utils::MockSubject; #[tokio::test] @@ -128,7 +122,7 @@ mod tests { let subject = MockSubject::new("did:mock:123".to_string(), "key_identifier".to_string()).unwrap(); // Create a new provider. - let provider = Provider::new(subject, MemoryStorage::default()).await.unwrap(); + let provider = Provider::new(subject).await.unwrap(); // Get a new SIOP request with response mode `post` for cross-device communication. let request_url = "\ @@ -154,7 +148,7 @@ mod tests { #[tokio::test] async fn test_provider_subject_syntax_types_supported() { // Create a new provider. - let provider = Provider::::default(); + let provider = Provider::::default(); // Test whether the provider returns the correct subject syntax types. assert_eq!( diff --git a/src/relying_party.rs b/src/relying_party.rs index 87e61608..b64dd400 100644 --- a/src/relying_party.rs +++ b/src/relying_party.rs @@ -136,7 +136,7 @@ mod tests { let storage = MemoryStorage::new(serde_json::from_value(USER_CLAIMS.clone()).unwrap()); // Create a new provider. - let provider = Provider::new(subject, storage).await.unwrap(); + let provider = Provider::new(subject).await.unwrap(); // Create a new RequestUrl which includes a `request_uri` pointing to the mock server's `request_uri` endpoint. let request_url = RequestUrl::builder() From 7c424d94343edd2ea3fba1caacb596b24f332906 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 19 Apr 2023 17:46:58 +0200 Subject: [PATCH 09/30] Remove storage.rs --- src/storage.rs | 55 -------------------------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 src/storage.rs diff --git a/src/storage.rs b/src/storage.rs deleted file mode 100644 index ca33bf79..00000000 --- a/src/storage.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::{claims::Claim, StandardClaims}; - -pub trait Storage { - fn fetch_claims(&self, request_claims: &StandardClaims) -> StandardClaims; -} - -#[derive(Default, Debug)] -pub struct MemoryStorage { - data: StandardClaims, -} - -impl MemoryStorage { - pub fn new(data: StandardClaims) -> Self { - MemoryStorage { data } - } -} - -impl Storage for MemoryStorage { - fn fetch_claims(&self, request_claims: &StandardClaims) -> StandardClaims { - let mut present = StandardClaims::default(); - - macro_rules! present_if { - ($claim:ident) => { - if let Some(claim) = &request_claims.$claim { - match claim { - Claim::Request(_) | Claim::Default => present.$claim = self.data.$claim.clone(), - _ => {} - } - } - }; - } - - present_if!(name); - present_if!(family_name); - present_if!(given_name); - present_if!(middle_name); - present_if!(nickname); - present_if!(preferred_username); - present_if!(profile); - present_if!(picture); - present_if!(website); - present_if!(gender); - present_if!(birthdate); - present_if!(zoneinfo); - present_if!(locale); - present_if!(updated_at); - present_if!(email); - present_if!(email_verified); - present_if!(address); - present_if!(phone_number); - present_if!(phone_number_verified); - - present - } -} From 4f28f97759ece0008003ca83536cce076523907d Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Mon, 24 Apr 2023 14:46:59 +0200 Subject: [PATCH 10/30] fix: fix dev-dependencies --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 311de555..48c587dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,8 @@ is_empty = "0.2.0" serde_urlencoded = "0.7.1" derive_more = "0.99.16" merge = "0.1.0" -# [dev-dependencies] + +[dev-dependencies] ed25519-dalek = "1.0.1" rand = "0.7" lazy_static = "1.4.0" From 47f728c3154661a493fc85b2d6ba4345829b3c81 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 25 Apr 2023 13:18:13 +0200 Subject: [PATCH 11/30] fix: fex rebase to dev --- src/provider.rs | 19 -------- src/request.rs | 127 ------------------------------------------------ 2 files changed, 146 deletions(-) diff --git a/src/provider.rs b/src/provider.rs index baf97695..d74a3982 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -84,25 +84,6 @@ where Ok(SiopResponse::new(request.redirect_uri().clone(), jwt)) } - // TODO: needs refactoring. - /// Generates a [`SiopResponse`] in response to a [`SiopRequest`]. The [`SiopResponse`] contains an [`IdToken`], - /// which is signed by the [`Subject`] of the [`Provider`]. - pub async fn generate_response(&self, request: SiopRequest) -> Result { - let subject_did = self.subject.did()?; - let id_token = IdToken::new( - subject_did.to_string(), - subject_did.to_string(), - request.client_id().clone(), - request.nonce().clone(), - (Utc::now() + Duration::minutes(10)).timestamp(), - ) - .state(request.state().clone()); - - let jwt = self.subject.encode(id_token).await?; - - Ok(SiopResponse::new(jwt, request.redirect_uri().clone())) - } - pub async fn send_response(&self, response: SiopResponse) -> Result<()> { let client = reqwest::Client::new(); let builder = client.post(response.redirect_uri()).form(&response); diff --git a/src/request.rs b/src/request.rs index 99d603e7..9d93f5e1 100644 --- a/src/request.rs +++ b/src/request.rs @@ -271,130 +271,3 @@ mod tests { assert!(request_url.is_err(),); } } - -#[derive(Deserialize, Getters, Debug)] -pub struct Registration { - #[getset(get = "pub")] - subject_syntax_types_supported: Option>, - id_token_signing_alg_values_supported: Option>, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_registration() { - let request_url = RequestUrl::from_str( - "\ - siopv2://idtoken?\ - scope=openid\ - &response_type=id_token\ - &client_id=did%3Aexample%3AEiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA\ - &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb\ - &response_mode=post\ - ®istration=%7B%22subject_syntax_types_supported%22%3A\ - %5B%22did%3Amock%22%5D%2C%0A%20%20%20%20\ - %22id_token_signing_alg_values_supported%22%3A%5B%22EdDSA%22%5D%7D\ - &nonce=n-0S6_WzA2Mj\ - ", - ) - .unwrap(); - - assert_eq!( - RequestUrl::from_str(&RequestUrl::to_string(&request_url)).unwrap(), - request_url - ); - } - - #[test] - fn test_valid_request_uri() { - // A form urlencoded string with a `request_uri` parameter should deserialize into the `RequestUrl::RequestUri` variant. - let request_url = RequestUrl::from_str("siopv2://idtoken?request_uri=https://example.com/request_uri").unwrap(); - assert_eq!( - request_url, - RequestUrl::RequestUri { - request_uri: "https://example.com/request_uri".to_owned() - } - ); - } - - #[test] - fn test_valid_request() { - // A form urlencoded string without a `request_uri` parameter should deserialize into the `RequestUrl::Request` variant. - let request_url = RequestUrl::from_str( - "\ - siopv2://idtoken?\ - scope=openid\ - &response_type=id_token\ - &client_id=did%3Aexample%3AEiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA\ - &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb\ - &response_mode=post\ - ®istration=%7B%22subject_syntax_types_supported%22%3A\ - %5B%22did%3Amock%22%5D%2C%0A%20%20%20%20\ - %22id_token_signing_alg_values_supported%22%3A%5B%22EdDSA%22%5D%7D\ - &nonce=n-0S6_WzA2Mj\ - ", - ) - .unwrap(); - assert_eq!( - request_url.clone(), - RequestUrl::Request(Box::new(SiopRequest { - response_type: ResponseType::IdToken, - response_mode: Some("post".to_owned()), - client_id: "did:example:\ - EiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA" - .to_owned(), - scope: "openid".to_owned(), - claims: None, - redirect_uri: "https://client.example.org/cb".to_owned(), - nonce: "n-0S6_WzA2Mj".to_owned(), - registration: Some(Registration { - subject_syntax_types_supported: Some(vec!["did:mock".to_owned()]), - id_token_signing_alg_values_supported: Some(vec!["EdDSA".to_owned()]), - }), - iss: None, - iat: None, - exp: None, - nbf: None, - jti: None, - state: None, - })) - ); - - assert_eq!( - request_url, - RequestUrl::from_str(&RequestUrl::to_string(&request_url)).unwrap() - ); - } - - #[test] - fn test_invalid_request() { - // A form urlencoded string with an otherwise valid request is invalid when the `request_uri` parameter is also - // present. - let request_url = RequestUrl::from_str( - "\ - siopv2://idtoken?\ - scope=openid\ - &response_type=id_token\ - &client_id=did%3Aexample%3AEiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA\ - &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb\ - &response_mode=post\ - ®istration=%7B%22subject_syntax_types_supported%22%3A\ - %5B%22did%3Amock%22%5D%2C%0A%20%20%20%20\ - %22id_token_signing_alg_values_supported%22%3A%5B%22EdDSA%22%5D%7D\ - &nonce=n-0S6_WzA2Mj\ - &request_uri=https://example.com/request_uri\ - ", - ); - assert!(request_url.is_err()) - } - - #[test] - fn test_invalid_request_uri() { - // A form urlencoded string with a `request_uri` should not have any other parameters. - let request_url = - RequestUrl::from_str("siopv2://idtoken?request_uri=https://example.com/request_uri&scope=openid"); - assert!(request_url.is_err(),); - } -} From 41a23392b1aec0aeb7680d747f8bfb7a2d94700c Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 25 Apr 2023 13:18:54 +0200 Subject: [PATCH 12/30] fix: fix rebase to dev --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index d45bfb0f..fa81771d 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ use wiremock::{ matchers::{method, path}, Mock, MockServer, ResponseTemplate, }; -use std::str::FromStr; lazy_static! { pub static ref MOCK_KEYPAIR: Keypair = Keypair::generate(&mut OsRng); From ce6a463ba7dff18717400b39b4d4707941310f3c Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Fri, 12 May 2023 11:06:21 +0200 Subject: [PATCH 13/30] feat: add Claim trait with associated types --- Cargo.toml | 1 - src/request.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 48c587dc..cadbf3ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,6 @@ url = { version = "2.3.1", features = ["serde"] } is_empty = "0.2.0" serde_urlencoded = "0.7.1" derive_more = "0.99.16" -merge = "0.1.0" [dev-dependencies] ed25519-dalek = "1.0.1" diff --git a/src/request.rs b/src/request.rs index 9d93f5e1..f8591567 100644 --- a/src/request.rs +++ b/src/request.rs @@ -2,7 +2,6 @@ use crate::{claims::ClaimRequests, Registration, RequestUrlBuilder, Scope, Stand use anyhow::{anyhow, Result}; use derive_more::Display; use getset::Getters; -use merge::Merge; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::convert::TryInto; From 5cabc484b6a0a237bf4feaea5372ffa4b14bafcf Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 23 May 2023 20:40:10 +0200 Subject: [PATCH 14/30] fix: build --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index cadbf3ba..e5b5ae1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ homepage = "https://www.impierce.com/" keywords = ["openid4vc", "siopv2", "openid4vp", "openid4vci", "OpenID Connect"] license = "Apache-2.0" repository = "https://github.com/impierce/openid4vc" +build = "build.rs" [dependencies] tokio = { version = "1.26.0", features = ["rt", "macros", "rt-multi-thread"] } From 40d0d06b852932dcf5799d882f400f632691921e Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 24 May 2023 18:13:34 +0200 Subject: [PATCH 15/30] fix: remove build.rs and change crate name in doc tests --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e5b5ae1a..cadbf3ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ homepage = "https://www.impierce.com/" keywords = ["openid4vc", "siopv2", "openid4vp", "openid4vci", "OpenID Connect"] license = "Apache-2.0" repository = "https://github.com/impierce/openid4vc" -build = "build.rs" [dependencies] tokio = { version = "1.26.0", features = ["rt", "macros", "rt-multi-thread"] } From bbcf6e7fe135cc818c109fe15cba89f22aefa61a Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 30 May 2023 09:25:33 +0200 Subject: [PATCH 16/30] feat: refactor claims.rs --- src/key_method.rs | 2 +- src/subject.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/key_method.rs b/src/key_method.rs index cb67fcf2..6af076b9 100644 --- a/src/key_method.rs +++ b/src/key_method.rs @@ -91,7 +91,7 @@ async fn resolve_public_key(kid: &str) -> Result> { #[cfg(test)] mod tests { use super::*; - use crate::{IdToken, Provider, RelyingParty, StandardClaims}; + use crate::{IdToken, Provider, RelyingParty}; use chrono::{Duration, Utc}; #[tokio::test] diff --git a/src/subject.rs b/src/subject.rs index c3722440..98dc5ffc 100644 --- a/src/subject.rs +++ b/src/subject.rs @@ -37,7 +37,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{test_utils::MockSubject, IdToken, StandardClaims, Validator}; + use crate::{test_utils::MockSubject, IdToken, Validator}; use serde_json::json; #[tokio::test] From 1db5af78dd14732d5c48a2c50a71abe15bad3ab4 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 24 May 2023 22:12:45 +0200 Subject: [PATCH 17/30] feat: Add builder for Response and IdToken --- README.md | 52 ++++++++++---------- src/id_token.rs | 104 +++++++++++++++++++++++++++++----------- src/key_method.rs | 16 +------ src/lib.rs | 18 ++++++- src/provider.rs | 43 +++++++++-------- src/relying_party.rs | 36 ++++++-------- src/response.rs | 110 ++++++++++++++++++++++++++++++++++++++++--- src/subject.rs | 20 ++++---- 8 files changed, 269 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index fa81771d..7ad83402 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ OpenID for Verifiable Credentials (OID4VC) consists of the following specificati Currently the Implicit Flow is consists of four major parts: -- A `Provider` that can accept a `SiopRequest` and generate a `SiopResponse` by creating an `IdToken`, adding its key identifier to the header of the `id_token`, signing the `id_token` and wrap it into a `SiopResponse`. It can also send the `SiopResponse` using the `redirect_uri` parameter. -- A `RelyingParty` struct which can validate a `SiopResponse` by validating its `IdToken` using a key identifier (which is extracted from the `id_token`) and its public key. -- The `Subject` trait can be implemented on a custom struct representing the signing logic of a DID method. A `Provider` can ingest an object that implements the `Subject` trait so that during generation of a `SiopResponse` the DID method syntax, key identifier and signing method of the specific `Subject` can be used. +- A `Provider` that can accept a `SiopRequest` and generate a `Response` by creating an `IdToken`, adding its key identifier to the header of the `id_token`, signing the `id_token` and wrap it into a `Response`. It can also send the `Response` using the `redirect_uri` parameter. +- A `RelyingParty` struct which can validate a `Response` by validating its `IdToken` using a key identifier (which is extracted from the `id_token`) and its public key. +- The `Subject` trait can be implemented on a custom struct representing the signing logic of a DID method. A `Provider` can ingest an object that implements the `Subject` trait so that during generation of a `Response` the DID method syntax, key identifier and signing method of the specific `Subject` can be used. - The `Validator` trait can be implemented on a custom struct representing the validating logic of a DID method. When ingested by a `RelyingParty`, it can resolve the public key that is needed for validating an `IdToken`. ## Example @@ -29,12 +29,12 @@ use async_trait::async_trait; use chrono::{Duration, Utc}; use ed25519_dalek::{Keypair, Signature, Signer}; use lazy_static::lazy_static; -use rand::rngs::OsRng; -use siopv2::{ - claims::{Claim, ClaimRequests}, - request::ResponseType, StandardClaim, - IdToken, Provider, Registration, RelyingParty, RequestUrl, Scope, SiopRequest, SiopResponse, Subject, Validator, +use openid4vc::{ + claims::{ClaimRequests, ClaimValue, IndividualClaimRequest}, + request::ResponseType, + Provider, Registration, RelyingParty, RequestUrl, Response, Scope, SiopRequest, StandardClaims, Subject, Validator, }; +use rand::rngs::OsRng; use wiremock::{ http::Method, matchers::{method, path}, @@ -114,8 +114,8 @@ async fn main() { .with_id_token_signing_alg_values_supported(vec!["EdDSA".to_string()]), ) .claims(ClaimRequests { - id_token: Some(StandardClaim { - name: Some(Claim::default()), + id_token: Some(StandardClaims { + name: Some(IndividualClaimRequest::default()), ..Default::default() }), ..Default::default() @@ -133,7 +133,7 @@ async fn main() { .mount(&mock_server) .await; - // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `SiopResponse`. + // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `Response`. Mock::given(method("POST")) .and(path("/redirect_uri")) .respond_with(ResponseTemplate::new(200)) @@ -165,35 +165,35 @@ async fn main() { // Let the provider generate a response based on the validated request. The response is an `IdToken` which is // encoded as a JWT. let response = provider - .generate_response(request, StandardClaim::default()) + .generate_response( + request, + StandardClaims { + name: Some(ClaimValue("Jane Doe".to_string())), + ..Default::default() + }, + ) .await .unwrap(); // The provider sends it's response to the mock server's `redirect_uri` endpoint. provider.send_response(response).await.unwrap(); - // Assert that the SiopResponse was successfully received by the mock server at the expected endpoint. + // Assert that the Response was successfully received by the mock server at the expected endpoint. let post_request = mock_server.received_requests().await.unwrap()[1].clone(); assert_eq!(post_request.method, Method::Post); assert_eq!(post_request.url.path(), "/redirect_uri"); - let response: SiopResponse = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap(); + let response: Response = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap(); // The `RelyingParty` then validates the response by decoding the header of the id_token, by fetching the public // key corresponding to the key identifier and finally decoding the id_token using the public key and by // validating the signature. let id_token = relying_party.validate_response(&response).await.unwrap(); - let IdToken { - iss, sub, aud, nonce, .. - } = IdToken::new( - "did:mymethod:subject".to_string(), - "did:mymethod:subject".to_string(), - "did:mymethod:relyingparty".to_string(), - "n-0S6_WzA2Mj".to_string(), - (Utc::now() + Duration::minutes(10)).timestamp(), + assert_eq!( + id_token.standard_claims(), + &StandardClaims { + name: Some(ClaimValue("Jane Doe".to_string())), + ..Default::default() + } ); - assert_eq!(id_token.iss, iss); - assert_eq!(id_token.sub, sub); - assert_eq!(id_token.aud, aud); - assert_eq!(id_token.nonce, nonce); } ``` diff --git a/src/id_token.rs b/src/id_token.rs index 16edf7de..9eab5512 100644 --- a/src/id_token.rs +++ b/src/id_token.rs @@ -1,46 +1,94 @@ -use crate::{claims::StandardClaims, StandardClaimsValues}; -use chrono::Utc; -use getset::Setters; +use crate::{builder_fn, StandardClaimsValues}; +use getset::Getters; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; // TODO: make fully feature complete and implement builder pattern: https://github.com/impierce/siopv2/issues/20 /// An SIOPv2 [`IdToken`] as specified in the [SIOPv2 specification](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-self-issued-id-token). -#[derive(Serialize, Deserialize, Debug, Setters, Default, PartialEq)] +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Getters, Default, PartialEq)] pub struct IdToken { - pub iss: String, - // TODO: sub should be part of the standard claims? - pub sub: String, - #[getset(set)] #[serde(flatten)] - pub standard_claims: StandardClaimsValues, - pub aud: String, - pub exp: i64, - pub iat: i64, - pub nonce: String, - pub state: Option, + #[getset(get = "pub")] + rfc7519_claims: RFC7519Claims, + #[serde(flatten)] + #[getset(get = "pub")] + standard_claims: StandardClaimsValues, + nonce: Option, + state: Option, + sub_jwk: Option, } impl IdToken { - pub fn new(iss: String, sub: String, aud: String, nonce: String, exp: i64) -> Self { - IdToken { - iss, - sub, - standard_claims: StandardClaims::default(), - aud, - exp, - iat: Utc::now().timestamp(), - nonce, - state: None, - } + pub fn builder() -> IdTokenBuilder { + IdTokenBuilder::new() } +} - pub fn state(mut self, state: Option) -> Self { - self.state = state; - self +/// Set of IANA registered claims by the Internet Engineering Task Force (IETF) in +/// [RFC 7519](https://tools.ietf.org/html/rfc7519#section-4.1). +#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] +pub struct RFC7519Claims { + pub iss: Option, + pub sub: Option, + pub aud: Option, + pub exp: Option, + pub nbf: Option, + pub iat: Option, + pub jti: Option, +} + +#[derive(Default)] +pub struct IdTokenBuilder { + rfc7519_claims: RFC7519Claims, + standard_claims: StandardClaimsValues, + nonce: Option, + state: Option, + sub_jwk: Option, +} + +impl IdTokenBuilder { + pub fn new() -> Self { + IdTokenBuilder::default() + } + + pub fn build(self) -> anyhow::Result { + anyhow::ensure!(self.rfc7519_claims.iss.is_some(), "iss claim is required"); + anyhow::ensure!(self.rfc7519_claims.sub.is_some(), "sub claim is required"); + anyhow::ensure!( + self.rfc7519_claims.sub.as_ref().filter(|s| s.len() <= 255).is_some(), + "sub claim MUST NOT exceed 255 ASCII characters in length" + ); + anyhow::ensure!(self.rfc7519_claims.aud.is_some(), "aud claim is required"); + anyhow::ensure!(self.rfc7519_claims.exp.is_some(), "exp claim is required"); + anyhow::ensure!(self.rfc7519_claims.iat.is_some(), "iat claim is required"); + anyhow::ensure!( + self.rfc7519_claims.iss == self.rfc7519_claims.sub, + "iss and sub must be equal" + ); + + Ok(IdToken { + rfc7519_claims: self.rfc7519_claims, + standard_claims: self.standard_claims, + nonce: self.nonce, + state: self.state, + sub_jwk: self.sub_jwk, + }) } pub fn claims(mut self, claims: StandardClaimsValues) -> Self { self.standard_claims = claims; self } + + builder_fn!(rfc7519_claims, iss, String); + builder_fn!(rfc7519_claims, sub, String); + builder_fn!(rfc7519_claims, aud, String); + builder_fn!(rfc7519_claims, exp, i64); + builder_fn!(rfc7519_claims, nbf, i64); + builder_fn!(rfc7519_claims, iat, i64); + builder_fn!(rfc7519_claims, jti, String); + builder_fn!(nonce, String); + builder_fn!(state, String); + builder_fn!(sub_jwk, String); } diff --git a/src/key_method.rs b/src/key_method.rs index 6af076b9..c5ded5b1 100644 --- a/src/key_method.rs +++ b/src/key_method.rs @@ -91,8 +91,7 @@ async fn resolve_public_key(kid: &str) -> Result> { #[cfg(test)] mod tests { use super::*; - use crate::{IdToken, Provider, RelyingParty}; - use chrono::{Duration, Utc}; + use crate::{Provider, RelyingParty}; #[tokio::test] async fn test_key_subject() { @@ -124,17 +123,6 @@ mod tests { // Let the relying party validate the response. let relying_party = RelyingParty::new(KeySubject::new()); - let id_token = relying_party.validate_response(&response).await.unwrap(); - - let IdToken { aud, nonce, .. } = IdToken::new( - "".to_string(), - "".to_string(), - "did:key:z6MkiTcXZ1JxooACo99YcfkugH6Kifzj7ZupSDCmLEABpjpF".to_string(), - "n-0S6_WzA2Mj".to_string(), - (Utc::now() + Duration::minutes(10)).timestamp(), - ); - assert_eq!(id_token.iss, id_token.sub); - assert_eq!(id_token.aud, aud); - assert_eq!(id_token.nonce, nonce); + assert!(relying_party.validate_response(&response).await.is_ok()); } } diff --git a/src/lib.rs b/src/lib.rs index e7916759..09411e5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,10 +20,26 @@ pub use registration::Registration; pub use relying_party::RelyingParty; pub use request::{RequestUrl, SiopRequest}; pub use request_builder::RequestUrlBuilder; -pub use response::SiopResponse; +pub use response::Response; pub use scope::Scope; pub use subject::Subject; pub use validator::Validator; #[cfg(test)] pub mod test_utils; + +#[macro_export] +macro_rules! builder_fn { + ( $name:ident, $ty:ty) => { + pub fn $name(mut self, value: $ty) -> Self { + self.$name.replace(value); + self + } + }; + ($field:ident, $name:ident, $ty:ty) => { + pub fn $name(mut self, value: $ty) -> Self { + self.$field.$name.replace(value); + self + } + }; +} diff --git a/src/provider.rs b/src/provider.rs index d74a3982..267c6277 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,4 +1,4 @@ -use crate::{IdToken, RequestUrl, SiopRequest, SiopResponse, StandardClaimsValues, Subject, Validator}; +use crate::{id_token::IdTokenBuilder, RequestUrl, Response, SiopRequest, StandardClaimsValues, Subject, Validator}; use anyhow::{anyhow, Result}; use chrono::{Duration, Utc}; @@ -54,37 +54,36 @@ where } // TODO: needs refactoring. - /// Generates a [`SiopResponse`] in response to a [`SiopRequest`] and the user's claims. The [`SiopResponse`] + /// Generates a [`Response`] in response to a [`SiopRequest`] and the user's claims. The [`Response`] /// contains an [`IdToken`], which is signed by the [`Subject`] of the [`Provider`]. - pub async fn generate_response( - &self, - request: SiopRequest, - user_claims: StandardClaimsValues, - ) -> Result { + pub async fn generate_response(&self, request: SiopRequest, user_claims: StandardClaimsValues) -> Result { let subject_did = self.subject.did()?; - let id_token = { - let mut id_token = IdToken::new( - subject_did.to_string(), - subject_did.to_string(), - request.client_id().clone(), - request.nonce().clone(), - (Utc::now() + Duration::minutes(10)).timestamp(), - ) - .state(request.state().clone()); - // Include the user claims in the id token. - id_token.standard_claims = user_claims; - id_token - }; + + let mut builder = IdTokenBuilder::default() + .iss(subject_did.to_string()) + .sub(subject_did.to_string()) + .aud(request.client_id().to_owned()) + .nonce(request.nonce().to_owned()) + .exp((Utc::now() + Duration::minutes(10)).timestamp()) + .iat((Utc::now()).timestamp()) + .claims(user_claims); + if let Some(state) = request.state() { + builder = builder.state(state.clone()); + } + let id_token = builder.build()?; // Include the user claims in the id token. id_token.standard_claims = user_claims; let jwt = self.subject.encode(id_token).await?; - Ok(SiopResponse::new(request.redirect_uri().clone(), jwt)) + Response::builder() + .redirect_uri(request.redirect_uri().to_owned()) + .id_token(jwt) + .build() } - pub async fn send_response(&self, response: SiopResponse) -> Result<()> { + pub async fn send_response(&self, response: Response) -> Result<()> { let client = reqwest::Client::new(); let builder = client.post(response.redirect_uri()).form(&response); builder.send().await?.text().await?; diff --git a/src/relying_party.rs b/src/relying_party.rs index b64dd400..eda47735 100644 --- a/src/relying_party.rs +++ b/src/relying_party.rs @@ -1,4 +1,4 @@ -use crate::{IdToken, SiopRequest, SiopResponse, Subject, Validator}; +use crate::{IdToken, Response, SiopRequest, Subject, Validator}; use anyhow::Result; pub struct RelyingParty @@ -20,10 +20,15 @@ where self.validator.encode(request).await } - /// Validates a [`SiopResponse`] by decoding the header of the id_token, fetching the public key corresponding to + /// Validates a [`Response`] by decoding the header of the id_token, fetching the public key corresponding to /// the key identifier and finally decoding the id_token using the public key and by validating the signature. - pub async fn validate_response(&self, response: &SiopResponse) -> Result { - let token = response.id_token.clone(); + // TODO: Validate the claims in the id_token as described here: + // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-self-issued-id-token-valida + pub async fn validate_response(&self, response: &Response) -> Result { + let token = response + .id_token() + .to_owned() + .ok_or(anyhow::anyhow!("No id_token parameter in response"))?; let id_token: IdToken = self.validator.decode(token).await?; Ok(id_token) } @@ -37,7 +42,7 @@ mod tests { request::ResponseType, scope::{Scope, ScopeValue}, test_utils::{MemoryStorage, MockSubject, Storage}, - IdToken, Provider, Registration, RequestUrl, StandardClaimsRequests, + Provider, Registration, RequestUrl, StandardClaimsRequests, }; use chrono::{Duration, Utc}; use lazy_static::lazy_static; @@ -122,7 +127,7 @@ mod tests { .mount(&mock_server) .await; - // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `SiopResponse`. + // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `Response`. Mock::given(method("POST")) .and(path("/redirect_uri")) .respond_with(ResponseTemplate::new(200)) @@ -179,31 +184,18 @@ mod tests { // The provider sends it's response to the mock server's `redirect_uri` endpoint. provider.send_response(response).await.unwrap(); - // Assert that the SiopResponse was successfully received by the mock server at the expected endpoint. + // Assert that the Response was successfully received by the mock server at the expected endpoint. let post_request = mock_server.received_requests().await.unwrap()[1].clone(); assert_eq!(post_request.method, Method::Post); assert_eq!(post_request.url.path(), "/redirect_uri"); - let response: SiopResponse = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap(); + let response: Response = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap(); // The `RelyingParty` then validates the response by decoding the header of the id_token, by fetching the public // key corresponding to the key identifier and finally decoding the id_token using the public key and by // validating the signature. let id_token = relying_party.validate_response(&response).await.unwrap(); - let IdToken { - iss, sub, aud, nonce, .. - } = IdToken::new( - "did:mock:123".to_string(), - "did:mock:123".to_string(), - "did:mock:1".to_string(), - "n-0S6_WzA2Mj".to_string(), - (Utc::now() + Duration::minutes(10)).timestamp(), - ); - assert_eq!(id_token.iss, iss); - assert_eq!(id_token.sub, sub); - assert_eq!(id_token.aud, aud); - assert_eq!(id_token.nonce, nonce); assert_eq!( - id_token.standard_claims, + id_token.standard_claims().to_owned(), StandardClaims { name: Some("Jane Doe".to_string()), email: Some("jane.doe@example.com".to_string()), diff --git a/src/response.rs b/src/response.rs index a38c19a7..575d4467 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,17 +1,113 @@ +use crate::builder_fn; +use anyhow::{anyhow, Result}; use getset::Getters; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; -/// Current implementation only supports the `id_token` response type and the cross-device implicit flow. -#[derive(Serialize, Deserialize, Debug, Getters)] -pub struct SiopResponse { +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(untagged)] +pub enum Openid4vpParams { + Jwt { + response: String, + }, + Params { + vp_token: String, + presentation_submission: String, + }, +} + +#[derive(Serialize, Default, Deserialize, Debug, Getters, PartialEq)] +#[skip_serializing_none] +pub struct Response { #[serde(skip)] #[getset(get = "pub")] redirect_uri: String, - pub id_token: String, + #[getset(get = "pub")] + id_token: Option, + #[serde(flatten)] + openid4vp_response: Option, +} + +impl Response { + pub fn builder() -> ResponseBuilder { + ResponseBuilder::new() + } +} + +#[derive(Default)] +pub struct ResponseBuilder { + redirect_uri: Option, + id_token: Option, + vp_token: Option, + presentation_submission: Option, + openid4vp_response_jwt: Option, } -impl SiopResponse { - pub fn new(redirect_uri: String, id_token: String) -> Self { - SiopResponse { redirect_uri, id_token } +impl ResponseBuilder { + pub fn new() -> Self { + ResponseBuilder::default() + } + + pub fn build(&mut self) -> Result { + let redirect_uri = self + .redirect_uri + .take() + .ok_or(anyhow!("redirect_uri parameter is required."))?; + + let openid4vp_response = match ( + self.vp_token.take(), + self.presentation_submission.take(), + self.openid4vp_response_jwt.take(), + ) { + (Some(vp_token), Some(presentation_submission), None) => Ok(Some(Openid4vpParams::Params { + vp_token, + presentation_submission, + })), + (None, None, Some(response)) => Ok(Some(Openid4vpParams::Jwt { response })), + (None, None, None) => Ok(None), + _ => Err(anyhow!("Invalid combination of openid4vp response parameters.")), + }?; + + Ok(Response { + redirect_uri, + id_token: self.id_token.take(), + openid4vp_response, + }) + } + + builder_fn!(redirect_uri, String); + builder_fn!(id_token, String); + builder_fn!(vp_token, String); + builder_fn!(presentation_submission, String); + builder_fn!(openid4vp_response_jwt, String); +} + +// TODO: Improve tests +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_openid4vp_response() { + let response = Response::builder() + .redirect_uri("redirect".to_string()) + .vp_token("vp_token".to_string()) + .presentation_submission("presentation_submission".to_string()) + .build() + .unwrap(); + + let response_string = serde_json::to_string(&response).unwrap(); + + assert_eq!( + Response { + id_token: None, + openid4vp_response: Some(Openid4vpParams::Params { + vp_token: "vp_token".to_string(), + presentation_submission: "presentation_submission".to_string(), + }), + ..Default::default() + }, + serde_json::from_str(&response_string).unwrap() + ); } } diff --git a/src/subject.rs b/src/subject.rs index 98dc5ffc..a89827a1 100644 --- a/src/subject.rs +++ b/src/subject.rs @@ -54,18 +54,18 @@ mod tests { let subject = MockSubject::new("did:mock:123".to_string(), "key_identifier".to_string()).unwrap(); let encoded = subject.encode(claims).await.unwrap(); let decoded = subject.decode::(encoded).await.unwrap(); + assert_eq!( decoded, - IdToken { - iss: "did:example:123".to_string(), - sub: "did:example:123".to_string(), - standard_claims: Default::default(), - aud: "did:example:456".to_string(), - exp: 9223372036854775807, - iat: 1593436422, - nonce: "nonce".to_string(), - state: None, - } + IdToken::builder() + .iss("did:example:123".to_string()) + .sub("did:example:123".to_string()) + .aud("did:example:456".to_string()) + .exp(9223372036854775807i64) + .iat(1593436422) + .nonce("nonce".to_string()) + .build() + .unwrap() ) } } From 970cf9ba6bedf1b380b5886008f64ecad1bff31f Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 30 May 2023 09:57:49 +0200 Subject: [PATCH 18/30] fix: silence clippy warning --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 09411e5b..6047a5e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,12 +31,14 @@ pub mod test_utils; #[macro_export] macro_rules! builder_fn { ( $name:ident, $ty:ty) => { + #[allow(clippy::should_implement_trait)] pub fn $name(mut self, value: $ty) -> Self { self.$name.replace(value); self } }; ($field:ident, $name:ident, $ty:ty) => { + #[allow(clippy::should_implement_trait)] pub fn $name(mut self, value: $ty) -> Self { self.$field.$name.replace(value); self From 23f9e48e0a33c5a1487b66041b7dfa4dfa88565d Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 30 May 2023 12:09:15 +0200 Subject: [PATCH 19/30] feat: add missing ID Token claim parameters --- src/claims.rs | 18 ++---- src/id_token.rs | 94 --------------------------- src/lib.rs | 27 ++++++-- src/provider.rs | 20 +++--- src/relying_party.rs | 8 +-- src/response.rs | 4 ++ src/token/id_token.rs | 98 ++++++++++++++++++++++++++++ src/token/id_token_builder.rs | 117 ++++++++++++++++++++++++++++++++++ src/token/mod.rs | 2 + 9 files changed, 260 insertions(+), 128 deletions(-) delete mode 100644 src/id_token.rs create mode 100644 src/token/id_token.rs create mode 100644 src/token/id_token_builder.rs create mode 100644 src/token/mod.rs diff --git a/src/claims.rs b/src/claims.rs index 69bfa2c9..b1561918 100644 --- a/src/claims.rs +++ b/src/claims.rs @@ -1,4 +1,7 @@ -use crate::scope::{Scope, ScopeValue}; +use crate::{ + parse_other, + scope::{Scope, ScopeValue}, +}; use serde::{Deserialize, Deserializer, Serialize}; use serde_with::skip_serializing_none; @@ -82,19 +85,6 @@ impl IndividualClaimRequest { object_member!(other, serde_json::Map); } -// When a struct has fields of type `Option>`, by default these fields are deserialized as -// `Some(Object {})` instead of None when the corresponding values are missing. -// The `parse_other()` helper function ensures that these fields are deserialized as `None` when no value is present. -fn parse_other<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - serde_json::Value::deserialize(deserializer).map(|value| match value { - serde_json::Value::Object(object) if !object.is_empty() => Some(object), - _ => None, - }) -} - /// An individual claim request as defined in [OpenID Connect Core 1.0, section 5.5.1](https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests). /// Individual claims can be requested by simply some key with a `null` value, or by using the `essential`, `value`, /// and `values` fields. Additional information about the requested claim MAY be added to the claim request. This diff --git a/src/id_token.rs b/src/id_token.rs deleted file mode 100644 index 9eab5512..00000000 --- a/src/id_token.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::{builder_fn, StandardClaimsValues}; -use getset::Getters; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; - -// TODO: make fully feature complete and implement builder pattern: https://github.com/impierce/siopv2/issues/20 -/// An SIOPv2 [`IdToken`] as specified in the [SIOPv2 specification](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-self-issued-id-token). -#[skip_serializing_none] -#[derive(Serialize, Deserialize, Debug, Getters, Default, PartialEq)] -pub struct IdToken { - #[serde(flatten)] - #[getset(get = "pub")] - rfc7519_claims: RFC7519Claims, - #[serde(flatten)] - #[getset(get = "pub")] - standard_claims: StandardClaimsValues, - nonce: Option, - state: Option, - sub_jwk: Option, -} - -impl IdToken { - pub fn builder() -> IdTokenBuilder { - IdTokenBuilder::new() - } -} - -/// Set of IANA registered claims by the Internet Engineering Task Force (IETF) in -/// [RFC 7519](https://tools.ietf.org/html/rfc7519#section-4.1). -#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] -pub struct RFC7519Claims { - pub iss: Option, - pub sub: Option, - pub aud: Option, - pub exp: Option, - pub nbf: Option, - pub iat: Option, - pub jti: Option, -} - -#[derive(Default)] -pub struct IdTokenBuilder { - rfc7519_claims: RFC7519Claims, - standard_claims: StandardClaimsValues, - nonce: Option, - state: Option, - sub_jwk: Option, -} - -impl IdTokenBuilder { - pub fn new() -> Self { - IdTokenBuilder::default() - } - - pub fn build(self) -> anyhow::Result { - anyhow::ensure!(self.rfc7519_claims.iss.is_some(), "iss claim is required"); - anyhow::ensure!(self.rfc7519_claims.sub.is_some(), "sub claim is required"); - anyhow::ensure!( - self.rfc7519_claims.sub.as_ref().filter(|s| s.len() <= 255).is_some(), - "sub claim MUST NOT exceed 255 ASCII characters in length" - ); - anyhow::ensure!(self.rfc7519_claims.aud.is_some(), "aud claim is required"); - anyhow::ensure!(self.rfc7519_claims.exp.is_some(), "exp claim is required"); - anyhow::ensure!(self.rfc7519_claims.iat.is_some(), "iat claim is required"); - anyhow::ensure!( - self.rfc7519_claims.iss == self.rfc7519_claims.sub, - "iss and sub must be equal" - ); - - Ok(IdToken { - rfc7519_claims: self.rfc7519_claims, - standard_claims: self.standard_claims, - nonce: self.nonce, - state: self.state, - sub_jwk: self.sub_jwk, - }) - } - - pub fn claims(mut self, claims: StandardClaimsValues) -> Self { - self.standard_claims = claims; - self - } - - builder_fn!(rfc7519_claims, iss, String); - builder_fn!(rfc7519_claims, sub, String); - builder_fn!(rfc7519_claims, aud, String); - builder_fn!(rfc7519_claims, exp, i64); - builder_fn!(rfc7519_claims, nbf, i64); - builder_fn!(rfc7519_claims, iat, i64); - builder_fn!(rfc7519_claims, jti, String); - builder_fn!(nonce, String); - builder_fn!(state, String); - builder_fn!(sub_jwk, String); -} diff --git a/src/lib.rs b/src/lib.rs index 6047a5e3..a0af869c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ pub mod claims; -pub mod id_token; pub mod jwt; pub mod key_method; pub mod provider; @@ -10,10 +9,10 @@ pub mod request_builder; pub mod response; pub mod scope; pub mod subject; +pub mod token; pub mod validator; pub use claims::{StandardClaimsRequests, StandardClaimsValues}; -pub use id_token::IdToken; pub use jwt::JsonWebToken; pub use provider::Provider; pub use registration::Registration; @@ -23,8 +22,11 @@ pub use request_builder::RequestUrlBuilder; pub use response::Response; pub use scope::Scope; pub use subject::Subject; +pub use token::{id_token::IdToken, id_token_builder::IdTokenBuilder}; pub use validator::Validator; +use serde::{Deserialize, Deserializer}; + #[cfg(test)] pub mod test_utils; @@ -32,16 +34,29 @@ pub mod test_utils; macro_rules! builder_fn { ( $name:ident, $ty:ty) => { #[allow(clippy::should_implement_trait)] - pub fn $name(mut self, value: $ty) -> Self { - self.$name.replace(value); + pub fn $name(mut self, value: impl Into<$ty>) -> Self { + self.$name.replace(value.into()); self } }; ($field:ident, $name:ident, $ty:ty) => { #[allow(clippy::should_implement_trait)] - pub fn $name(mut self, value: $ty) -> Self { - self.$field.$name.replace(value); + pub fn $name(mut self, value: impl Into<$ty>) -> Self { + self.$field.$name.replace(value.into()); self } }; } + +// When a struct has fields of type `Option>`, by default these fields are deserialized as +// `Some(Object {})` instead of None when the corresponding values are missing. +// The `parse_other()` helper function ensures that these fields are deserialized as `None` when no value is present. +pub fn parse_other<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + serde_json::Value::deserialize(deserializer).map(|value| match value { + serde_json::Value::Object(object) if !object.is_empty() => Some(object), + _ => None, + }) +} diff --git a/src/provider.rs b/src/provider.rs index 267c6277..b94101ce 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,4 +1,4 @@ -use crate::{id_token::IdTokenBuilder, RequestUrl, Response, SiopRequest, StandardClaimsValues, Subject, Validator}; +use crate::{IdToken, RequestUrl, Response, SiopRequest, StandardClaimsValues, Subject, Validator}; use anyhow::{anyhow, Result}; use chrono::{Duration, Utc}; @@ -59,28 +59,28 @@ where pub async fn generate_response(&self, request: SiopRequest, user_claims: StandardClaimsValues) -> Result { let subject_did = self.subject.did()?; - let mut builder = IdTokenBuilder::default() + let id_token = IdToken::builder() .iss(subject_did.to_string()) .sub(subject_did.to_string()) .aud(request.client_id().to_owned()) .nonce(request.nonce().to_owned()) .exp((Utc::now() + Duration::minutes(10)).timestamp()) .iat((Utc::now()).timestamp()) - .claims(user_claims); - if let Some(state) = request.state() { - builder = builder.state(state.clone()); - } - let id_token = builder.build()?; + .claims(user_claims) + .build()?; // Include the user claims in the id token. id_token.standard_claims = user_claims; let jwt = self.subject.encode(id_token).await?; - Response::builder() + let mut builder = Response::builder() .redirect_uri(request.redirect_uri().to_owned()) - .id_token(jwt) - .build() + .id_token(jwt); + if let Some(state) = request.state() { + builder = builder.state(state.clone()); + } + builder.build() } pub async fn send_response(&self, response: Response) -> Result<()> { diff --git a/src/relying_party.rs b/src/relying_party.rs index eda47735..360445c8 100644 --- a/src/relying_party.rs +++ b/src/relying_party.rs @@ -38,11 +38,11 @@ where mod tests { use super::*; use crate::{ - claims::{Address, ClaimRequests, IndividualClaimRequest, StandardClaims}, + claims::{Address, ClaimRequests, IndividualClaimRequest}, request::ResponseType, scope::{Scope, ScopeValue}, test_utils::{MemoryStorage, MockSubject, Storage}, - Provider, Registration, RequestUrl, StandardClaimsRequests, + Provider, Registration, RequestUrl, StandardClaimsRequests, StandardClaimsValues, }; use chrono::{Duration, Utc}; use lazy_static::lazy_static; @@ -158,7 +158,7 @@ mod tests { let request_claims = request.id_token_request_claims().unwrap(); assert_eq!( request_claims, - StandardClaims { + StandardClaimsRequests { name: Some(IndividualClaimRequest::Null), email: Some(IndividualClaimRequest::object().essential(true)), address: Some(IndividualClaimRequest::Null), @@ -196,7 +196,7 @@ mod tests { let id_token = relying_party.validate_response(&response).await.unwrap(); assert_eq!( id_token.standard_claims().to_owned(), - StandardClaims { + StandardClaimsValues { name: Some("Jane Doe".to_string()), email: Some("jane.doe@example.com".to_string()), updated_at: Some(1311280970), diff --git a/src/response.rs b/src/response.rs index 575d4467..c63f6826 100644 --- a/src/response.rs +++ b/src/response.rs @@ -26,6 +26,7 @@ pub struct Response { id_token: Option, #[serde(flatten)] openid4vp_response: Option, + state: Option, } impl Response { @@ -41,6 +42,7 @@ pub struct ResponseBuilder { vp_token: Option, presentation_submission: Option, openid4vp_response_jwt: Option, + state: Option, } impl ResponseBuilder { @@ -72,6 +74,7 @@ impl ResponseBuilder { redirect_uri, id_token: self.id_token.take(), openid4vp_response, + state: self.state.take(), }) } @@ -80,6 +83,7 @@ impl ResponseBuilder { builder_fn!(vp_token, String); builder_fn!(presentation_submission, String); builder_fn!(openid4vp_response_jwt, String); + builder_fn!(state, String); } // TODO: Improve tests diff --git a/src/token/id_token.rs b/src/token/id_token.rs new file mode 100644 index 00000000..c44533ce --- /dev/null +++ b/src/token/id_token.rs @@ -0,0 +1,98 @@ +use super::id_token_builder::IdTokenBuilder; +use crate::{parse_other, StandardClaimsValues}; +use getset::Getters; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +/// An SIOPv2 [`IdToken`] as specified in the [SIOPv2 specification](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-self-issued-id-token) +/// and [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#IDToken). +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Getters, Default, PartialEq)] +pub struct IdToken { + #[serde(flatten)] + #[getset(get = "pub")] + pub(super) rfc7519_claims: RFC7519Claims, + #[serde(flatten)] + #[getset(get = "pub")] + pub(super) standard_claims: StandardClaimsValues, + pub(super) auth_time: Option, + pub(super) nonce: Option, + pub(super) acr: Option, + pub(super) amr: Option>, + pub(super) azp: Option, + pub(super) sub_jwk: Option, + #[serde(flatten, deserialize_with = "parse_other")] + pub(super) other: Option>, +} + +impl IdToken { + pub fn builder() -> IdTokenBuilder { + IdTokenBuilder::new() + } +} + +// TODO: Make feature complete. +#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] +pub struct SubJwk { + pub(super) kty: String, + pub(super) n: String, + pub(super) e: String, +} + +/// Set of IANA registered claims by the Internet Engineering Task Force (IETF) in +/// [RFC 7519](https://tools.ietf.org/html/rfc7519#section-4.1). +#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] +pub struct RFC7519Claims { + pub(super) iss: Option, + pub(super) sub: Option, + pub(super) aud: Option, + pub(super) exp: Option, + pub(super) nbf: Option, + pub(super) iat: Option, + pub(super) jti: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_id_token() { + let id_token: IdToken = serde_json::from_str( + r#"{ + "iss": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs", + "sub": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs", + "aud": "https://client.example.org/cb", + "nonce": "n-0S6_WzA2Mj", + "exp": 1311281970, + "iat": 1311280970, + "sub_jwk": { + "kty": "RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e": "AQAB" + } + }"#, + ) + .unwrap(); + assert_eq!( + id_token, + IdToken { + rfc7519_claims: RFC7519Claims { + iss: Some("NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs".to_string()), + sub: Some("NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs".to_string()), + aud: Some("https://client.example.org/cb".to_string()), + exp: Some(1311281970), + iat: Some(1311280970), + ..Default::default() + }, + nonce: Some("n-0S6_WzA2Mj".to_string()), + sub_jwk: Some(SubJwk { + kty: "RSA".to_string(), + n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw".to_string(), + e: "AQAB".to_string(), + }), + ..Default::default() + } + ); + } +} diff --git a/src/token/id_token_builder.rs b/src/token/id_token_builder.rs new file mode 100644 index 00000000..680b5742 --- /dev/null +++ b/src/token/id_token_builder.rs @@ -0,0 +1,117 @@ +use super::id_token::{RFC7519Claims, SubJwk}; +use crate::{builder_fn, IdToken, StandardClaimsValues}; + +#[derive(Default)] +pub struct IdTokenBuilder { + rfc7519_claims: RFC7519Claims, + standard_claims: StandardClaimsValues, + auth_time: Option, + nonce: Option, + acr: Option, + amr: Option>, + azp: Option, + sub_jwk: Option, + other: Option>, +} + +impl IdTokenBuilder { + pub fn new() -> Self { + IdTokenBuilder::default() + } + + pub fn build(self) -> anyhow::Result { + anyhow::ensure!(self.rfc7519_claims.iss.is_some(), "iss claim is required"); + anyhow::ensure!(self.rfc7519_claims.sub.is_some(), "sub claim is required"); + anyhow::ensure!( + self.rfc7519_claims.sub.as_ref().filter(|s| s.len() <= 255).is_some(), + "sub claim MUST NOT exceed 255 ASCII characters in length" + ); + anyhow::ensure!(self.rfc7519_claims.aud.is_some(), "aud claim is required"); + anyhow::ensure!(self.rfc7519_claims.exp.is_some(), "exp claim is required"); + anyhow::ensure!(self.rfc7519_claims.iat.is_some(), "iat claim is required"); + anyhow::ensure!( + self.rfc7519_claims.iss == self.rfc7519_claims.sub, + "iss and sub must be equal" + ); + + Ok(IdToken { + rfc7519_claims: self.rfc7519_claims, + standard_claims: self.standard_claims, + auth_time: self.auth_time, + nonce: self.nonce, + acr: self.acr, + amr: self.amr, + azp: self.azp, + sub_jwk: self.sub_jwk, + other: self.other, + }) + } + + pub fn claims(mut self, claims: StandardClaimsValues) -> Self { + self.standard_claims = claims; + self + } + + builder_fn!(rfc7519_claims, iss, String); + builder_fn!(rfc7519_claims, sub, String); + builder_fn!(rfc7519_claims, aud, String); + builder_fn!(rfc7519_claims, exp, i64); + builder_fn!(rfc7519_claims, nbf, i64); + builder_fn!(rfc7519_claims, iat, i64); + builder_fn!(rfc7519_claims, jti, String); + builder_fn!(auth_time, i64); + builder_fn!(nonce, String); + builder_fn!(acr, String); + builder_fn!(amr, Vec); + builder_fn!(azp, String); + builder_fn!(sub_jwk, SubJwk); + builder_fn!(other, serde_json::Map); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_build() { + assert!(IdTokenBuilder::new() + .iss("NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs") + .sub("NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs") + .aud("https://client.example.org/cb") + .exp(1311281970) + .iat(1311280970) + .build() + .is_ok()); + } + + #[test] + fn test_invalid_build() { + assert!(IdTokenBuilder::new().build().is_err()); + + assert!(IdTokenBuilder::new() + .iss("iss") + .build() + .unwrap_err() + .to_string() + .contains("sub claim is required")); + + assert!(IdTokenBuilder::new() + .iss("iss") + .sub("x".repeat(256)) + .build() + .unwrap_err() + .to_string() + .contains("sub claim MUST NOT exceed 255 ASCII characters in length")); + + assert!(IdTokenBuilder::new() + .iss("iss") + .sub("sub") + .aud("aud") + .exp(0) + .iat(0) + .build() + .unwrap_err() + .to_string() + .contains("iss and sub must be equal")); + } +} diff --git a/src/token/mod.rs b/src/token/mod.rs new file mode 100644 index 00000000..a4646031 --- /dev/null +++ b/src/token/mod.rs @@ -0,0 +1,2 @@ +pub mod id_token; +pub mod id_token_builder; From 6fe43e83043032f7f4cb489d5904b3b3faf8f51b Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 25 Apr 2023 14:39:12 +0200 Subject: [PATCH 20/30] fix: remove skeptic crate --- src/main.rs | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/main.rs diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..d75e84e0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,222 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +mod old { + use super::*; + + #[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)] + pub struct ClaimRequests { + pub user_claims: Option, + pub id_token: Option, + } + + /// an enum to represent a claim. It can be a value, a request or the default value. + #[derive(Debug, PartialEq, Clone, Serialize, Default, Deserialize)] + #[serde(untagged)] + pub enum Claim { + #[default] + Default, + Value(T), + Request(IndividualClaimRequest), + } + + /// An individual claim request as defined in [OpenID Connect Core 1.0, section 5.5.1](https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests). + #[skip_serializing_none] + #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + pub struct IndividualClaimRequest { + pub essential: Option, + pub value: Option, + pub values: Option>, + } + + /// Standard claims as defined in [OpenID Connect Core 1.0, section 5.1](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims). + #[skip_serializing_none] + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] + #[serde(default, deny_unknown_fields)] + pub struct StandardClaims { + // Profile scope + pub name: Option>, + pub family_name: Option>, + pub given_name: Option>, + pub birthdate: Option>, + // Email scope + pub email: Option>, + pub email_verified: Option>, + // Phone scope + pub phone_number: Option>, + pub phone_number_verified: Option>, + } +} + +mod new { + use serde::{de::DeserializeOwned, Deserializer}; + + use super::*; + + /// [`Claim`] trait that is implemented by both [`ClaimValue`] and [`IndividualClaimRequest`]. + pub trait Claim: Default + Clone + DeserializeOwned { + type ValueType; + type ClaimType: Claim + Serialize + where + S: Serialize + Default + Clone + DeserializeOwned; + } + + #[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)] + pub struct ClaimValue(pub T); + + impl Claim for ClaimValue { + type ValueType = T; + type ClaimType = ClaimValue where S: Serialize + Default + Clone + DeserializeOwned; + } + + #[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)] + pub struct IndividualClaimRequest(Option>); + + impl Claim for IndividualClaimRequest { + type ValueType = T; + type ClaimType = IndividualClaimRequest where S: Serialize + Default + Clone + DeserializeOwned; + } + + // impl IndividualClaimRequest { + // pub fn from_request_object(request: IndividualClaimRequestObject) -> Self { + // IndividualClaimRequest(Some(request)) + // } + // } + + #[skip_serializing_none] + #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + pub struct IndividualClaimRequestObject { + // By requesting Claims as Essential Claims, the RP indicates to the End-User that releasing these Claims will + // ensure a smooth authorization for the specific task requested by the End-User. + pub essential: Option, + // Requests that the Claim be returned with a particular value. + pub value: Option, + // Requests that the Claim be returned with one of a set of values, with the values appearing in order of + // preference. + pub values: Option>, + // Other members MAY be defined to provide additional information about the requested Claims. Any members used that + // are not understood MUST be ignored. + #[serde(flatten, deserialize_with = "parse_other")] + pub other: Option, + } + + // When a struct has fields of type `Option`, by default these fields are deserialized as + // `Some(Object {})` instead of None when the corresponding values are missing. + // The `parse_other()` helper function ensures that these fields are deserialized as `None` when no value is present. + fn parse_other<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + match value { + serde_json::Value::Null => Ok(None), + serde_json::Value::Object(object) if object.is_empty() => Ok(None), + _ => Ok(Some(value)), + } + } + + /// This struct represents the standard claims as defined in the + /// [OpenID Connect Core 1.0 Specification](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) + /// specification. It can be used either for requesting claims using [`IndividualClaimRequest`]'s in the `claims` + /// parameter of a [`crate::SiopRequest`], or for returning actual [`ClaimValue`]'s in an [`crate::IdToken`]. + #[skip_serializing_none] + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] + #[serde(default, deny_unknown_fields)] + pub struct StandardClaims { + // Profile scope + #[serde(deserialize_with = "parse_optional_claim")] + pub name: Option>, + #[serde(deserialize_with = "parse_optional_claim")] + pub family_name: Option>, + #[serde(deserialize_with = "parse_optional_claim")] + pub given_name: Option>, + #[serde(deserialize_with = "parse_optional_claim")] + pub birthdate: Option>, + // Email scope + #[serde(deserialize_with = "parse_optional_claim")] + pub email: Option>, + #[serde(deserialize_with = "parse_optional_claim")] + pub email_verified: Option>, + // Phone scope + #[serde(deserialize_with = "parse_optional_claim")] + pub phone_number: Option>, + #[serde(deserialize_with = "parse_optional_claim")] + pub phone_number_verified: Option>, + } + + /// A helper function to deserialize a claim. If the claim is not present, it will be deserialized as a `None` value. + /// If the claim is present, but the value is `null`, it will be deserialized as `Some(Claim::default())`. + fn parse_optional_claim<'de, D, T, C>(d: D) -> Result, D::Error> + where + D: Deserializer<'de>, + T: DeserializeOwned, + C: Claim, + { + Deserialize::deserialize(d).map(|x: Option<_>| x.unwrap_or(Some(C::default()))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_old() { + let claims = serde_json::from_value::(json!({ + "name": null, + "family_name": { + "essential": true + }, + "given_name": null, + "birthdate": { + "essential": true, + "between": [ + "1970-01-01", + "2000-01-01" + ] + }})) + .unwrap(); + dbg!(claims); + } + + #[test] + fn test_new() { + let claims = serde_json::from_value::>(json!({ + "name": null, + "family_name": { + "essential": true + }, + "given_name": null, + "birthdate": { + "essential": true, + "between": [ + "1970-01-01", + "2000-01-01" + ] + }})) + .unwrap(); + dbg!(claims); + } + + #[test] + fn test_new2() { + let claims = serde_json::from_value::>(json!({ + "name": null, + "family_name": { + "essential": true + }, + "given_name": null, + "birthdate": { + "essential": true, + "between": [ + "1970-01-01", + "2000-01-01" + ] + }})) + .unwrap(); + dbg!(claims); + } +} + +fn main() {} From d5a25423f61324b25f9b721cc01e29b3e5400ee3 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Fri, 12 May 2023 20:31:27 +0200 Subject: [PATCH 21/30] feat: allow json arguments for claims() method --- src/claims.rs | 9 +++++++++ src/relying_party.rs | 20 ++++++++++---------- src/request_builder.rs | 13 +++++++++++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/claims.rs b/src/claims.rs index b1561918..37293a8c 100644 --- a/src/claims.rs +++ b/src/claims.rs @@ -12,6 +12,14 @@ pub struct ClaimRequests { pub id_token: Option, } +impl TryFrom for ClaimRequests { + type Error = anyhow::Error; + + fn try_from(value: serde_json::Value) -> Result { + serde_json::from_value(value).map_err(Into::into) + } +} + mod sealed { /// [`Claim`] trait that is implemented by both [`ClaimValue`] and [`ClaimRequest`]. pub trait Claim { @@ -229,6 +237,7 @@ where } } +// TODO: Check whether claims from a scope are essential or not. impl From<&Scope> for StandardClaimsRequests { fn from(value: &Scope) -> Self { value diff --git a/src/relying_party.rs b/src/relying_party.rs index 360445c8..55d601a3 100644 --- a/src/relying_party.rs +++ b/src/relying_party.rs @@ -104,16 +104,16 @@ mod tests { .with_subject_syntax_types_supported(vec!["did:mock".to_string()]) .with_id_token_signing_alg_values_supported(vec!["EdDSA".to_string()]), ) - .claims(ClaimRequests { - id_token: Some(StandardClaimsRequests { - name: Some(IndividualClaimRequest::Null), - email: Some(IndividualClaimRequest::object().essential(true)), - address: Some(IndividualClaimRequest::Null), - updated_at: Some(IndividualClaimRequest::Null), - ..Default::default() - }), - ..Default::default() - }) + .claims(json!({ + "id_token": { + "name": null, + "email": { + "essential": true + }, + "address": null, + "updated_at": null + } + })) .exp((Utc::now() + Duration::minutes(10)).timestamp()) .nonce("n-0S6_WzA2Mj".to_string()) .build() diff --git a/src/request_builder.rs b/src/request_builder.rs index 6a9ab1b3..d7e2a82a 100644 --- a/src/request_builder.rs +++ b/src/request_builder.rs @@ -1,5 +1,5 @@ use crate::{ - claims::ClaimRequests, + claims::{ClaimRequests, IndividualClaimRequest}, request::{RequestUrl, ResponseType, SiopRequest}, Registration, Scope, }; @@ -77,12 +77,21 @@ impl RequestUrlBuilder { } } + pub fn claims>(mut self, value: T) -> Self + where + >::Error: std::fmt::Debug, + { + let value = value.try_into().unwrap(); + self.claims = Some(value); + self + } + builder_fn!(request_uri, String); builder_fn!(response_type, ResponseType); builder_fn!(response_mode, String); builder_fn!(client_id, String); builder_fn!(scope, Scope); - builder_fn!(claims, ClaimRequests); + // builder_fn!(claims, ClaimRequests); builder_fn!(redirect_uri, String); builder_fn!(nonce, String); builder_fn!(registration, Registration); From 7a96a0f5cadb697bb903fd5a2aa135a8650a4ee9 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Sun, 21 May 2023 16:13:02 +0200 Subject: [PATCH 22/30] fix: replace unwraps --- src/request_builder.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/request_builder.rs b/src/request_builder.rs index d7e2a82a..c42549df 100644 --- a/src/request_builder.rs +++ b/src/request_builder.rs @@ -1,5 +1,5 @@ use crate::{ - claims::{ClaimRequests, IndividualClaimRequest}, + claims::ClaimRequests, request::{RequestUrl, ResponseType, SiopRequest}, Registration, Scope, }; @@ -13,7 +13,7 @@ pub struct RequestUrlBuilder { response_mode: Option, client_id: Option, scope: Option, - claims: Option, + claims: Option>, redirect_uri: Option, nonce: Option, registration: Option, @@ -55,7 +55,7 @@ impl RequestUrlBuilder { .clone() .ok_or(anyhow!("client_id parameter is required."))?, scope: self.scope.clone().ok_or(anyhow!("scope parameter is required."))?, - claims: self.claims.clone(), + claims: self.claims.as_ref().and_then(|c| c.as_ref().ok()).cloned(), redirect_uri: self .redirect_uri .clone() @@ -77,11 +77,8 @@ impl RequestUrlBuilder { } } - pub fn claims>(mut self, value: T) -> Self - where - >::Error: std::fmt::Debug, - { - let value = value.try_into().unwrap(); + pub fn claims>(mut self, value: T) -> Self { + let value = value.try_into().map_err(|_| anyhow!("faild to convert")); self.claims = Some(value); self } @@ -91,7 +88,6 @@ impl RequestUrlBuilder { builder_fn!(response_mode, String); builder_fn!(client_id, String); builder_fn!(scope, Scope); - // builder_fn!(claims, ClaimRequests); builder_fn!(redirect_uri, String); builder_fn!(nonce, String); builder_fn!(registration, Registration); From 3a7be432dfc6e9a265fc2237dcc7dd412c909bc1 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Fri, 26 May 2023 12:21:15 +0200 Subject: [PATCH 23/30] style: add specific request folder --- src/lib.rs | 4 +- src/main.rs | 222 --------------------------- src/relying_party.rs | 2 +- src/{request.rs => request/mod.rs} | 2 + src/{ => request}/request_builder.rs | 53 +++---- 5 files changed, 29 insertions(+), 254 deletions(-) delete mode 100644 src/main.rs rename src/{request.rs => request/mod.rs} (99%) rename src/{ => request}/request_builder.rs (75%) diff --git a/src/lib.rs b/src/lib.rs index a0af869c..53a79bd4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ pub mod provider; pub mod registration; pub mod relying_party; pub mod request; -pub mod request_builder; pub mod response; pub mod scope; pub mod subject; @@ -17,8 +16,7 @@ pub use jwt::JsonWebToken; pub use provider::Provider; pub use registration::Registration; pub use relying_party::RelyingParty; -pub use request::{RequestUrl, SiopRequest}; -pub use request_builder::RequestUrlBuilder; +pub use request::{request_builder::RequestUrlBuilder, RequestUrl, SiopRequest}; pub use response::Response; pub use scope::Scope; pub use subject::Subject; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index d75e84e0..00000000 --- a/src/main.rs +++ /dev/null @@ -1,222 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; - -mod old { - use super::*; - - #[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)] - pub struct ClaimRequests { - pub user_claims: Option, - pub id_token: Option, - } - - /// an enum to represent a claim. It can be a value, a request or the default value. - #[derive(Debug, PartialEq, Clone, Serialize, Default, Deserialize)] - #[serde(untagged)] - pub enum Claim { - #[default] - Default, - Value(T), - Request(IndividualClaimRequest), - } - - /// An individual claim request as defined in [OpenID Connect Core 1.0, section 5.5.1](https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests). - #[skip_serializing_none] - #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] - pub struct IndividualClaimRequest { - pub essential: Option, - pub value: Option, - pub values: Option>, - } - - /// Standard claims as defined in [OpenID Connect Core 1.0, section 5.1](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims). - #[skip_serializing_none] - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - #[serde(default, deny_unknown_fields)] - pub struct StandardClaims { - // Profile scope - pub name: Option>, - pub family_name: Option>, - pub given_name: Option>, - pub birthdate: Option>, - // Email scope - pub email: Option>, - pub email_verified: Option>, - // Phone scope - pub phone_number: Option>, - pub phone_number_verified: Option>, - } -} - -mod new { - use serde::{de::DeserializeOwned, Deserializer}; - - use super::*; - - /// [`Claim`] trait that is implemented by both [`ClaimValue`] and [`IndividualClaimRequest`]. - pub trait Claim: Default + Clone + DeserializeOwned { - type ValueType; - type ClaimType: Claim + Serialize - where - S: Serialize + Default + Clone + DeserializeOwned; - } - - #[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)] - pub struct ClaimValue(pub T); - - impl Claim for ClaimValue { - type ValueType = T; - type ClaimType = ClaimValue where S: Serialize + Default + Clone + DeserializeOwned; - } - - #[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)] - pub struct IndividualClaimRequest(Option>); - - impl Claim for IndividualClaimRequest { - type ValueType = T; - type ClaimType = IndividualClaimRequest where S: Serialize + Default + Clone + DeserializeOwned; - } - - // impl IndividualClaimRequest { - // pub fn from_request_object(request: IndividualClaimRequestObject) -> Self { - // IndividualClaimRequest(Some(request)) - // } - // } - - #[skip_serializing_none] - #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] - pub struct IndividualClaimRequestObject { - // By requesting Claims as Essential Claims, the RP indicates to the End-User that releasing these Claims will - // ensure a smooth authorization for the specific task requested by the End-User. - pub essential: Option, - // Requests that the Claim be returned with a particular value. - pub value: Option, - // Requests that the Claim be returned with one of a set of values, with the values appearing in order of - // preference. - pub values: Option>, - // Other members MAY be defined to provide additional information about the requested Claims. Any members used that - // are not understood MUST be ignored. - #[serde(flatten, deserialize_with = "parse_other")] - pub other: Option, - } - - // When a struct has fields of type `Option`, by default these fields are deserialized as - // `Some(Object {})` instead of None when the corresponding values are missing. - // The `parse_other()` helper function ensures that these fields are deserialized as `None` when no value is present. - fn parse_other<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let value = serde_json::Value::deserialize(deserializer)?; - match value { - serde_json::Value::Null => Ok(None), - serde_json::Value::Object(object) if object.is_empty() => Ok(None), - _ => Ok(Some(value)), - } - } - - /// This struct represents the standard claims as defined in the - /// [OpenID Connect Core 1.0 Specification](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) - /// specification. It can be used either for requesting claims using [`IndividualClaimRequest`]'s in the `claims` - /// parameter of a [`crate::SiopRequest`], or for returning actual [`ClaimValue`]'s in an [`crate::IdToken`]. - #[skip_serializing_none] - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - #[serde(default, deny_unknown_fields)] - pub struct StandardClaims { - // Profile scope - #[serde(deserialize_with = "parse_optional_claim")] - pub name: Option>, - #[serde(deserialize_with = "parse_optional_claim")] - pub family_name: Option>, - #[serde(deserialize_with = "parse_optional_claim")] - pub given_name: Option>, - #[serde(deserialize_with = "parse_optional_claim")] - pub birthdate: Option>, - // Email scope - #[serde(deserialize_with = "parse_optional_claim")] - pub email: Option>, - #[serde(deserialize_with = "parse_optional_claim")] - pub email_verified: Option>, - // Phone scope - #[serde(deserialize_with = "parse_optional_claim")] - pub phone_number: Option>, - #[serde(deserialize_with = "parse_optional_claim")] - pub phone_number_verified: Option>, - } - - /// A helper function to deserialize a claim. If the claim is not present, it will be deserialized as a `None` value. - /// If the claim is present, but the value is `null`, it will be deserialized as `Some(Claim::default())`. - fn parse_optional_claim<'de, D, T, C>(d: D) -> Result, D::Error> - where - D: Deserializer<'de>, - T: DeserializeOwned, - C: Claim, - { - Deserialize::deserialize(d).map(|x: Option<_>| x.unwrap_or(Some(C::default()))) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_old() { - let claims = serde_json::from_value::(json!({ - "name": null, - "family_name": { - "essential": true - }, - "given_name": null, - "birthdate": { - "essential": true, - "between": [ - "1970-01-01", - "2000-01-01" - ] - }})) - .unwrap(); - dbg!(claims); - } - - #[test] - fn test_new() { - let claims = serde_json::from_value::>(json!({ - "name": null, - "family_name": { - "essential": true - }, - "given_name": null, - "birthdate": { - "essential": true, - "between": [ - "1970-01-01", - "2000-01-01" - ] - }})) - .unwrap(); - dbg!(claims); - } - - #[test] - fn test_new2() { - let claims = serde_json::from_value::>(json!({ - "name": null, - "family_name": { - "essential": true - }, - "given_name": null, - "birthdate": { - "essential": true, - "between": [ - "1970-01-01", - "2000-01-01" - ] - }})) - .unwrap(); - dbg!(claims); - } -} - -fn main() {} diff --git a/src/relying_party.rs b/src/relying_party.rs index 55d601a3..76e2e58a 100644 --- a/src/relying_party.rs +++ b/src/relying_party.rs @@ -38,7 +38,7 @@ where mod tests { use super::*; use crate::{ - claims::{Address, ClaimRequests, IndividualClaimRequest}, + claims::{Address, IndividualClaimRequest}, request::ResponseType, scope::{Scope, ScopeValue}, test_utils::{MemoryStorage, MockSubject, Storage}, diff --git a/src/request.rs b/src/request/mod.rs similarity index 99% rename from src/request.rs rename to src/request/mod.rs index f8591567..7b2c7f62 100644 --- a/src/request.rs +++ b/src/request/mod.rs @@ -7,6 +7,8 @@ use serde_json::{Map, Value}; use std::convert::TryInto; use std::str::FromStr; +pub mod request_builder; + /// As specified in the /// [SIOPv2 specification](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-self-issued-openid-provider-a) /// [`RelyingParty`]'s can either send a request as a query parameter or as a request URI. diff --git a/src/request_builder.rs b/src/request/request_builder.rs similarity index 75% rename from src/request_builder.rs rename to src/request/request_builder.rs index c42549df..7db529a8 100644 --- a/src/request_builder.rs +++ b/src/request/request_builder.rs @@ -43,34 +43,31 @@ impl RequestUrlBuilder { let request_uri = self.request_uri.take(); match (request_uri, self.is_empty()) { (Some(request_uri), true) => Ok(RequestUrl::RequestUri { request_uri }), - (None, _) => { - let request = SiopRequest { - response_type: self - .response_type - .clone() - .ok_or(anyhow!("response_type parameter is required."))?, - response_mode: self.response_mode.clone(), - client_id: self - .client_id - .clone() - .ok_or(anyhow!("client_id parameter is required."))?, - scope: self.scope.clone().ok_or(anyhow!("scope parameter is required."))?, - claims: self.claims.as_ref().and_then(|c| c.as_ref().ok()).cloned(), - redirect_uri: self - .redirect_uri - .clone() - .ok_or(anyhow!("redirect_uri parameter is required."))?, - nonce: self.nonce.clone().ok_or(anyhow!("nonce parameter is required."))?, - registration: self.registration.clone(), - iss: self.iss.clone(), - iat: self.iat, - exp: self.exp, - nbf: self.nbf, - jti: self.jti.clone(), - state: self.state.clone(), - }; - Ok(RequestUrl::Request(Box::new(request))) - } + (None, _) => Ok(RequestUrl::Request(Box::new(SiopRequest { + response_type: self + .response_type + .clone() + .ok_or(anyhow!("response_type parameter is required."))?, + response_mode: self.response_mode.clone(), + client_id: self + .client_id + .clone() + .ok_or(anyhow!("client_id parameter is required."))?, + scope: self.scope.clone().ok_or(anyhow!("scope parameter is required."))?, + claims: self.claims.as_ref().and_then(|c| c.as_ref().ok()).cloned(), + redirect_uri: self + .redirect_uri + .clone() + .ok_or(anyhow!("redirect_uri parameter is required."))?, + nonce: self.nonce.clone().ok_or(anyhow!("nonce parameter is required."))?, + registration: self.registration.clone(), + iss: self.iss.clone(), + iat: self.iat, + exp: self.exp, + nbf: self.nbf, + jti: self.jti.clone(), + state: self.state.clone(), + }))), _ => Err(anyhow!( "request_uri and other parameters cannot be set at the same time." )), From 3187c47d5a90ecf19627c61a919c2abbf106ff0e Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 31 May 2023 10:26:32 +0200 Subject: [PATCH 24/30] fix: undo unnecassary cloning --- src/claims.rs | 8 +++++ src/lib.rs | 2 +- src/request/request_builder.rs | 65 +++++++++++++++++++++++++--------- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/claims.rs b/src/claims.rs index 37293a8c..9e0d1a80 100644 --- a/src/claims.rs +++ b/src/claims.rs @@ -20,6 +20,14 @@ impl TryFrom for ClaimRequests { } } +impl TryFrom<&str> for ClaimRequests { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + serde_json::from_str(value).map_err(Into::into) + } +} + mod sealed { /// [`Claim`] trait that is implemented by both [`ClaimValue`] and [`ClaimRequest`]. pub trait Claim { diff --git a/src/lib.rs b/src/lib.rs index 53a79bd4..749904ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ pub mod subject; pub mod token; pub mod validator; -pub use claims::{StandardClaimsRequests, StandardClaimsValues}; +pub use claims::{ClaimRequests, StandardClaimsRequests, StandardClaimsValues}; pub use jwt::JsonWebToken; pub use provider::Provider; pub use registration::Registration; diff --git a/src/request/request_builder.rs b/src/request/request_builder.rs index 7db529a8..1f69011d 100644 --- a/src/request/request_builder.rs +++ b/src/request/request_builder.rs @@ -46,27 +46,28 @@ impl RequestUrlBuilder { (None, _) => Ok(RequestUrl::Request(Box::new(SiopRequest { response_type: self .response_type - .clone() + .take() .ok_or(anyhow!("response_type parameter is required."))?, - response_mode: self.response_mode.clone(), + response_mode: self.response_mode.take(), client_id: self .client_id - .clone() + .take() .ok_or(anyhow!("client_id parameter is required."))?, - scope: self.scope.clone().ok_or(anyhow!("scope parameter is required."))?, - claims: self.claims.as_ref().and_then(|c| c.as_ref().ok()).cloned(), + scope: self.scope.take().ok_or(anyhow!("scope parameter is required."))?, + // claims: self.claims.take().and_then(|c| c.ok()), + claims: self.claims.take().transpose()?, redirect_uri: self .redirect_uri - .clone() + .take() .ok_or(anyhow!("redirect_uri parameter is required."))?, - nonce: self.nonce.clone().ok_or(anyhow!("nonce parameter is required."))?, - registration: self.registration.clone(), - iss: self.iss.clone(), + nonce: self.nonce.take().ok_or(anyhow!("nonce parameter is required."))?, + registration: self.registration.take(), + iss: self.iss.take(), iat: self.iat, exp: self.exp, nbf: self.nbf, - jti: self.jti.clone(), - state: self.state.clone(), + jti: self.jti.take(), + state: self.state.take(), }))), _ => Err(anyhow!( "request_uri and other parameters cannot be set at the same time." @@ -75,8 +76,7 @@ impl RequestUrlBuilder { } pub fn claims>(mut self, value: T) -> Self { - let value = value.try_into().map_err(|_| anyhow!("faild to convert")); - self.claims = Some(value); + self.claims = Some(value.try_into().map_err(|_| anyhow!("failed to convert"))); self } @@ -99,6 +99,7 @@ impl RequestUrlBuilder { #[cfg(test)] mod tests { use super::*; + use crate::{claims::IndividualClaimRequest, ClaimRequests, StandardClaimsRequests}; #[test] fn test_valid_request_builder() { @@ -108,6 +109,13 @@ mod tests { .scope(Scope::openid()) .redirect_uri("https://example.com".to_string()) .nonce("nonce".to_string()) + .claims( + r#"{ + "id_token": { + "name": null + } + }"#, + ) .build() .unwrap(); @@ -118,7 +126,13 @@ mod tests { response_mode: None, client_id: "did:example:123".to_string(), scope: Scope::openid(), - claims: None, + claims: Some(ClaimRequests { + id_token: Some(StandardClaimsRequests { + name: Some(IndividualClaimRequest::Null), + ..Default::default() + }), + ..Default::default() + }), redirect_uri: "https://example.com".to_string(), nonce: "nonce".to_string(), registration: None, @@ -135,15 +149,32 @@ mod tests { #[test] fn test_invalid_request_builder() { // A request builder with a `request_uri` parameter should fail to build. - let request_url = RequestUrl::builder() + assert!(RequestUrl::builder() .response_type(ResponseType::IdToken) .client_id("did:example:123".to_string()) .scope(Scope::openid()) .redirect_uri("https://example.com".to_string()) .nonce("nonce".to_string()) .request_uri("https://example.com/request_uri".to_string()) - .build(); - assert!(request_url.is_err()); + .build() + .is_err()); + + // A request builder without an invalid claim request should fail to build. + assert!(RequestUrl::builder() + .response_type(ResponseType::IdToken) + .client_id("did:example:123".to_string()) + .scope(Scope::openid()) + .redirect_uri("https://example.com".to_string()) + .nonce("nonce".to_string()) + .claims( + r#"{ + "id_token": { + "name": "invalid" + } + }"#, + ) + .build() + .is_err()); } #[test] From 015c970f18c5bbc070cecfd35ae2eddfaf3d2341 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 31 May 2023 10:29:29 +0200 Subject: [PATCH 25/30] style: explicit serde_json usage --- src/relying_party.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/relying_party.rs b/src/relying_party.rs index 76e2e58a..a7e2b491 100644 --- a/src/relying_party.rs +++ b/src/relying_party.rs @@ -46,7 +46,6 @@ mod tests { }; use chrono::{Duration, Utc}; use lazy_static::lazy_static; - use serde_json::{json, Value}; use wiremock::{ http::Method, matchers::{method, path}, @@ -54,7 +53,7 @@ mod tests { }; lazy_static! { - pub static ref USER_CLAIMS: Value = json!( + pub static ref USER_CLAIMS: serde_json::Value = serde_json::json!( { "name": "Jane Doe", "given_name": "Jane", @@ -104,16 +103,18 @@ mod tests { .with_subject_syntax_types_supported(vec!["did:mock".to_string()]) .with_id_token_signing_alg_values_supported(vec!["EdDSA".to_string()]), ) - .claims(json!({ - "id_token": { - "name": null, - "email": { - "essential": true - }, - "address": null, - "updated_at": null - } - })) + .claims( + r#"{ + "id_token": { + "name": null, + "email": { + "essential": true + }, + "address": null, + "updated_at": null + } + }"#, + ) .exp((Utc::now() + Duration::minutes(10)).timestamp()) .nonce("n-0S6_WzA2Mj".to_string()) .build() From 7534575142a4887346995c9b421d33ee5380f78a Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 31 May 2023 11:05:07 +0200 Subject: [PATCH 26/30] test: improve RequestBuilder tests --- src/claims.rs | 1 - src/provider.rs | 1 - src/request/request_builder.rs | 1 - src/response.rs | 80 +++++++++++++++++++++++++++------- 4 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/claims.rs b/src/claims.rs index 9e0d1a80..ccf1e980 100644 --- a/src/claims.rs +++ b/src/claims.rs @@ -245,7 +245,6 @@ where } } -// TODO: Check whether claims from a scope are essential or not. impl From<&Scope> for StandardClaimsRequests { fn from(value: &Scope) -> Self { value diff --git a/src/provider.rs b/src/provider.rs index b94101ce..2f503acb 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -53,7 +53,6 @@ where }) } - // TODO: needs refactoring. /// Generates a [`Response`] in response to a [`SiopRequest`] and the user's claims. The [`Response`] /// contains an [`IdToken`], which is signed by the [`Subject`] of the [`Provider`]. pub async fn generate_response(&self, request: SiopRequest, user_claims: StandardClaimsValues) -> Result { diff --git a/src/request/request_builder.rs b/src/request/request_builder.rs index 1f69011d..67cfa53e 100644 --- a/src/request/request_builder.rs +++ b/src/request/request_builder.rs @@ -54,7 +54,6 @@ impl RequestUrlBuilder { .take() .ok_or(anyhow!("client_id parameter is required."))?, scope: self.scope.take().ok_or(anyhow!("scope parameter is required."))?, - // claims: self.claims.take().and_then(|c| c.ok()), claims: self.claims.take().transpose()?, redirect_uri: self .redirect_uri diff --git a/src/response.rs b/src/response.rs index c63f6826..8f3adf54 100644 --- a/src/response.rs +++ b/src/response.rs @@ -67,7 +67,15 @@ impl ResponseBuilder { })), (None, None, Some(response)) => Ok(Some(Openid4vpParams::Jwt { response })), (None, None, None) => Ok(None), - _ => Err(anyhow!("Invalid combination of openid4vp response parameters.")), + (Some(_), None, None) => Err(anyhow!( + "`presentation_submission` parameter is required when using `vp_token` parameter." + )), + (None, Some(_), None) => Err(anyhow!( + "`vp_token` parameter is required when using `presentation_submission` parameter." + )), + _ => Err(anyhow!( + "`response` parameter can not be used with `vp_token` and `presentation_submission` parameters." + )), }?; Ok(Response { @@ -86,32 +94,74 @@ impl ResponseBuilder { builder_fn!(state, String); } -// TODO: Improve tests #[cfg(test)] mod tests { use super::*; #[test] - fn test_openid4vp_response() { - let response = Response::builder() + fn test_valid_response() { + assert!(Response::builder() + .redirect_uri("redirect".to_string()) + .id_token("id_token".to_string()) + .build() + .is_ok()); + + assert!(Response::builder() .redirect_uri("redirect".to_string()) .vp_token("vp_token".to_string()) .presentation_submission("presentation_submission".to_string()) .build() - .unwrap(); + .is_ok()); + + assert!(Response::builder() + .redirect_uri("redirect".to_string()) + .id_token("id_token".to_string()) + .vp_token("vp_token".to_string()) + .presentation_submission("presentation_submission".to_string()) + .build() + .is_ok()); + } - let response_string = serde_json::to_string(&response).unwrap(); + #[test] + fn test_invalid_response() { + assert_eq!( + Response::builder() + .id_token("id_token".to_string()) + .build() + .unwrap_err() + .to_string(), + "redirect_uri parameter is required." + ); + + assert_eq!( + Response::builder() + .redirect_uri("redirect".to_string()) + .vp_token("vp_token".to_string()) + .build() + .unwrap_err() + .to_string(), + "`presentation_submission` parameter is required when using `vp_token` parameter." + ); + + assert_eq!( + Response::builder() + .redirect_uri("redirect".to_string()) + .presentation_submission("presentation_submission".to_string()) + .build() + .unwrap_err() + .to_string(), + "`vp_token` parameter is required when using `presentation_submission` parameter." + ); assert_eq!( - Response { - id_token: None, - openid4vp_response: Some(Openid4vpParams::Params { - vp_token: "vp_token".to_string(), - presentation_submission: "presentation_submission".to_string(), - }), - ..Default::default() - }, - serde_json::from_str(&response_string).unwrap() + Response::builder() + .redirect_uri("redirect".to_string()) + .presentation_submission("presentation_submission".to_string()) + .openid4vp_response_jwt("response".to_string()) + .build() + .unwrap_err() + .to_string(), + "`response` parameter can not be used with `vp_token` and `presentation_submission` parameters." ); } } From 79053a26597e92f67de41d8e0389542416dac46c Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Mon, 5 Jun 2023 21:19:08 +0200 Subject: [PATCH 27/30] fix: fix rebase --- src/provider.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/provider.rs b/src/provider.rs index 2f503acb..e3838954 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -68,9 +68,6 @@ where .claims(user_claims) .build()?; - // Include the user claims in the id token. - id_token.standard_claims = user_claims; - let jwt = self.subject.encode(id_token).await?; let mut builder = Response::builder() From 7907fac54054bdd9f6b1daeed49e57534d8dd521 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Mon, 5 Jun 2023 21:40:56 +0200 Subject: [PATCH 28/30] style: Rename SiopRequest and add comments --- README.md | 8 ++++---- src/claims.rs | 4 ++-- src/lib.rs | 2 +- src/provider.rs | 12 ++++++------ src/relying_party.rs | 8 ++++---- src/request/mod.rs | 14 +++++++------- src/request/request_builder.rs | 6 +++--- src/response.rs | 4 ++++ 8 files changed, 31 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 7ad83402..e19498af 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ OpenID for Verifiable Credentials (OID4VC) consists of the following specificati Currently the Implicit Flow is consists of four major parts: -- A `Provider` that can accept a `SiopRequest` and generate a `Response` by creating an `IdToken`, adding its key identifier to the header of the `id_token`, signing the `id_token` and wrap it into a `Response`. It can also send the `Response` using the `redirect_uri` parameter. +- A `Provider` that can accept a `Request` and generate a `Response` by creating an `IdToken`, adding its key identifier to the header of the `id_token`, signing the `id_token` and wrap it into a `Response`. It can also send the `Response` using the `redirect_uri` parameter. - A `RelyingParty` struct which can validate a `Response` by validating its `IdToken` using a key identifier (which is extracted from the `id_token`) and its public key. - The `Subject` trait can be implemented on a custom struct representing the signing logic of a DID method. A `Provider` can ingest an object that implements the `Subject` trait so that during generation of a `Response` the DID method syntax, key identifier and signing method of the specific `Subject` can be used. - The `Validator` trait can be implemented on a custom struct representing the validating logic of a DID method. When ingested by a `RelyingParty`, it can resolve the public key that is needed for validating an `IdToken`. @@ -32,7 +32,7 @@ use lazy_static::lazy_static; use openid4vc::{ claims::{ClaimRequests, ClaimValue, IndividualClaimRequest}, request::ResponseType, - Provider, Registration, RelyingParty, RequestUrl, Response, Scope, SiopRequest, StandardClaims, Subject, Validator, + Provider, Registration, RelyingParty, RequestUrl, Response, Scope, Request, StandardClaims, Subject, Validator, }; use rand::rngs::OsRng; use wiremock::{ @@ -102,7 +102,7 @@ async fn main() { let relying_party = RelyingParty::new(validator); // Create a new RequestUrl with response mode `post` for cross-device communication. - let request: SiopRequest = RequestUrl::builder() + let request: Request = RequestUrl::builder() .response_type(ResponseType::IdToken) .client_id("did:mymethod:relyingparty".to_string()) .scope(Scope::openid()) @@ -126,7 +126,7 @@ async fn main() { .and_then(TryInto::try_into) .unwrap(); - // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `SiopRequest`. + // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `Request`. Mock::given(method("GET")) .and(path("/request_uri")) .respond_with(ResponseTemplate::new(200).set_body_string(relying_party.encode(&request).await.unwrap())) diff --git a/src/claims.rs b/src/claims.rs index ccf1e980..848d3a92 100644 --- a/src/claims.rs +++ b/src/claims.rs @@ -5,7 +5,7 @@ use crate::{ use serde::{Deserialize, Deserializer, Serialize}; use serde_with::skip_serializing_none; -/// Functions as the `claims` parameter inside a [`crate::SiopRequest`]. +/// Functions as the `claims` parameter inside a [`crate::Request`]. #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ClaimRequests { pub user_claims: Option, @@ -134,7 +134,7 @@ pub type StandardClaimsValues = StandardClaims>; /// This struct represents the standard claims as defined in the /// [OpenID Connect Core 1.0 Specification](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) /// specification. It can be used either for requesting claims using [`IndividualClaimRequest`]'s in the `claims` -/// parameter of a [`crate::SiopRequest`], or for returning actual [`ClaimValue`]'s in an [`crate::IdToken`]. +/// parameter of a [`crate::Request`], or for returning actual [`ClaimValue`]'s in an [`crate::IdToken`]. #[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] diff --git a/src/lib.rs b/src/lib.rs index 749904ed..7d2468f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ pub use jwt::JsonWebToken; pub use provider::Provider; pub use registration::Registration; pub use relying_party::RelyingParty; -pub use request::{request_builder::RequestUrlBuilder, RequestUrl, SiopRequest}; +pub use request::{request_builder::RequestUrlBuilder, Request, RequestUrl}; pub use response::Response; pub use scope::Scope; pub use subject::Subject; diff --git a/src/provider.rs b/src/provider.rs index e3838954..93992e0d 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,9 +1,9 @@ -use crate::{IdToken, RequestUrl, Response, SiopRequest, StandardClaimsValues, Subject, Validator}; +use crate::{IdToken, Request, RequestUrl, Response, StandardClaimsValues, Subject, Validator}; use anyhow::{anyhow, Result}; use chrono::{Duration, Utc}; /// A Self-Issued OpenID Provider (SIOP), which is responsible for generating and signing [`IdToken`]'s in response to -/// [`SiopRequest`]'s from [crate::relying_party::RelyingParty]'s (RPs). The [`Provider`] acts as a trusted intermediary between the RPs and +/// [`Request`]'s from [crate::relying_party::RelyingParty]'s (RPs). The [`Provider`] acts as a trusted intermediary between the RPs and /// the user who is trying to authenticate. #[derive(Default)] pub struct Provider @@ -27,10 +27,10 @@ where } /// TODO: Add more validation rules. - /// Takes a [`RequestUrl`] and returns a [`SiopRequest`]. The [`RequestUrl`] can either be a [`SiopRequest`] or a + /// Takes a [`RequestUrl`] and returns a [`Request`]. The [`RequestUrl`] can either be a [`Request`] or a /// request by value. If the [`RequestUrl`] is a request by value, the request is decoded by the [`Subject`] of the [`Provider`]. /// If the request is valid, the request is returned. - pub async fn validate_request(&self, request: RequestUrl) -> Result { + pub async fn validate_request(&self, request: RequestUrl) -> Result { let request = match request { RequestUrl::Request(request) => *request, RequestUrl::RequestUri { request_uri } => { @@ -53,9 +53,9 @@ where }) } - /// Generates a [`Response`] in response to a [`SiopRequest`] and the user's claims. The [`Response`] + /// Generates a [`Response`] in response to a [`Request`] and the user's claims. The [`Response`] /// contains an [`IdToken`], which is signed by the [`Subject`] of the [`Provider`]. - pub async fn generate_response(&self, request: SiopRequest, user_claims: StandardClaimsValues) -> Result { + pub async fn generate_response(&self, request: Request, user_claims: StandardClaimsValues) -> Result { let subject_did = self.subject.did()?; let id_token = IdToken::builder() diff --git a/src/relying_party.rs b/src/relying_party.rs index a7e2b491..eae7500e 100644 --- a/src/relying_party.rs +++ b/src/relying_party.rs @@ -1,4 +1,4 @@ -use crate::{IdToken, Response, SiopRequest, Subject, Validator}; +use crate::{IdToken, Request, Response, Subject, Validator}; use anyhow::Result; pub struct RelyingParty @@ -16,7 +16,7 @@ where RelyingParty { validator } } - pub async fn encode(&self, request: &SiopRequest) -> Result { + pub async fn encode(&self, request: &Request) -> Result { self.validator.encode(request).await } @@ -92,7 +92,7 @@ mod tests { let relying_party = RelyingParty::new(validator); // Create a new RequestUrl with response mode `post` for cross-device communication. - let request: SiopRequest = RequestUrl::builder() + let request: Request = RequestUrl::builder() .response_type(ResponseType::IdToken) .client_id("did:mock:1".to_string()) .scope(Scope::from(vec![ScopeValue::OpenId, ScopeValue::Phone])) @@ -121,7 +121,7 @@ mod tests { .and_then(TryInto::try_into) .unwrap(); - // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `SiopRequest`. + // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `Request`. Mock::given(method("GET")) .and(path("/request_uri")) .respond_with(ResponseTemplate::new(200).set_body_string(relying_party.encode(&request).await.unwrap())) diff --git a/src/request/mod.rs b/src/request/mod.rs index 7b2c7f62..f7704562 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -52,7 +52,7 @@ pub mod request_builder; #[derive(Deserialize, Debug, PartialEq, Clone, Serialize)] #[serde(untagged, deny_unknown_fields)] pub enum RequestUrl { - Request(Box), + Request(Box), // TODO: Add client_id parameter. RequestUri { request_uri: String }, } @@ -63,10 +63,10 @@ impl RequestUrl { } } -impl TryInto for RequestUrl { +impl TryInto for RequestUrl { type Error = anyhow::Error; - fn try_into(self) -> Result { + fn try_into(self) -> Result { match self { RequestUrl::Request(request) => Ok(*request), RequestUrl::RequestUri { .. } => Err(anyhow!("Request is a request URI.")), @@ -125,11 +125,11 @@ pub enum ResponseType { IdToken, } -/// [`SiopRequest`] is a request from a [crate::relying_party::RelyingParty] (RP) to a [crate::provider::Provider] (SIOP). +/// [`Request`] is a request from a [crate::relying_party::RelyingParty] (RP) to a [crate::provider::Provider] (SIOP). #[allow(dead_code)] #[derive(Debug, Getters, PartialEq, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct SiopRequest { +pub struct Request { pub(crate) response_type: ResponseType, pub(crate) response_mode: Option, #[getset(get = "pub")] @@ -153,7 +153,7 @@ pub struct SiopRequest { pub(crate) state: Option, } -impl SiopRequest { +impl Request { pub fn is_cross_device_request(&self) -> bool { self.response_mode == Some("post".to_string()) } @@ -212,7 +212,7 @@ mod tests { .unwrap(); assert_eq!( request_url.clone(), - RequestUrl::Request(Box::new(SiopRequest { + RequestUrl::Request(Box::new(Request { response_type: ResponseType::IdToken, response_mode: Some("post".to_string()), client_id: "did:example:\ diff --git a/src/request/request_builder.rs b/src/request/request_builder.rs index 67cfa53e..0fd49762 100644 --- a/src/request/request_builder.rs +++ b/src/request/request_builder.rs @@ -1,6 +1,6 @@ use crate::{ claims::ClaimRequests, - request::{RequestUrl, ResponseType, SiopRequest}, + request::{Request, RequestUrl, ResponseType}, Registration, Scope, }; use anyhow::{anyhow, Result}; @@ -43,7 +43,7 @@ impl RequestUrlBuilder { let request_uri = self.request_uri.take(); match (request_uri, self.is_empty()) { (Some(request_uri), true) => Ok(RequestUrl::RequestUri { request_uri }), - (None, _) => Ok(RequestUrl::Request(Box::new(SiopRequest { + (None, _) => Ok(RequestUrl::Request(Box::new(Request { response_type: self .response_type .take() @@ -120,7 +120,7 @@ mod tests { assert_eq!( request_url, - RequestUrl::Request(Box::new(SiopRequest { + RequestUrl::Request(Box::new(Request { response_type: ResponseType::IdToken, response_mode: None, client_id: "did:example:123".to_string(), diff --git a/src/response.rs b/src/response.rs index 8f3adf54..c251322a 100644 --- a/src/response.rs +++ b/src/response.rs @@ -4,6 +4,8 @@ use getset::Getters; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; +/// Represents the parameters of an OpenID4VP response. It can hold a Verifiable Presentation Token and a Presentation +/// Submission, or a JWT containing them. #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(untagged)] pub enum Openid4vpParams { @@ -16,6 +18,8 @@ pub enum Openid4vpParams { }, } +/// Represents an Authorization Response. It can hold an ID Token, a Verifiable Presentation Token, a Presentation +/// Submission, or a combination of them. #[derive(Serialize, Default, Deserialize, Debug, Getters, PartialEq)] #[skip_serializing_none] pub struct Response { From c0d8b8caed2e1817a2455bb4066f6954de6d9202 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Mon, 5 Jun 2023 22:47:27 +0200 Subject: [PATCH 29/30] style: rename Request and Response --- README.md | 18 +++++++++--------- src/claims.rs | 4 ++-- src/lib.rs | 4 ++-- src/provider.rs | 24 +++++++++++++++--------- src/relying_party.rs | 18 +++++++++--------- src/request/mod.rs | 24 ++++++++++++------------ src/request/request_builder.rs | 6 +++--- src/response.rs | 24 ++++++++++++------------ 8 files changed, 64 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index e19498af..8ea5f737 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ OpenID for Verifiable Credentials (OID4VC) consists of the following specificati Currently the Implicit Flow is consists of four major parts: -- A `Provider` that can accept a `Request` and generate a `Response` by creating an `IdToken`, adding its key identifier to the header of the `id_token`, signing the `id_token` and wrap it into a `Response`. It can also send the `Response` using the `redirect_uri` parameter. -- A `RelyingParty` struct which can validate a `Response` by validating its `IdToken` using a key identifier (which is extracted from the `id_token`) and its public key. -- The `Subject` trait can be implemented on a custom struct representing the signing logic of a DID method. A `Provider` can ingest an object that implements the `Subject` trait so that during generation of a `Response` the DID method syntax, key identifier and signing method of the specific `Subject` can be used. +- A `Provider` that can accept a `AuthorizationRequest` and generate a `AuthorizationResponse` by creating an `IdToken`, adding its key identifier to the header of the `id_token`, signing the `id_token` and wrap it into a `AuthorizationResponse`. It can also send the `AuthorizationResponse` using the `redirect_uri` parameter. +- A `RelyingParty` struct which can validate a `AuthorizationResponse` by validating its `IdToken` using a key identifier (which is extracted from the `id_token`) and its public key. +- The `Subject` trait can be implemented on a custom struct representing the signing logic of a DID method. A `Provider` can ingest an object that implements the `Subject` trait so that during generation of a `AuthorizationResponse` the DID method syntax, key identifier and signing method of the specific `Subject` can be used. - The `Validator` trait can be implemented on a custom struct representing the validating logic of a DID method. When ingested by a `RelyingParty`, it can resolve the public key that is needed for validating an `IdToken`. ## Example @@ -32,7 +32,7 @@ use lazy_static::lazy_static; use openid4vc::{ claims::{ClaimRequests, ClaimValue, IndividualClaimRequest}, request::ResponseType, - Provider, Registration, RelyingParty, RequestUrl, Response, Scope, Request, StandardClaims, Subject, Validator, + Provider, Registration, RelyingParty, RequestUrl, AuthorizationResponse, Scope, AuthorizationRequest, StandardClaims, Subject, Validator, }; use rand::rngs::OsRng; use wiremock::{ @@ -102,7 +102,7 @@ async fn main() { let relying_party = RelyingParty::new(validator); // Create a new RequestUrl with response mode `post` for cross-device communication. - let request: Request = RequestUrl::builder() + let request: AuthorizationRequest = RequestUrl::builder() .response_type(ResponseType::IdToken) .client_id("did:mymethod:relyingparty".to_string()) .scope(Scope::openid()) @@ -126,14 +126,14 @@ async fn main() { .and_then(TryInto::try_into) .unwrap(); - // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `Request`. + // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `AuthorizationRequest`. Mock::given(method("GET")) .and(path("/request_uri")) .respond_with(ResponseTemplate::new(200).set_body_string(relying_party.encode(&request).await.unwrap())) .mount(&mock_server) .await; - // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `Response`. + // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `AuthorizationResponse`. Mock::given(method("POST")) .and(path("/redirect_uri")) .respond_with(ResponseTemplate::new(200)) @@ -178,11 +178,11 @@ async fn main() { // The provider sends it's response to the mock server's `redirect_uri` endpoint. provider.send_response(response).await.unwrap(); - // Assert that the Response was successfully received by the mock server at the expected endpoint. + // Assert that the AuthorizationResponse was successfully received by the mock server at the expected endpoint. let post_request = mock_server.received_requests().await.unwrap()[1].clone(); assert_eq!(post_request.method, Method::Post); assert_eq!(post_request.url.path(), "/redirect_uri"); - let response: Response = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap(); + let response: AuthorizationResponse = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap(); // The `RelyingParty` then validates the response by decoding the header of the id_token, by fetching the public // key corresponding to the key identifier and finally decoding the id_token using the public key and by diff --git a/src/claims.rs b/src/claims.rs index 848d3a92..6828880b 100644 --- a/src/claims.rs +++ b/src/claims.rs @@ -5,7 +5,7 @@ use crate::{ use serde::{Deserialize, Deserializer, Serialize}; use serde_with::skip_serializing_none; -/// Functions as the `claims` parameter inside a [`crate::Request`]. +/// Functions as the `claims` parameter inside a [`crate::AuthorizationRequest`]. #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ClaimRequests { pub user_claims: Option, @@ -134,7 +134,7 @@ pub type StandardClaimsValues = StandardClaims>; /// This struct represents the standard claims as defined in the /// [OpenID Connect Core 1.0 Specification](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) /// specification. It can be used either for requesting claims using [`IndividualClaimRequest`]'s in the `claims` -/// parameter of a [`crate::Request`], or for returning actual [`ClaimValue`]'s in an [`crate::IdToken`]. +/// parameter of a [`crate::AuthorizationRequest`], or for returning actual [`ClaimValue`]'s in an [`crate::IdToken`]. #[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] diff --git a/src/lib.rs b/src/lib.rs index 7d2468f5..40154660 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,8 +16,8 @@ pub use jwt::JsonWebToken; pub use provider::Provider; pub use registration::Registration; pub use relying_party::RelyingParty; -pub use request::{request_builder::RequestUrlBuilder, Request, RequestUrl}; -pub use response::Response; +pub use request::{request_builder::RequestUrlBuilder, AuthorizationRequest, RequestUrl}; +pub use response::AuthorizationResponse; pub use scope::Scope; pub use subject::Subject; pub use token::{id_token::IdToken, id_token_builder::IdTokenBuilder}; diff --git a/src/provider.rs b/src/provider.rs index 93992e0d..eca6a825 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,9 +1,11 @@ -use crate::{IdToken, Request, RequestUrl, Response, StandardClaimsValues, Subject, Validator}; +use crate::{ + AuthorizationRequest, AuthorizationResponse, IdToken, RequestUrl, StandardClaimsValues, Subject, Validator, +}; use anyhow::{anyhow, Result}; use chrono::{Duration, Utc}; /// A Self-Issued OpenID Provider (SIOP), which is responsible for generating and signing [`IdToken`]'s in response to -/// [`Request`]'s from [crate::relying_party::RelyingParty]'s (RPs). The [`Provider`] acts as a trusted intermediary between the RPs and +/// [`AuthorizationRequest`]'s from [crate::relying_party::RelyingParty]'s (RPs). The [`Provider`] acts as a trusted intermediary between the RPs and /// the user who is trying to authenticate. #[derive(Default)] pub struct Provider @@ -27,12 +29,12 @@ where } /// TODO: Add more validation rules. - /// Takes a [`RequestUrl`] and returns a [`Request`]. The [`RequestUrl`] can either be a [`Request`] or a + /// Takes a [`RequestUrl`] and returns a [`AuthorizationRequest`]. The [`RequestUrl`] can either be a [`AuthorizationRequest`] or a /// request by value. If the [`RequestUrl`] is a request by value, the request is decoded by the [`Subject`] of the [`Provider`]. /// If the request is valid, the request is returned. - pub async fn validate_request(&self, request: RequestUrl) -> Result { + pub async fn validate_request(&self, request: RequestUrl) -> Result { let request = match request { - RequestUrl::Request(request) => *request, + RequestUrl::AuthorizationRequest(request) => *request, RequestUrl::RequestUri { request_uri } => { let client = reqwest::Client::new(); let builder = client.get(request_uri); @@ -53,9 +55,13 @@ where }) } - /// Generates a [`Response`] in response to a [`Request`] and the user's claims. The [`Response`] + /// Generates a [`AuthorizationResponse`] in response to a [`AuthorizationRequest`] and the user's claims. The [`AuthorizationResponse`] /// contains an [`IdToken`], which is signed by the [`Subject`] of the [`Provider`]. - pub async fn generate_response(&self, request: Request, user_claims: StandardClaimsValues) -> Result { + pub async fn generate_response( + &self, + request: AuthorizationRequest, + user_claims: StandardClaimsValues, + ) -> Result { let subject_did = self.subject.did()?; let id_token = IdToken::builder() @@ -70,7 +76,7 @@ where let jwt = self.subject.encode(id_token).await?; - let mut builder = Response::builder() + let mut builder = AuthorizationResponse::builder() .redirect_uri(request.redirect_uri().to_owned()) .id_token(jwt); if let Some(state) = request.state() { @@ -79,7 +85,7 @@ where builder.build() } - pub async fn send_response(&self, response: Response) -> Result<()> { + pub async fn send_response(&self, response: AuthorizationResponse) -> Result<()> { let client = reqwest::Client::new(); let builder = client.post(response.redirect_uri()).form(&response); builder.send().await?.text().await?; diff --git a/src/relying_party.rs b/src/relying_party.rs index eae7500e..7e4873c4 100644 --- a/src/relying_party.rs +++ b/src/relying_party.rs @@ -1,4 +1,4 @@ -use crate::{IdToken, Request, Response, Subject, Validator}; +use crate::{AuthorizationRequest, AuthorizationResponse, IdToken, Subject, Validator}; use anyhow::Result; pub struct RelyingParty @@ -16,15 +16,15 @@ where RelyingParty { validator } } - pub async fn encode(&self, request: &Request) -> Result { + pub async fn encode(&self, request: &AuthorizationRequest) -> Result { self.validator.encode(request).await } - /// Validates a [`Response`] by decoding the header of the id_token, fetching the public key corresponding to + /// Validates a [`AuthorizationResponse`] by decoding the header of the id_token, fetching the public key corresponding to /// the key identifier and finally decoding the id_token using the public key and by validating the signature. // TODO: Validate the claims in the id_token as described here: // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-self-issued-id-token-valida - pub async fn validate_response(&self, response: &Response) -> Result { + pub async fn validate_response(&self, response: &AuthorizationResponse) -> Result { let token = response .id_token() .to_owned() @@ -92,7 +92,7 @@ mod tests { let relying_party = RelyingParty::new(validator); // Create a new RequestUrl with response mode `post` for cross-device communication. - let request: Request = RequestUrl::builder() + let request: AuthorizationRequest = RequestUrl::builder() .response_type(ResponseType::IdToken) .client_id("did:mock:1".to_string()) .scope(Scope::from(vec![ScopeValue::OpenId, ScopeValue::Phone])) @@ -121,14 +121,14 @@ mod tests { .and_then(TryInto::try_into) .unwrap(); - // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `Request`. + // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `AuthorizationRequest`. Mock::given(method("GET")) .and(path("/request_uri")) .respond_with(ResponseTemplate::new(200).set_body_string(relying_party.encode(&request).await.unwrap())) .mount(&mock_server) .await; - // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `Response`. + // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `AuthorizationResponse`. Mock::given(method("POST")) .and(path("/redirect_uri")) .respond_with(ResponseTemplate::new(200)) @@ -185,11 +185,11 @@ mod tests { // The provider sends it's response to the mock server's `redirect_uri` endpoint. provider.send_response(response).await.unwrap(); - // Assert that the Response was successfully received by the mock server at the expected endpoint. + // Assert that the AuthorizationResponse was successfully received by the mock server at the expected endpoint. let post_request = mock_server.received_requests().await.unwrap()[1].clone(); assert_eq!(post_request.method, Method::Post); assert_eq!(post_request.url.path(), "/redirect_uri"); - let response: Response = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap(); + let response: AuthorizationResponse = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap(); // The `RelyingParty` then validates the response by decoding the header of the id_token, by fetching the public // key corresponding to the key identifier and finally decoding the id_token using the public key and by diff --git a/src/request/mod.rs b/src/request/mod.rs index f7704562..c29e2107 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -28,7 +28,7 @@ pub mod request_builder; /// } /// ); /// -/// // An example of a form-urlencoded request that is parsed as a `RequestUrl::Request` variant. +/// // An example of a form-urlencoded request that is parsed as a `RequestUrl::AuthorizationRequest` variant. /// let request_url = RequestUrl::from_str( /// "\ /// siopv2://idtoken?\ @@ -45,14 +45,14 @@ pub mod request_builder; /// ) /// .unwrap(); /// assert!(match request_url { -/// RequestUrl::Request(_) => Ok(()), +/// RequestUrl::AuthorizationRequest(_) => Ok(()), /// RequestUrl::RequestUri { .. } => Err(()), /// }.is_ok()); /// ``` #[derive(Deserialize, Debug, PartialEq, Clone, Serialize)] #[serde(untagged, deny_unknown_fields)] pub enum RequestUrl { - Request(Box), + AuthorizationRequest(Box), // TODO: Add client_id parameter. RequestUri { request_uri: String }, } @@ -63,13 +63,13 @@ impl RequestUrl { } } -impl TryInto for RequestUrl { +impl TryInto for RequestUrl { type Error = anyhow::Error; - fn try_into(self) -> Result { + fn try_into(self) -> Result { match self { - RequestUrl::Request(request) => Ok(*request), - RequestUrl::RequestUri { .. } => Err(anyhow!("Request is a request URI.")), + RequestUrl::AuthorizationRequest(request) => Ok(*request), + RequestUrl::RequestUri { .. } => Err(anyhow!("AuthorizationRequest is a request URI.")), } } } @@ -125,11 +125,11 @@ pub enum ResponseType { IdToken, } -/// [`Request`] is a request from a [crate::relying_party::RelyingParty] (RP) to a [crate::provider::Provider] (SIOP). +/// [`AuthorizationRequest`] is a request from a [crate::relying_party::RelyingParty] (RP) to a [crate::provider::Provider] (SIOP). #[allow(dead_code)] #[derive(Debug, Getters, PartialEq, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct Request { +pub struct AuthorizationRequest { pub(crate) response_type: ResponseType, pub(crate) response_mode: Option, #[getset(get = "pub")] @@ -153,7 +153,7 @@ pub struct Request { pub(crate) state: Option, } -impl Request { +impl AuthorizationRequest { pub fn is_cross_device_request(&self) -> bool { self.response_mode == Some("post".to_string()) } @@ -194,7 +194,7 @@ mod tests { #[test] fn test_valid_request() { - // A form urlencoded string without a `request_uri` parameter should deserialize into the `RequestUrl::Request` variant. + // A form urlencoded string without a `request_uri` parameter should deserialize into the `RequestUrl::AuthorizationRequest` variant. let request_url = RequestUrl::from_str( "\ siopv2://idtoken?\ @@ -212,7 +212,7 @@ mod tests { .unwrap(); assert_eq!( request_url.clone(), - RequestUrl::Request(Box::new(Request { + RequestUrl::AuthorizationRequest(Box::new(AuthorizationRequest { response_type: ResponseType::IdToken, response_mode: Some("post".to_string()), client_id: "did:example:\ diff --git a/src/request/request_builder.rs b/src/request/request_builder.rs index 0fd49762..c6ac4bc1 100644 --- a/src/request/request_builder.rs +++ b/src/request/request_builder.rs @@ -1,6 +1,6 @@ use crate::{ claims::ClaimRequests, - request::{Request, RequestUrl, ResponseType}, + request::{AuthorizationRequest, RequestUrl, ResponseType}, Registration, Scope, }; use anyhow::{anyhow, Result}; @@ -43,7 +43,7 @@ impl RequestUrlBuilder { let request_uri = self.request_uri.take(); match (request_uri, self.is_empty()) { (Some(request_uri), true) => Ok(RequestUrl::RequestUri { request_uri }), - (None, _) => Ok(RequestUrl::Request(Box::new(Request { + (None, _) => Ok(RequestUrl::AuthorizationRequest(Box::new(AuthorizationRequest { response_type: self .response_type .take() @@ -120,7 +120,7 @@ mod tests { assert_eq!( request_url, - RequestUrl::Request(Box::new(Request { + RequestUrl::AuthorizationRequest(Box::new(AuthorizationRequest { response_type: ResponseType::IdToken, response_mode: None, client_id: "did:example:123".to_string(), diff --git a/src/response.rs b/src/response.rs index c251322a..f9891035 100644 --- a/src/response.rs +++ b/src/response.rs @@ -18,11 +18,11 @@ pub enum Openid4vpParams { }, } -/// Represents an Authorization Response. It can hold an ID Token, a Verifiable Presentation Token, a Presentation +/// Represents an Authorization AuthorizationResponse. It can hold an ID Token, a Verifiable Presentation Token, a Presentation /// Submission, or a combination of them. #[derive(Serialize, Default, Deserialize, Debug, Getters, PartialEq)] #[skip_serializing_none] -pub struct Response { +pub struct AuthorizationResponse { #[serde(skip)] #[getset(get = "pub")] redirect_uri: String, @@ -33,7 +33,7 @@ pub struct Response { state: Option, } -impl Response { +impl AuthorizationResponse { pub fn builder() -> ResponseBuilder { ResponseBuilder::new() } @@ -54,7 +54,7 @@ impl ResponseBuilder { ResponseBuilder::default() } - pub fn build(&mut self) -> Result { + pub fn build(&mut self) -> Result { let redirect_uri = self .redirect_uri .take() @@ -82,7 +82,7 @@ impl ResponseBuilder { )), }?; - Ok(Response { + Ok(AuthorizationResponse { redirect_uri, id_token: self.id_token.take(), openid4vp_response, @@ -104,20 +104,20 @@ mod tests { #[test] fn test_valid_response() { - assert!(Response::builder() + assert!(AuthorizationResponse::builder() .redirect_uri("redirect".to_string()) .id_token("id_token".to_string()) .build() .is_ok()); - assert!(Response::builder() + assert!(AuthorizationResponse::builder() .redirect_uri("redirect".to_string()) .vp_token("vp_token".to_string()) .presentation_submission("presentation_submission".to_string()) .build() .is_ok()); - assert!(Response::builder() + assert!(AuthorizationResponse::builder() .redirect_uri("redirect".to_string()) .id_token("id_token".to_string()) .vp_token("vp_token".to_string()) @@ -129,7 +129,7 @@ mod tests { #[test] fn test_invalid_response() { assert_eq!( - Response::builder() + AuthorizationResponse::builder() .id_token("id_token".to_string()) .build() .unwrap_err() @@ -138,7 +138,7 @@ mod tests { ); assert_eq!( - Response::builder() + AuthorizationResponse::builder() .redirect_uri("redirect".to_string()) .vp_token("vp_token".to_string()) .build() @@ -148,7 +148,7 @@ mod tests { ); assert_eq!( - Response::builder() + AuthorizationResponse::builder() .redirect_uri("redirect".to_string()) .presentation_submission("presentation_submission".to_string()) .build() @@ -158,7 +158,7 @@ mod tests { ); assert_eq!( - Response::builder() + AuthorizationResponse::builder() .redirect_uri("redirect".to_string()) .presentation_submission("presentation_submission".to_string()) .openid4vp_response_jwt("response".to_string()) From 50d4842cc91d494b7543bb616b0836b458bf1d48 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 7 Jun 2023 12:44:46 +0200 Subject: [PATCH 30/30] style: remove whitespace --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 40154660..1ae36462 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ pub mod test_utils; #[macro_export] macro_rules! builder_fn { - ( $name:ident, $ty:ty) => { + ($name:ident, $ty:ty) => { #[allow(clippy::should_implement_trait)] pub fn $name(mut self, value: impl Into<$ty>) -> Self { self.$name.replace(value.into());