Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: builder pattern for id_token and response #26

Merged
merged 30 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
586c6da
Add support for Request by reference
nanderstabel Apr 14, 2023
0bd23b7
Improve struct field serde
nanderstabel Apr 17, 2023
6c67a90
fix: remove custom serde
nanderstabel Apr 19, 2023
eb5990d
Add claims and scope parameters
nanderstabel Apr 19, 2023
c4e78b6
Add Storage and RelyingParty test improvement
nanderstabel Apr 19, 2023
cbf35ac
Update README example
nanderstabel Apr 19, 2023
db8e0b6
fix: Add standard_claims to test IdToken
nanderstabel Apr 19, 2023
ba19bf9
Move Storage trait to test_utils
nanderstabel Apr 19, 2023
7c424d9
Remove storage.rs
nanderstabel Apr 19, 2023
4f28f97
fix: fix dev-dependencies
nanderstabel Apr 24, 2023
47f728c
fix: fex rebase to dev
nanderstabel Apr 25, 2023
41a2339
fix: fix rebase to dev
nanderstabel Apr 25, 2023
ce6a463
feat: add Claim trait with associated types
nanderstabel May 12, 2023
5cabc48
fix: build
nanderstabel May 23, 2023
40d0d06
fix: remove build.rs and change crate name in doc tests
nanderstabel May 24, 2023
bbcf6e7
feat: refactor claims.rs
nanderstabel May 30, 2023
1db5af7
feat: Add builder for Response and IdToken
nanderstabel May 24, 2023
970cf9b
fix: silence clippy warning
nanderstabel May 30, 2023
23f9e48
feat: add missing ID Token claim parameters
nanderstabel May 30, 2023
6fe43e8
fix: remove skeptic crate
nanderstabel Apr 25, 2023
d5a2542
feat: allow json arguments for claims() method
nanderstabel May 12, 2023
7a96a0f
fix: replace unwraps
nanderstabel May 21, 2023
3a7be43
style: add specific request folder
nanderstabel May 26, 2023
3187c47
fix: undo unnecassary cloning
nanderstabel May 31, 2023
015c970
style: explicit serde_json usage
nanderstabel May 31, 2023
7534575
test: improve RequestBuilder tests
nanderstabel May 31, 2023
79053a2
fix: fix rebase
nanderstabel Jun 5, 2023
7907fac
style: Rename SiopRequest and add comments
nanderstabel Jun 5, 2023
c0d8b8c
style: rename Request and Response
nanderstabel Jun 5, 2023
50d4842
style: remove whitespace
nanderstabel Jun 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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