Skip to content

Commit bcd4a00

Browse files
authored
feat: builder pattern for id_token and response (#26)
* 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 * Improve struct field serde * fix: remove custom serde * Add claims and scope parameters * Add Storage and RelyingParty test improvement * Update README example * fix: Add standard_claims to test IdToken * Move Storage trait to test_utils * Remove storage.rs * fix: fix dev-dependencies * fix: fex rebase to dev * fix: fix rebase to dev * feat: add Claim trait with associated types * fix: build * fix: remove build.rs and change crate name in doc tests * feat: refactor claims.rs * feat: Add builder for Response and IdToken * fix: silence clippy warning * feat: add missing ID Token claim parameters * fix: remove skeptic crate * feat: allow json arguments for claims() method * fix: replace unwraps * style: add specific request folder * fix: undo unnecassary cloning * style: explicit serde_json usage * test: improve RequestBuilder tests * fix: fix rebase * style: Rename SiopRequest and add comments * style: rename Request and Response * style: remove whitespace
1 parent 198e111 commit bcd4a00

14 files changed

+621
-240
lines changed

README.md

+28-28
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ OpenID for Verifiable Credentials (OID4VC) consists of the following specificati
1616

1717
Currently the Implicit Flow is consists of four major parts:
1818

19-
- 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.
20-
- 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.
21-
- 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.
19+
- 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.
20+
- 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.
21+
- 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.
2222
- 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`.
2323

2424
## Example
@@ -29,12 +29,12 @@ use async_trait::async_trait;
2929
use chrono::{Duration, Utc};
3030
use ed25519_dalek::{Keypair, Signature, Signer};
3131
use lazy_static::lazy_static;
32-
use rand::rngs::OsRng;
33-
use siopv2::{
34-
claims::{Claim, ClaimRequests},
35-
request::ResponseType, StandardClaim,
36-
IdToken, Provider, Registration, RelyingParty, RequestUrl, Scope, SiopRequest, SiopResponse, Subject, Validator,
32+
use openid4vc::{
33+
claims::{ClaimRequests, ClaimValue, IndividualClaimRequest},
34+
request::ResponseType,
35+
Provider, Registration, RelyingParty, RequestUrl, AuthorizationResponse, Scope, AuthorizationRequest, StandardClaims, Subject, Validator,
3736
};
37+
use rand::rngs::OsRng;
3838
use wiremock::{
3939
http::Method,
4040
matchers::{method, path},
@@ -102,7 +102,7 @@ async fn main() {
102102
let relying_party = RelyingParty::new(validator);
103103

104104
// Create a new RequestUrl with response mode `post` for cross-device communication.
105-
let request: SiopRequest = RequestUrl::builder()
105+
let request: AuthorizationRequest = RequestUrl::builder()
106106
.response_type(ResponseType::IdToken)
107107
.client_id("did:mymethod:relyingparty".to_string())
108108
.scope(Scope::openid())
@@ -114,8 +114,8 @@ async fn main() {
114114
.with_id_token_signing_alg_values_supported(vec!["EdDSA".to_string()]),
115115
)
116116
.claims(ClaimRequests {
117-
id_token: Some(StandardClaim {
118-
name: Some(Claim::default()),
117+
id_token: Some(StandardClaims {
118+
name: Some(IndividualClaimRequest::default()),
119119
..Default::default()
120120
}),
121121
..Default::default()
@@ -126,14 +126,14 @@ async fn main() {
126126
.and_then(TryInto::try_into)
127127
.unwrap();
128128

129-
// Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `SiopRequest`.
129+
// Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `AuthorizationRequest`.
130130
Mock::given(method("GET"))
131131
.and(path("/request_uri"))
132132
.respond_with(ResponseTemplate::new(200).set_body_string(relying_party.encode(&request).await.unwrap()))
133133
.mount(&mock_server)
134134
.await;
135135

136-
// Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `SiopResponse`.
136+
// Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `AuthorizationResponse`.
137137
Mock::given(method("POST"))
138138
.and(path("/redirect_uri"))
139139
.respond_with(ResponseTemplate::new(200))
@@ -165,35 +165,35 @@ async fn main() {
165165
// Let the provider generate a response based on the validated request. The response is an `IdToken` which is
166166
// encoded as a JWT.
167167
let response = provider
168-
.generate_response(request, StandardClaim::default())
168+
.generate_response(
169+
request,
170+
StandardClaims {
171+
name: Some(ClaimValue("Jane Doe".to_string())),
172+
..Default::default()
173+
},
174+
)
169175
.await
170176
.unwrap();
171177

172178
// The provider sends it's response to the mock server's `redirect_uri` endpoint.
173179
provider.send_response(response).await.unwrap();
174180

175-
// Assert that the SiopResponse was successfully received by the mock server at the expected endpoint.
181+
// Assert that the AuthorizationResponse was successfully received by the mock server at the expected endpoint.
176182
let post_request = mock_server.received_requests().await.unwrap()[1].clone();
177183
assert_eq!(post_request.method, Method::Post);
178184
assert_eq!(post_request.url.path(), "/redirect_uri");
179-
let response: SiopResponse = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap();
185+
let response: AuthorizationResponse = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap();
180186

181187
// The `RelyingParty` then validates the response by decoding the header of the id_token, by fetching the public
182188
// key corresponding to the key identifier and finally decoding the id_token using the public key and by
183189
// validating the signature.
184190
let id_token = relying_party.validate_response(&response).await.unwrap();
185-
let IdToken {
186-
iss, sub, aud, nonce, ..
187-
} = IdToken::new(
188-
"did:mymethod:subject".to_string(),
189-
"did:mymethod:subject".to_string(),
190-
"did:mymethod:relyingparty".to_string(),
191-
"n-0S6_WzA2Mj".to_string(),
192-
(Utc::now() + Duration::minutes(10)).timestamp(),
191+
assert_eq!(
192+
id_token.standard_claims(),
193+
&StandardClaims {
194+
name: Some(ClaimValue("Jane Doe".to_string())),
195+
..Default::default()
196+
}
193197
);
194-
assert_eq!(id_token.iss, iss);
195-
assert_eq!(id_token.sub, sub);
196-
assert_eq!(id_token.aud, aud);
197-
assert_eq!(id_token.nonce, nonce);
198198
}
199199
```

src/claims.rs

+22-16
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
1-
use crate::scope::{Scope, ScopeValue};
1+
use crate::{
2+
parse_other,
3+
scope::{Scope, ScopeValue},
4+
};
25
use serde::{Deserialize, Deserializer, Serialize};
36
use serde_with::skip_serializing_none;
47

5-
/// Functions as the `claims` parameter inside a [`crate::SiopRequest`].
8+
/// Functions as the `claims` parameter inside a [`crate::AuthorizationRequest`].
69
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
710
pub struct ClaimRequests {
811
pub user_claims: Option<StandardClaimsRequests>,
912
pub id_token: Option<StandardClaimsRequests>,
1013
}
1114

15+
impl TryFrom<serde_json::Value> for ClaimRequests {
16+
type Error = anyhow::Error;
17+
18+
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
19+
serde_json::from_value(value).map_err(Into::into)
20+
}
21+
}
22+
23+
impl TryFrom<&str> for ClaimRequests {
24+
type Error = anyhow::Error;
25+
26+
fn try_from(value: &str) -> Result<Self, Self::Error> {
27+
serde_json::from_str(value).map_err(Into::into)
28+
}
29+
}
30+
1231
mod sealed {
1332
/// [`Claim`] trait that is implemented by both [`ClaimValue`] and [`ClaimRequest`].
1433
pub trait Claim {
@@ -82,19 +101,6 @@ impl<T> IndividualClaimRequest<T> {
82101
object_member!(other, serde_json::Map<String, serde_json::Value>);
83102
}
84103

85-
// When a struct has fields of type `Option<serde_json::Map<String, serde_json::Value>>`, by default these fields are deserialized as
86-
// `Some(Object {})` instead of None when the corresponding values are missing.
87-
// The `parse_other()` helper function ensures that these fields are deserialized as `None` when no value is present.
88-
fn parse_other<'de, D>(deserializer: D) -> Result<Option<serde_json::Map<String, serde_json::Value>>, D::Error>
89-
where
90-
D: Deserializer<'de>,
91-
{
92-
serde_json::Value::deserialize(deserializer).map(|value| match value {
93-
serde_json::Value::Object(object) if !object.is_empty() => Some(object),
94-
_ => None,
95-
})
96-
}
97-
98104
/// 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).
99105
/// Individual claims can be requested by simply some key with a `null` value, or by using the `essential`, `value`,
100106
/// and `values` fields. Additional information about the requested claim MAY be added to the claim request. This
@@ -128,7 +134,7 @@ pub type StandardClaimsValues = StandardClaims<ClaimValue<()>>;
128134
/// This struct represents the standard claims as defined in the
129135
/// [OpenID Connect Core 1.0 Specification](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims)
130136
/// specification. It can be used either for requesting claims using [`IndividualClaimRequest`]'s in the `claims`
131-
/// parameter of a [`crate::SiopRequest`], or for returning actual [`ClaimValue`]'s in an [`crate::IdToken`].
137+
/// parameter of a [`crate::AuthorizationRequest`], or for returning actual [`ClaimValue`]'s in an [`crate::IdToken`].
132138
#[skip_serializing_none]
133139
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
134140
#[serde(default, deny_unknown_fields)]

src/id_token.rs

-46
This file was deleted.

src/key_method.rs

+2-14
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,7 @@ async fn resolve_public_key(kid: &str) -> Result<Vec<u8>> {
9191
#[cfg(test)]
9292
mod tests {
9393
use super::*;
94-
use crate::{IdToken, Provider, RelyingParty};
95-
use chrono::{Duration, Utc};
94+
use crate::{Provider, RelyingParty};
9695

9796
#[tokio::test]
9897
async fn test_key_subject() {
@@ -124,17 +123,6 @@ mod tests {
124123

125124
// Let the relying party validate the response.
126125
let relying_party = RelyingParty::new(KeySubject::new());
127-
let id_token = relying_party.validate_response(&response).await.unwrap();
128-
129-
let IdToken { aud, nonce, .. } = IdToken::new(
130-
"".to_string(),
131-
"".to_string(),
132-
"did:key:z6MkiTcXZ1JxooACo99YcfkugH6Kifzj7ZupSDCmLEABpjpF".to_string(),
133-
"n-0S6_WzA2Mj".to_string(),
134-
(Utc::now() + Duration::minutes(10)).timestamp(),
135-
);
136-
assert_eq!(id_token.iss, id_token.sub);
137-
assert_eq!(id_token.aud, aud);
138-
assert_eq!(id_token.nonce, nonce);
126+
assert!(relying_party.validate_response(&response).await.is_ok());
139127
}
140128
}

src/lib.rs

+38-7
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,60 @@
11
pub mod claims;
2-
pub mod id_token;
32
pub mod jwt;
43
pub mod key_method;
54
pub mod provider;
65
pub mod registration;
76
pub mod relying_party;
87
pub mod request;
9-
pub mod request_builder;
108
pub mod response;
119
pub mod scope;
1210
pub mod subject;
11+
pub mod token;
1312
pub mod validator;
1413

15-
pub use claims::{StandardClaimsRequests, StandardClaimsValues};
16-
pub use id_token::IdToken;
14+
pub use claims::{ClaimRequests, StandardClaimsRequests, StandardClaimsValues};
1715
pub use jwt::JsonWebToken;
1816
pub use provider::Provider;
1917
pub use registration::Registration;
2018
pub use relying_party::RelyingParty;
21-
pub use request::{RequestUrl, SiopRequest};
22-
pub use request_builder::RequestUrlBuilder;
23-
pub use response::SiopResponse;
19+
pub use request::{request_builder::RequestUrlBuilder, AuthorizationRequest, RequestUrl};
20+
pub use response::AuthorizationResponse;
2421
pub use scope::Scope;
2522
pub use subject::Subject;
23+
pub use token::{id_token::IdToken, id_token_builder::IdTokenBuilder};
2624
pub use validator::Validator;
2725

26+
use serde::{Deserialize, Deserializer};
27+
2828
#[cfg(test)]
2929
pub mod test_utils;
30+
31+
#[macro_export]
32+
macro_rules! builder_fn {
33+
($name:ident, $ty:ty) => {
34+
#[allow(clippy::should_implement_trait)]
35+
pub fn $name(mut self, value: impl Into<$ty>) -> Self {
36+
self.$name.replace(value.into());
37+
self
38+
}
39+
};
40+
($field:ident, $name:ident, $ty:ty) => {
41+
#[allow(clippy::should_implement_trait)]
42+
pub fn $name(mut self, value: impl Into<$ty>) -> Self {
43+
self.$field.$name.replace(value.into());
44+
self
45+
}
46+
};
47+
}
48+
49+
// When a struct has fields of type `Option<serde_json::Map<String, serde_json::Value>>`, by default these fields are deserialized as
50+
// `Some(Object {})` instead of None when the corresponding values are missing.
51+
// The `parse_other()` helper function ensures that these fields are deserialized as `None` when no value is present.
52+
pub fn parse_other<'de, D>(deserializer: D) -> Result<Option<serde_json::Map<String, serde_json::Value>>, D::Error>
53+
where
54+
D: Deserializer<'de>,
55+
{
56+
serde_json::Value::deserialize(deserializer).map(|value| match value {
57+
serde_json::Value::Object(object) if !object.is_empty() => Some(object),
58+
_ => None,
59+
})
60+
}

0 commit comments

Comments
 (0)