Skip to content

Commit

Permalink
feat: builder pattern for id_token and response (#26)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nanderstabel authored Jun 7, 2023
1 parent 198e111 commit bcd4a00
Show file tree
Hide file tree
Showing 14 changed files with 621 additions and 240 deletions.
56 changes: 28 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `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
Expand All @@ -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, AuthorizationResponse, Scope, AuthorizationRequest, StandardClaims, Subject, Validator,
};
use rand::rngs::OsRng;
use wiremock::{
http::Method,
matchers::{method, path},
Expand Down Expand Up @@ -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: AuthorizationRequest = RequestUrl::builder()
.response_type(ResponseType::IdToken)
.client_id("did:mymethod:relyingparty".to_string())
.scope(Scope::openid())
Expand All @@ -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()
Expand All @@ -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 `SiopRequest`.
// 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 `SiopResponse`.
// 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))
Expand Down Expand Up @@ -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 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: SiopResponse = 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
// 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);
}
```
38 changes: 22 additions & 16 deletions src/claims.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
use crate::scope::{Scope, ScopeValue};
use crate::{
parse_other,
scope::{Scope, ScopeValue},
};
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::AuthorizationRequest`].
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClaimRequests {
pub user_claims: Option<StandardClaimsRequests>,
pub id_token: Option<StandardClaimsRequests>,
}

impl TryFrom<serde_json::Value> for ClaimRequests {
type Error = anyhow::Error;

fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
serde_json::from_value(value).map_err(Into::into)
}
}

impl TryFrom<&str> for ClaimRequests {
type Error = anyhow::Error;

fn try_from(value: &str) -> Result<Self, Self::Error> {
serde_json::from_str(value).map_err(Into::into)
}
}

mod sealed {
/// [`Claim`] trait that is implemented by both [`ClaimValue`] and [`ClaimRequest`].
pub trait Claim {
Expand Down Expand Up @@ -82,19 +101,6 @@ impl<T> IndividualClaimRequest<T> {
object_member!(other, serde_json::Map<String, serde_json::Value>);
}

// When a struct has fields of type `Option<serde_json::Map<String, serde_json::Value>>`, 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<Option<serde_json::Map<String, serde_json::Value>>, 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
Expand Down Expand Up @@ -128,7 +134,7 @@ pub type StandardClaimsValues = StandardClaims<ClaimValue<()>>;
/// 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::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)]
Expand Down
46 changes: 0 additions & 46 deletions src/id_token.rs

This file was deleted.

16 changes: 2 additions & 14 deletions src/key_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ async fn resolve_public_key(kid: &str) -> Result<Vec<u8>> {
#[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() {
Expand Down Expand Up @@ -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());
}
}
45 changes: 38 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
pub mod claims;
pub mod id_token;
pub mod jwt;
pub mod key_method;
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;
pub mod token;
pub mod validator;

pub use claims::{StandardClaimsRequests, StandardClaimsValues};
pub use id_token::IdToken;
pub use claims::{ClaimRequests, StandardClaimsRequests, StandardClaimsValues};
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 response::SiopResponse;
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};
pub use validator::Validator;

use serde::{Deserialize, Deserializer};

#[cfg(test)]
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: 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: impl Into<$ty>) -> Self {
self.$field.$name.replace(value.into());
self
}
};
}

// When a struct has fields of type `Option<serde_json::Map<String, serde_json::Value>>`, 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<Option<serde_json::Map<String, serde_json::Value>>, 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,
})
}
Loading

0 comments on commit bcd4a00

Please sign in to comment.