diff --git a/Cargo.toml b/Cargo.toml index 4f801b1..cadad42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ async-signature = "0.3.0" rand = "0.8.5" time = { version = "0.3.29", features = ["serde"] } thiserror = "1.0.49" +tracing = "0.1" base64 = "0.21.4" serde_urlencoded = "0.7.1" diff --git a/src/authorization.rs b/src/authorization.rs index b8a7749..a03cd76 100644 --- a/src/authorization.rs +++ b/src/authorization.rs @@ -3,11 +3,11 @@ use openidconnect::{CsrfToken, IssuerUrl}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::profiles::AuthorizationDetaislProfile; +use crate::profiles::AuthorizationDetailsProfile; pub struct AuthorizationRequest<'a, AD> where - AD: AuthorizationDetaislProfile, + AD: AuthorizationDetailsProfile, { inner: oauth2::AuthorizationRequest<'a>, // TODO authorization_details: Vec>, @@ -20,7 +20,7 @@ where impl<'a, AD> AuthorizationRequest<'a, AD> where - AD: AuthorizationDetaislProfile, + AD: AuthorizationDetailsProfile, { pub(crate) fn new( inner: oauth2::AuthorizationRequest<'a>, @@ -74,10 +74,10 @@ where #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct AuthorizationDetail where - AD: AuthorizationDetaislProfile, + AD: AuthorizationDetailsProfile, { r#type: AuthorizationDetailType, - #[serde(flatten, bound = "AD: AuthorizationDetaislProfile")] + #[serde(flatten, bound = "AD: AuthorizationDetailsProfile")] addition_profile_fields: AD, #[serde(skip_serializing_if = "Option::is_none")] locations: Option>, @@ -97,7 +97,7 @@ mod test { use serde_json::json; use crate::{ - core::profiles::{w3c, CoreProfilesAuthorizationDetails}, + core::profiles::{w3c, CoreProfilesAuthorizationDetails, ValueAuthorizationDetails}, metadata::CredentialUrl, }; @@ -121,6 +121,34 @@ mod test { .unwrap(); } + #[test] + fn example_authorization_details_credential_configuration_id() { + let _: Vec> = + serde_json::from_value(json!([ + { + "type": "openid_credential", + "credential_configuration_id": "UniversityDegreeCredential" + } + ])) + .unwrap(); + } + + #[test] + fn example_authorization_details_credential_configuration_id_deny() { + assert!( + serde_json::from_value::>>( + json!([ + { + "type": "openid_credential", + "format": "jwt_vc_json", + "credential_configuration_id": "UniversityDegreeCredential" + } + ]) + ) + .is_err() + ); + } + #[test] fn example_authorization_details_locations() { let _: Vec> = @@ -190,11 +218,13 @@ mod test { let state = CsrfToken::new("state".into()); let authorization_details = vec![AuthorizationDetail { r#type: AuthorizationDetailType::OpenidCredential, - addition_profile_fields: CoreProfilesAuthorizationDetails::JWTVC( - w3c::jwt::AuthorizationDetails::new(w3c::CredentialDefinition::new(vec![ - "VerifiableCredential".into(), - "UniversityDegreeCredential".into(), - ])), + addition_profile_fields: CoreProfilesAuthorizationDetails::Value( + ValueAuthorizationDetails::JWTVC(w3c::jwt::AuthorizationDetails::new( + w3c::CredentialDefinition::new(vec![ + "VerifiableCredential".into(), + "UniversityDegreeCredential".into(), + ]), + )), ), locations: None, }]; diff --git a/src/client.rs b/src/client.rs index a4052c4..7077c7e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,11 +20,12 @@ use serde::{Deserialize, Serialize}; use crate::{ authorization::AuthorizationRequest, credential, + credential_response_encryption::CredentialResponseEncryptionMetadata, metadata::{ - AuthorizationMetadata, CredentialMetadata, CredentialUrl, IssuerMetadata, - IssuerMetadataDisplay, + AuthorizationMetadata, CredentialIssuerMetadata, CredentialIssuerMetadataDisplay, + CredentialMetadata, CredentialUrl, }, - profiles::{AuthorizationDetaislProfile, Profile}, + profiles::{AuthorizationDetailsProfile, Profile}, pushed_authorization::PushedAuthorizationRequest, token, types::{BatchCredentialUrl, DeferredCredentialUrl, ParUrl}, @@ -41,7 +42,7 @@ where C: Profile, JT: JsonWebKeyType, JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm, + JA: JweKeyManagementAlgorithm + Clone, { inner: oauth2::Client< BasicErrorResponse, @@ -56,11 +57,9 @@ where par_auth_url: Option, batch_credential_endpoint: Option, deferred_credential_endpoint: Option, - credential_response_encryption_alg_values_supported: Option>, - credential_response_encryption_enc_values_supported: Option>, - require_credential_response_encryption: Option, - credentials_supported: Vec>, - display: Option, + credential_response_encryption: Option>, + credential_configurations_supported: Vec>, + display: Option>, _phantom_jt: PhantomData, } @@ -89,10 +88,8 @@ where par_auth_url, batch_credential_endpoint: None, deferred_credential_endpoint: None, - credential_response_encryption_alg_values_supported: None, - credential_response_encryption_enc_values_supported: None, - require_credential_response_encryption: None, - credentials_supported: vec![], + credential_response_encryption: None, + credential_configurations_supported: vec![], display: None, _phantom_jt: PhantomData, } @@ -104,24 +101,22 @@ where set_credential_endpoint -> credential_endpoint[CredentialUrl], set_batch_credential_endpoint -> batch_credential_endpoint[Option], set_deferred_credential_endpoint -> deferred_credential_endpoint[Option], - set_credential_response_encryption_alg_values_supported -> credential_response_encryption_alg_values_supported[Option>], - set_credential_response_encryption_enc_values_supported -> credential_response_encryption_enc_values_supported[Option>], - set_require_credential_response_encryption -> require_credential_response_encryption[Option], - set_credentials_supported -> credentials_supported[Vec>], - set_display -> display[Option], + set_credential_response_encryption -> credential_response_encryption[Option>], + set_credential_configurations_supported -> credential_configurations_supported[Vec>], + set_display -> display[Option>], } ]; pub fn from_issuer_metadata( - issuer_metadata: IssuerMetadata, + credential_issuer_metadata: CredentialIssuerMetadata, authorization_metadata: AuthorizationMetadata, client_id: ClientId, redirect_uri: RedirectUrl, ) -> Self { Self::new( client_id, - issuer_metadata.credential_issuer().clone(), - issuer_metadata.credential_endpoint().clone(), + credential_issuer_metadata.credential_issuer().clone(), + credential_issuer_metadata.credential_endpoint().clone(), authorization_metadata.authorization_endpoint().clone(), authorization_metadata .pushed_authorization_endpoint() @@ -129,23 +124,27 @@ where authorization_metadata.token_endpoint().clone(), redirect_uri, ) - .set_batch_credential_endpoint(issuer_metadata.batch_credential_endpoint().cloned()) - .set_deferred_credential_endpoint(issuer_metadata.deferred_credential_endpoint().cloned()) - .set_credential_response_encryption_alg_values_supported( - issuer_metadata - .credential_response_encryption_alg_values_supported() + .set_batch_credential_endpoint( + credential_issuer_metadata + .batch_credential_endpoint() + .cloned(), + ) + .set_deferred_credential_endpoint( + credential_issuer_metadata + .deferred_credential_endpoint() .cloned(), ) - .set_credential_response_encryption_enc_values_supported( - issuer_metadata - .credential_response_encryption_enc_values_supported() + .set_credential_response_encryption( + credential_issuer_metadata + .credential_response_encryption() .cloned(), ) - .set_require_credential_response_encryption( - issuer_metadata.require_credential_response_encryption(), + .set_display(credential_issuer_metadata.display().cloned()) + .set_credential_configurations_supported( + credential_issuer_metadata + .credential_configurations_supported() + .clone(), ) - .set_credentials_supported(issuer_metadata.credentials_supported().clone()) - .set_display(issuer_metadata.display().cloned()) } pub fn pushed_authorization_request( @@ -154,7 +153,7 @@ where ) -> Result, Error> where S: FnOnce() -> CsrfToken, - AD: AuthorizationDetaislProfile, + AD: AuthorizationDetailsProfile, { if self.par_auth_url.is_none() { return Err(Error::ParUnsupported()); @@ -174,7 +173,7 @@ where pub fn authorize_url(&self, state_fn: S) -> AuthorizationRequest where S: FnOnce() -> CsrfToken, - AD: AuthorizationDetaislProfile, + AD: AuthorizationDetailsProfile, { let inner = self.inner.authorize_url(state_fn); AuthorizationRequest::new(inner, vec![], None, None, None) diff --git a/src/core/mod.rs b/src/core/mod.rs index 0074ac0..a482bfe 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -9,7 +9,7 @@ pub mod metadata { use super::profiles::CoreProfilesMetadata; - pub type IssuerMetadata = metadata::IssuerMetadata< + pub type CredentialIssuerMetadata = metadata::CredentialIssuerMetadata< CoreProfilesMetadata, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm, diff --git a/src/core/profiles/isomdl.rs b/src/core/profiles/isomdl.rs index c5a7445..1e8903d 100644 --- a/src/core/profiles/isomdl.rs +++ b/src/core/profiles/isomdl.rs @@ -4,7 +4,7 @@ use isomdl::definitions::device_request::DocType; use serde::{Deserialize, Serialize}; use super::{ - w3c::CredentialSubjectClaims, AuthorizationDetaislProfile, CredentialMetadataProfile, + w3c::CredentialSubjectClaims, AuthorizationDetailsProfile, CredentialMetadataProfile, CredentialOfferProfile, CredentialRequestProfile, CredentialResponseProfile, }; @@ -12,7 +12,7 @@ pub type Namespace = String; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Metadata { - // cryptographic_suites_supported: Option>, // TODO cose + // credential_signing_alg_values_supported: Option>, // TODO cose doctype: DocType, claims: Option>, order: Option>, @@ -63,6 +63,14 @@ impl CredentialOfferProfile for Offer {} pub struct AuthorizationDetails { doctype: DocType, claims: Option>, + + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_configuration_id" + )] + _credential_configuration_id: (), } impl AuthorizationDetails { @@ -70,7 +78,11 @@ impl AuthorizationDetails { doctype: DocType, claims: Option>, ) -> Self { - Self { doctype, claims } + Self { + doctype, + claims, + _credential_configuration_id: (), + } } field_getters_setters![ pub self [self] ["ISO mDL authorization details value"] { @@ -79,12 +91,20 @@ impl AuthorizationDetails { } ]; } -impl AuthorizationDetaislProfile for AuthorizationDetails {} +impl AuthorizationDetailsProfile for AuthorizationDetails {} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Request { doctype: DocType, claims: Option>, + + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_identifier" + )] + _credential_identifier: (), } impl Request { @@ -92,6 +112,7 @@ impl Request { Self { doctype, claims: None, + _credential_identifier: (), } } field_getters_setters![ diff --git a/src/core/profiles/mod.rs b/src/core/profiles/mod.rs index 61127a9..9ee8298 100644 --- a/src/core/profiles/mod.rs +++ b/src/core/profiles/mod.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use serde::{Deserialize, Serialize}; use crate::profiles::{ - AuthorizationDetaislProfile, CredentialMetadataProfile, CredentialOfferProfile, + AuthorizationDetailsProfile, CredentialMetadataProfile, CredentialOfferProfile, CredentialRequestProfile, CredentialResponseProfile, Profile, }; @@ -35,10 +35,18 @@ impl CredentialMetadataProfile for CoreProfilesMetadata { fn to_request(&self) -> Self::Request { match self { - CoreProfilesMetadata::JWTVC(m) => Self::Request::JWTVC(m.to_request()), - CoreProfilesMetadata::JWTLDVC(m) => Self::Request::JWTLDVC(m.to_request()), - CoreProfilesMetadata::LDVC(m) => Self::Request::LDVC(m.to_request()), - CoreProfilesMetadata::ISOmDL(m) => Self::Request::ISOmDL(m.to_request()), + CoreProfilesMetadata::JWTVC(m) => { + Self::Request::Value(ValueRequest::JWTVC(m.to_request())) + } + CoreProfilesMetadata::JWTLDVC(m) => { + Self::Request::Value(ValueRequest::JWTLDVC(m.to_request())) + } + CoreProfilesMetadata::LDVC(m) => { + Self::Request::Value(ValueRequest::LDVC(m.to_request())) + } + CoreProfilesMetadata::ISOmDL(m) => { + Self::Request::Value(ValueRequest::ISOmDL(m.to_request())) + } } } } @@ -57,9 +65,37 @@ pub enum CoreProfilesOffer { } impl CredentialOfferProfile for CoreProfilesOffer {} +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ReferencedAuthorizationDetails { + credential_configuration_id: String, + + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "format" + )] + _format: (), +} + +impl ReferencedAuthorizationDetails { + pub fn new(credential_configuration_id: String) -> Self { + Self { + credential_configuration_id, + _format: (), + } + } + + field_getters_setters![ + pub self [self] ["Authorization Details definition value"] { + set_credential_configuration_id -> credential_configuration_id[String], + } + ]; +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(tag = "format")] -pub enum CoreProfilesAuthorizationDetails { +pub enum ValueAuthorizationDetails { #[serde(rename = "jwt_vc_json")] JWTVC(w3c::jwt::AuthorizationDetails), #[serde(rename = "jwt_vc_json-ld")] @@ -69,11 +105,46 @@ pub enum CoreProfilesAuthorizationDetails { #[serde(rename = "mso_mdoc")] ISOmDL(isomdl::AuthorizationDetails), } -impl AuthorizationDetaislProfile for CoreProfilesAuthorizationDetails {} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum CoreProfilesAuthorizationDetails { + Value(ValueAuthorizationDetails), + Referenced(ReferencedAuthorizationDetails), +} +impl AuthorizationDetailsProfile for CoreProfilesAuthorizationDetails {} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ReferencedRequest { + credential_identifier: String, + + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "format" + )] + _format: (), +} + +impl ReferencedRequest { + pub fn new(credential_identifier: String) -> Self { + Self { + credential_identifier, + _format: (), + } + } + + field_getters_setters![ + pub self [self] ["Authorization Details definition value"] { + set_credential_identifier -> credential_identifier[String], + } + ]; +} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(tag = "format")] -pub enum CoreProfilesRequest { +pub enum ValueRequest { #[serde(rename = "jwt_vc_json")] JWTVC(w3c::jwt::Request), #[serde(rename = "jwt_vc_json-ld")] @@ -83,6 +154,14 @@ pub enum CoreProfilesRequest { #[serde(rename = "mso_mdoc")] ISOmDL(isomdl::Request), } + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum CoreProfilesRequest { + Value(ValueRequest), + Referenced(ReferencedRequest), +} + impl CredentialRequestProfile for CoreProfilesRequest { type Response = CoreProfilesResponse; } diff --git a/src/core/profiles/w3c/jwt.rs b/src/core/profiles/w3c/jwt.rs index 6ae21bd..a13f2aa 100644 --- a/src/core/profiles/w3c/jwt.rs +++ b/src/core/profiles/w3c/jwt.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use ssi_claims::CompactJWSString; use crate::profiles::{ - AuthorizationDetaislProfile, CredentialMetadataProfile, CredentialOfferProfile, + AuthorizationDetailsProfile, CredentialMetadataProfile, CredentialOfferProfile, CredentialRequestProfile, CredentialResponseProfile, }; @@ -10,7 +10,7 @@ use super::{CredentialDefinition, CredentialOfferDefinition}; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Metadata { - cryptographic_suites_supported: Option>, + credential_signing_alg_values_supported: Option>, credential_definition: CredentialDefinition, order: Option>, } @@ -18,14 +18,14 @@ pub struct Metadata { impl Metadata { pub fn new(credential_definition: CredentialDefinition) -> Self { Self { - cryptographic_suites_supported: None, + credential_signing_alg_values_supported: None, credential_definition, order: None, } } field_getters_setters![ pub self [self] ["JWT VC metadata value"] { - set_cryptographic_suites_supported -> cryptographic_suites_supported[Option>], + set_credential_signing_alg_values_supported -> credential_signing_alg_values_supported[Option>], set_credential_definition -> credential_definition[CredentialDefinition], set_order -> order[Option>], } @@ -61,12 +61,21 @@ impl CredentialOfferProfile for Offer {} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct AuthorizationDetails { credential_definition: CredentialDefinition, + + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_identifier" + )] + _credential_identifier: (), } impl AuthorizationDetails { pub fn new(credential_definition: CredentialDefinition) -> Self { Self { credential_definition, + _credential_identifier: (), } } field_getters_setters![ @@ -75,17 +84,26 @@ impl AuthorizationDetails { } ]; } -impl AuthorizationDetaislProfile for AuthorizationDetails {} +impl AuthorizationDetailsProfile for AuthorizationDetails {} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Request { credential_definition: CredentialDefinition, + + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_identifier" + )] + _credential_identifier: (), } impl Request { pub fn new(credential_definition: CredentialDefinition) -> Self { Self { credential_definition, + _credential_identifier: (), } } field_getters_setters![ diff --git a/src/core/profiles/w3c/jwtld.rs b/src/core/profiles/w3c/jwtld.rs index 651072c..ba5eb66 100644 --- a/src/core/profiles/w3c/jwtld.rs +++ b/src/core/profiles/w3c/jwtld.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::profiles::{ - AuthorizationDetaislProfile, CredentialMetadataProfile, CredentialOfferProfile, + AuthorizationDetailsProfile, CredentialMetadataProfile, CredentialOfferProfile, CredentialRequestProfile, CredentialResponseProfile, }; @@ -20,15 +20,33 @@ pub struct Offer {} impl CredentialOfferProfile for Offer {} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct AuthorizationDetails {} -impl AuthorizationDetaislProfile for AuthorizationDetails {} +pub struct AuthorizationDetails { + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_configuration_id" + )] + _credential_configuration_id: (), +} +impl AuthorizationDetailsProfile for AuthorizationDetails {} #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct Request {} +pub struct Request { + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_identifier" + )] + _credential_identifier: (), +} impl Request { pub fn new() -> Self { - Self {} + Self { + _credential_identifier: (), + } } } impl CredentialRequestProfile for Request { diff --git a/src/core/profiles/w3c/ldp.rs b/src/core/profiles/w3c/ldp.rs index 11a55eb..c518112 100644 --- a/src/core/profiles/w3c/ldp.rs +++ b/src/core/profiles/w3c/ldp.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use ssi_claims::vc::AnyJsonCredential; use crate::profiles::{ - AuthorizationDetaislProfile, CredentialMetadataProfile, CredentialOfferProfile, + AuthorizationDetailsProfile, CredentialMetadataProfile, CredentialOfferProfile, CredentialRequestProfile, CredentialResponseProfile, }; @@ -10,7 +10,7 @@ use super::{CredentialDefinition, CredentialOfferDefinition}; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Metadata { - cryptographic_suites_supported: Option>, + credential_signing_alg_values_supported: Option>, #[serde(rename = "@context")] context: Vec, credentials_definition: CredentialDefinitionLD, @@ -23,7 +23,7 @@ impl Metadata { credentials_definition: CredentialDefinitionLD, ) -> Self { Self { - cryptographic_suites_supported: None, + credential_signing_alg_values_supported: None, context, credentials_definition, order: None, @@ -31,7 +31,7 @@ impl Metadata { } field_getters_setters![ pub self [self] ["LD VC metadata value"] { - set_cryptographic_suites_supported -> cryptographic_suites_supported[Option>], + set_credential_signing_alg_values_supported -> credential_signing_alg_values_supported[Option>], set_context -> context[Vec], set_credentials_definition -> credentials_definition[CredentialDefinitionLD], set_order -> order[Option>], @@ -121,12 +121,21 @@ impl CredentialOfferProfile for Offer {} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct AuthorizationDetails { credential_definition: CredentialDefinitionLD, + + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_configuration_id" + )] + _credential_configuration_id: (), } impl AuthorizationDetails { pub fn new(credential_definition: CredentialDefinitionLD) -> Self { Self { credential_definition, + _credential_configuration_id: (), } } @@ -136,17 +145,26 @@ impl AuthorizationDetails { } ]; } -impl AuthorizationDetaislProfile for AuthorizationDetails {} +impl AuthorizationDetailsProfile for AuthorizationDetails {} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Request { credential_definition: CredentialDefinitionLD, + + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_identifier" + )] + _credential_identifier: (), } impl Request { pub fn new(credential_definition: CredentialDefinitionLD) -> Self { Self { credential_definition, + _credential_identifier: (), } } diff --git a/src/core/profiles/w3c/mod.rs b/src/core/profiles/w3c/mod.rs index e113e68..7c17592 100644 --- a/src/core/profiles/w3c/mod.rs +++ b/src/core/profiles/w3c/mod.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use crate::metadata::IssuerMetadataDisplay; +use crate::metadata::CredentialIssuerMetadataDisplay; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] @@ -53,7 +53,7 @@ impl CredentialOfferDefinition { pub struct CredentialSubjectClaims { mandatory: Option, value_type: Option, - display: Option>, + display: Option>, } impl CredentialSubjectClaims { @@ -69,7 +69,7 @@ impl CredentialSubjectClaims { pub self [self] ["credential subject claims value"] { set_mandatory -> mandatory[Option], set_value_type -> value_type[Option], - set_display -> display[Option>], + set_display -> display[Option>], } ]; } diff --git a/src/credential.rs b/src/credential.rs index 89f31a9..0b6a4f9 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -12,9 +12,9 @@ use openidconnect::{ JweKeyManagementAlgorithm, Nonce, }; use serde::{Deserialize, Serialize}; -use ssi_jwk::JWK; use crate::{ + credential_response_encryption::CredentialResponseEncryption, http_utils::{auth_bearer, content_type_has_essence, MIME_TYPE_JSON}, metadata::CredentialUrl, profiles::{CredentialRequestProfile, CredentialResponseProfile}, @@ -27,16 +27,15 @@ where CR: CredentialRequestProfile, JT: JsonWebKeyType, JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm, + JA: JweKeyManagementAlgorithm + Clone, { #[serde(flatten, bound = "CR: CredentialRequestProfile")] additional_profile_fields: CR, proof: Option, - credential_encryption_jwk: Option, - #[serde(bound = "JA: JweKeyManagementAlgorithm")] - credential_response_encryption_alg: Option, - #[serde(bound = "JE: JweContentEncryptionAlgorithm")] - credential_response_encryption_enc: Option, + #[serde( + bound = "JT: JsonWebKeyType, JA: JweKeyManagementAlgorithm, JE: JweContentEncryptionAlgorithm" + )] + credential_response_encryption: Option>, #[serde(skip)] _phantom_jt: PhantomData, } @@ -46,15 +45,13 @@ where CR: CredentialRequestProfile, JT: JsonWebKeyType, JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm, + JA: JweKeyManagementAlgorithm + Clone, { pub(crate) fn new(additional_profile_fields: CR) -> Self { Self { additional_profile_fields, proof: None, - credential_encryption_jwk: None, - credential_response_encryption_alg: None, - credential_response_encryption_enc: None, + credential_response_encryption: None, _phantom_jt: PhantomData, } } @@ -63,9 +60,7 @@ where pub self [self] ["credential request value"] { set_additional_profile_fields -> additional_profile_fields[CR], set_proof -> proof[Option], - set_credential_encryption_jwk -> credential_encryption_jwk[Option], - set_credential_response_encryption_alg -> credential_response_encryption_alg[Option], - set_credential_response_encryption_enc -> credential_response_encryption_enc[Option], + set_credential_response_encryption -> credential_response_encryption[Option>], } ]; } @@ -75,7 +70,7 @@ where CR: CredentialRequestProfile, JT: JsonWebKeyType, JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm, + JA: JweKeyManagementAlgorithm + Clone, { body: Request, url: CredentialUrl, @@ -87,7 +82,7 @@ where CR: CredentialRequestProfile, JT: JsonWebKeyType, JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm, + JA: JweKeyManagementAlgorithm + Clone, { pub(crate) fn new( body: Request, @@ -105,9 +100,7 @@ where pub self [self.body] ["credential request value"] { set_additional_profile_fields -> additional_profile_fields[CR], set_proof -> proof[Option], - set_credential_encryption_jwk -> credential_encryption_jwk[Option], - set_credential_response_encryption_alg -> credential_response_encryption_alg[Option], - set_credential_response_encryption_enc -> credential_response_encryption_enc[Option], + set_credential_response_encryption -> credential_response_encryption[Option>], } ]; @@ -262,8 +255,8 @@ where #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum ErrorType { - InvalidRequest, InvalidToken, + InvalidCredentialRequest, UnsupportedCredentialType, UnsupportedCredentialFormat, InvalidProof, @@ -278,7 +271,7 @@ where CR: CredentialRequestProfile, JT: JsonWebKeyType, JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm, + JA: JweKeyManagementAlgorithm + Clone, { #[serde(bound = "CR: CredentialRequestProfile")] credential_requests: Vec>, @@ -329,6 +322,39 @@ mod test { .unwrap(); } + #[test] + fn example_credential_request_referenced() { + let _: crate::core::credential::Request = serde_json::from_value(json!({ + "credential_identifier": "UniversityDegreeCredential", + "proof": { + "proof_type": "jwt", + "jwt": "eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8 + xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR + 0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE1MzY5NTk5NTksIm5vbmNlIjoidFppZ25zbk + ZicCJ9.ewdkIkPV50iOeBUqMXCC_aZKPxgihac0aW9EkL1nOzM" + } + })) + .unwrap(); + } + + #[test] + fn example_credential_request_deny() { + assert!( + serde_json::from_value::(json!({ + "format": "jwt_vc_json", + "credential_identifier": "UniversityDegreeCredential", + "proof": { + "proof_type": "jwt", + "jwt": "eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8 + xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR + 0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE1MzY5NTk5NTksIm5vbmNlIjoidFppZ25zbk + ZicCJ9.ewdkIkPV50iOeBUqMXCC_aZKPxgihac0aW9EkL1nOzM" + } + })) + .is_err() + ); + } + #[test] fn example_credential_response_object() { let _: Response = serde_json::from_value(json!({ diff --git a/src/credential_offer.rs b/src/credential_offer.rs index e95e1a7..2eab456 100644 --- a/src/credential_offer.rs +++ b/src/credential_offer.rs @@ -1,3 +1,5 @@ +#![allow(clippy::large_enum_variant)] + use openidconnect::{CsrfToken, IssuerUrl, Scope}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none}; @@ -7,13 +9,9 @@ use crate::profiles::CredentialOfferProfile; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] -pub enum CredentialOffer -where - CO: CredentialOfferProfile, -{ +pub enum CredentialOffer { Value { - #[serde(bound = "CO: CredentialOfferProfile")] - credential_offer: CredentialOfferParameters, + credential_offer: CredentialOfferParameters, }, Reference { credential_offer_uri: Url, @@ -23,13 +21,9 @@ where #[serde_as] #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CredentialOfferParameters -where - CO: CredentialOfferProfile, -{ +pub struct CredentialOfferParameters { credential_issuer: IssuerUrl, - #[serde(bound = "CO: CredentialOfferProfile")] - credentials: Vec>, + credential_configuration_ids: Vec, grants: Option, } @@ -56,34 +50,76 @@ pub struct CredentialOfferGrants { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct AuthorizationCodeGrant { issuer_state: Option, + authorization_server: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct PreAuthorizationCodeGrant { #[serde(rename = "pre-authorized_code")] pre_authorized_code: String, - user_pin_required: Option, + tx_code: Option, interval: Option, + authorization_server: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum InputMode { + #[serde(rename = "numeric")] + Numeric, + #[serde(rename = "text")] + Text, +} + +impl Default for InputMode { + fn default() -> Self { + Self::Numeric + } +} + +#[serde_as] +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TxCode { + input_mode: Option, + length: Option, + description: Option, +} + +impl TxCode { + pub fn new( + input_mode: Option, + length: Option, + description: Option, + ) -> Self { + Self { + input_mode, + length, + description, + } + } + + field_getters_setters![ + pub self [self] ["transaction code value"] { + set_input_mode -> input_mode[Option], + set_length -> length[Option], + set_description -> description[Option], + } + ]; } #[cfg(test)] mod test { use serde_json::json; - use crate::core::profiles::CoreProfilesOffer; - use super::*; #[test] fn example_credential_offer_object() { - let _: CredentialOfferParameters = serde_json::from_value(json!({ + let _: CredentialOfferParameters = serde_json::from_value(json!({ "credential_issuer": "https://credential-issuer.example.com", - "credentials": [ - "UniversityDegree_JWT", - { - "format": "mso_mdoc", - "doctype": "org.iso.18013.5.1.mDL" - } + "credential_configuration_ids": [ + "UniversityDegreeCredential", + "org.iso.18013.5.1.mDL" ], "grants": { "authorization_code": { @@ -91,7 +127,11 @@ mod test { }, "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": "adhjhdjajkdkhjhdj", - "user_pin_required": true + "tx_code": { + "length": 4, + "input_mode": "numeric", + "description": "Please provide the one-time code that was sent via e-mail" + } } } })) diff --git a/src/credential_response_encryption.rs b/src/credential_response_encryption.rs new file mode 100644 index 0000000..da466fc --- /dev/null +++ b/src/credential_response_encryption.rs @@ -0,0 +1,45 @@ +#![allow(clippy::type_complexity)] +use std::marker::PhantomData; + +use openidconnect::{JsonWebKeyType, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, skip_serializing_none}; +use ssi_jwk::JWK; + +pub use crate::types::{BatchCredentialUrl, CredentialUrl, DeferredCredentialUrl, ParUrl}; + +#[serde_as] +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct CredentialResponseEncryptionMetadata +where + JT: JsonWebKeyType, + JE: JweContentEncryptionAlgorithm, + JA: JweKeyManagementAlgorithm + Clone, +{ + #[serde(bound = "JA: JweKeyManagementAlgorithm")] + alg_values_supported: Vec, + #[serde(bound = "JE: JweContentEncryptionAlgorithm")] + enc_values_supported: Vec, + encryption_required: bool, + #[serde(skip)] + _phantom_jt: PhantomData, +} + +#[serde_as] +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct CredentialResponseEncryption +where + JT: JsonWebKeyType, + JE: JweContentEncryptionAlgorithm, + JA: JweKeyManagementAlgorithm + Clone, +{ + jwk: JWK, + #[serde(bound = "JA: JweKeyManagementAlgorithm")] + alg: JA, + #[serde(bound = "JE: JweContentEncryptionAlgorithm")] + enc: JE, + #[serde(skip)] + _phantom_jt: PhantomData, +} diff --git a/src/deny_field.rs b/src/deny_field.rs new file mode 100644 index 0000000..c671ea9 --- /dev/null +++ b/src/deny_field.rs @@ -0,0 +1,16 @@ +use serde::Deserializer; + +/// When using flattened structs with `serde`, it is not possible +/// to also use #[serde(deny_unknown_fields)] in the same struct +/// definition, but it is possible to create a custom deserializer +/// that just errors to be able to deny a specific field instead. +/// In this library, this is used mainly to implement parameter +/// either/or situations, such as when either `format` or +/// `credential_configuration_id` must be present, but not +/// both in `AuthorizationDetails`. +pub(crate) fn deny_field<'de, D>(_deserializer: D) -> Result<(), D::Error> +where + D: Deserializer<'de>, +{ + Err(serde::de::Error::custom("Field must not be present")) +} diff --git a/src/lib.rs b/src/lib.rs index 1f2ea10..e900bdf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,8 +6,11 @@ pub mod client; pub mod core; pub mod credential; pub mod credential_offer; +pub mod credential_response_encryption; +mod deny_field; mod http_utils; pub mod metadata; +pub mod notification; pub mod profiles; pub mod proof_of_possession; pub mod pushed_authorization; diff --git a/src/metadata.rs b/src/metadata.rs index d40fc59..c269c4c 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,5 +1,4 @@ #![allow(clippy::type_complexity)] -use std::{future::Future, marker::PhantomData}; use oauth2::{ http::{header::ACCEPT, HeaderValue, Method, StatusCode}, @@ -17,15 +16,21 @@ use openidconnect::{ ProviderMetadata, ResponseTypes, Scope, }; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, skip_serializing_none}; +use serde_with::{serde_as, skip_serializing_none, KeyValueMap}; +use std::{future::Future, marker::PhantomData}; +use tracing::{event, trace_span, Level}; use crate::{ + credential_response_encryption::CredentialResponseEncryptionMetadata, http_utils::{check_content_type, MIME_TYPE_JSON}, profiles::CredentialMetadataProfile, - proof_of_possession::KeyProofType, + proof_of_possession::KeyProofTypesSupported, + types::ImageUrl, }; -pub use crate::types::{BatchCredentialUrl, CredentialUrl, DeferredCredentialUrl, ParUrl}; +pub use crate::types::{ + BatchCredentialUrl, CredentialUrl, DeferredCredentialUrl, NotificationUrl, ParUrl, +}; const METADATA_URL_SUFFIX: &str = ".well-known/openid-credential-issuer"; const AUTHORIZATION_METADATA_URL_SUFFIX: &str = ".well-known/oauth-authorization-server"; @@ -33,7 +38,7 @@ const AUTHORIZATION_METADATA_URL_SUFFIX: &str = ".well-known/oauth-authorization #[serde_as] #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct IssuerMetadata +pub struct CredentialIssuerMetadata where CM: CredentialMetadataProfile, JT: JsonWebKeyType, @@ -41,23 +46,26 @@ where JA: JweKeyManagementAlgorithm + Clone, { credential_issuer: IssuerUrl, - authorization_server: Option, // Not sure this is the right type + authorization_servers: Option>, // Not sure this is the right type credential_endpoint: CredentialUrl, batch_credential_endpoint: Option, deferred_credential_endpoint: Option, - #[serde(bound = "JA: JweKeyManagementAlgorithm")] - credential_response_encryption_alg_values_supported: Option>, - #[serde(bound = "JE: JweContentEncryptionAlgorithm")] - credential_response_encryption_enc_values_supported: Option>, - require_credential_response_encryption: Option, + notification_endpoint: Option, + #[serde( + bound = "JT: JsonWebKeyType, JA: JweKeyManagementAlgorithm, JE: JweContentEncryptionAlgorithm" + )] + credential_response_encryption: Option>, + credential_identifiers_supported: Option, + signed_metadata: Option, + display: Option>, #[serde(bound = "CM: CredentialMetadataProfile")] - credentials_supported: Vec>, - display: Option, + #[serde_as(as = "KeyValueMap<_>")] + credential_configurations_supported: Vec>, #[serde(skip)] _phantom_jt: PhantomData, } -impl IssuerMetadata +impl CredentialIssuerMetadata where CM: CredentialMetadataProfile, JT: JsonWebKeyType, @@ -67,19 +75,20 @@ where pub fn new( credential_issuer: IssuerUrl, credential_endpoint: CredentialUrl, - credentials_supported: Vec>, + credential_configurations_supported: Vec>, ) -> Self { Self { credential_issuer, - authorization_server: None, + authorization_servers: None, credential_endpoint, batch_credential_endpoint: None, deferred_credential_endpoint: None, - credential_response_encryption_alg_values_supported: None, - credential_response_encryption_enc_values_supported: None, - require_credential_response_encryption: None, - credentials_supported, + notification_endpoint: None, + credential_response_encryption: None, + credential_identifiers_supported: None, + signed_metadata: None, display: None, + credential_configurations_supported, _phantom_jt: PhantomData, } } @@ -87,15 +96,16 @@ where field_getters_setters![ pub self [self] ["issuer metadata value"] { set_credential_issuer -> credential_issuer[IssuerUrl], - set_authorization_server -> authorization_server[Option], + set_authorization_servers -> authorization_servers[Option>], set_credential_endpoint -> credential_endpoint[CredentialUrl], set_batch_credential_endpoint -> batch_credential_endpoint[Option], set_deferred_credential_endpoint -> deferred_credential_endpoint[Option], - set_credential_response_encryption_alg_values_supported -> credential_response_encryption_alg_values_supported[Option>], - set_credential_response_encryption_enc_values_supported -> credential_response_encryption_enc_values_supported[Option>], - set_require_credential_response_encryption -> require_credential_response_encryption[Option], - set_credentials_supported -> credentials_supported[Vec>], - set_display -> display[Option], + set_notification_endpoint -> notification_endpoint[Option], + set_credential_response_encryption -> credential_response_encryption[Option>], + set_credential_identifiers_supported -> credential_identifiers_supported[Option], + set_signed_metadata -> signed_metadata[Option], + set_display -> display[Option>], + set_credential_configurations_supported -> credential_configurations_supported[Vec>], } ]; @@ -187,9 +197,47 @@ where } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct IssuerMetadataDisplay { +pub struct CredentialIssuerMetadataDisplay { name: Option, locale: Option, + logo: Option, +} + +impl CredentialIssuerMetadataDisplay { + pub fn new( + name: Option, + locale: Option, + logo: Option, + ) -> Self { + Self { name, locale, logo } + } + + field_getters_setters![ + pub self [self] ["metadata background image value"] { + set_name -> name[Option], + set_locale -> locale[Option], + set_logo -> logo[Option], + } + ]; +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct MetadataDisplayLogo { + url: LogoUrl, + alt_text: Option, +} + +impl MetadataDisplayLogo { + pub fn new(url: LogoUrl, alt_text: Option) -> Self { + Self { url, alt_text } + } + + field_getters_setters![ + pub self [self] ["metadata display logo value"] { + set_url -> url[LogoUrl], + set_alt_text -> alt_text[Option], + } + ]; } #[serde_as] @@ -199,9 +247,12 @@ pub struct CredentialMetadata where CM: CredentialMetadataProfile, { + #[serde(rename = "$key$")] + name: Option, scope: Option, cryptographic_binding_methods_supported: Option>, - proof_types_supported: Option>, + #[serde_as(as = "Option>")] + proof_types_supported: Option>, display: Option>, #[serde(bound = "CM: CredentialMetadataProfile")] #[serde(flatten)] @@ -225,6 +276,7 @@ where { pub fn new(additional_fields: CM) -> Self { Self { + name: None, scope: None, cryptographic_binding_methods_supported: None, proof_types_supported: None, @@ -237,7 +289,7 @@ where pub self [self] ["credential metadata value"] { set_scope -> scope[Option], set_cryptographic_binding_methods_supported -> cryptographic_binding_methods_supported[Option>], - set_proof_types_suuported -> proof_types_supported[Option>], + set_proof_types_supported -> proof_types_supported[Option>], set_display -> display[Option>], set_additional_fields -> additional_fields[CM], } @@ -263,16 +315,62 @@ pub enum CryptographicBindingMethod { pub struct CredentialMetadataDisplay { name: String, locale: Option, - logo: Option, + logo: Option, description: Option, background_color: Option, + background_image: Option, text_color: Option, } +impl CredentialMetadataDisplay { + pub fn new( + name: String, + locale: Option, + logo: Option, + description: Option, + background_color: Option, + background_image: Option, + text_color: Option, + ) -> Self { + Self { + name, + locale, + logo, + description, + background_color, + background_image, + text_color, + } + } + + field_getters_setters![ + pub self [self] ["credential metadata display value"] { + set_name -> name[String], + set_locale -> locale[Option], + set_logo -> logo[Option], + set_description -> description[Option], + set_background_color -> background_color[Option], + set_background_image -> background_image[Option], + set_text_color -> text_color[Option], + } + ]; +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct CredentialMetadataDisplayLogo { - url: Option, - alt_text: Option, +pub struct MetadataBackgroundImage { + uri: ImageUrl, +} + +impl MetadataBackgroundImage { + pub fn new(uri: ImageUrl) -> Self { + Self { uri } + } + + field_getters_setters![ + pub self [self] ["metadata background image value"] { + set_uri -> uri[ImageUrl], + } + ]; } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] @@ -349,8 +447,40 @@ impl AuthorizationMetadata { .set_token_endpoint(Some(token_endpoint)), ) } - pub fn discover( - issuer_metadata: &IssuerMetadata, + + fn trace_and_continue(err: DiscoveryError) -> DiscoveryError + where + RE: std::error::Error + 'static, + { + event!(Level::ERROR, "{err}"); + err + } + + fn discover_inner(http_client: HC, url: &IssuerUrl) -> Result> + where + HC: Fn(HttpRequest) -> Result, + RE: std::error::Error + 'static, + { + let _span = trace_span!("discover_inner").entered(); + event!(Level::DEBUG, "discovering {:?}", url); + + let issuer_url = url.clone(); + + let discovery_url = issuer_url + .join(AUTHORIZATION_METADATA_URL_SUFFIX) + .map_err(DiscoveryError::UrlParse) + .map_err(Self::trace_and_continue)?; + + http_client(Self::discovery_request(discovery_url)) + .map_err(DiscoveryError::Request) + .map_err(Self::trace_and_continue) + .and_then(|http_response| Self::discovery_response(&issuer_url, http_response)) + .map_err(Self::trace_and_continue) + } + + pub fn discover( + credential_issuer_metadata: &CredentialIssuerMetadata, + grant_type: Option, http_client: HC, ) -> Result> where @@ -361,21 +491,77 @@ impl AuthorizationMetadata { JE: JweContentEncryptionAlgorithm, JA: JweKeyManagementAlgorithm + Clone, { - let issuer_url = issuer_metadata - .authorization_server - .clone() - .unwrap_or(issuer_metadata.credential_issuer.clone()); + let _span = trace_span!("discover").entered(); + + if grant_type.is_none() { + return Self::discover_inner( + &http_client, + &credential_issuer_metadata.credential_issuer, + ); + } + + let tail = vec![credential_issuer_metadata.credential_issuer.clone()]; + let grant_type = grant_type.unwrap(); + let servers = match credential_issuer_metadata.authorization_servers { + Some(ref servers) => [servers.clone(), tail].concat(), + None => tail, + }; + + servers + .iter() + .map(|auth_server| { + let response = Self::discover_inner(&http_client, auth_server)?; + + Ok::, DiscoveryError>(response.0.grant_types_supported().and_then( + |gts| { + if gts.iter().any(|gt| *gt == grant_type) { + Some(response.clone()) + } else { + None + } + }, + )) + }) + .take_while(|s| !matches!(s, Ok(Some(_)))) + .next() + .ok_or(DiscoveryError::Other( + "failed to select an authorization server: no matching grant_type".to_string(), + ))?? + .ok_or(DiscoveryError::Other( + "failed to select an authorization server: no matching grant_type".to_string(), + )) + } + + async fn discover_async_inner( + http_client: &HC, + url: &IssuerUrl, + ) -> Result> + where + F: Future>, + HC: Fn(HttpRequest) -> F + 'static, + RE: std::error::Error + 'static, + { + let _span = trace_span!("discover_async_inner").entered(); + event!(Level::DEBUG, "discovering {:?}", url); + + let issuer_url = url.clone(); + let discovery_url = issuer_url .join(AUTHORIZATION_METADATA_URL_SUFFIX) - .map_err(DiscoveryError::UrlParse)?; + .map_err(DiscoveryError::UrlParse) + .map_err(Self::trace_and_continue)?; http_client(Self::discovery_request(discovery_url)) + .await .map_err(DiscoveryError::Request) + .map_err(Self::trace_and_continue) .and_then(|http_response| Self::discovery_response(&issuer_url, http_response)) + .map_err(Self::trace_and_continue) } - pub async fn discover_async( - issuer_metadata: &IssuerMetadata, + pub async fn discover_async( + credential_issuer_metadata: &CredentialIssuerMetadata, + grant_type: Option, http_client: HC, ) -> Result> where @@ -387,18 +573,43 @@ impl AuthorizationMetadata { JE: JweContentEncryptionAlgorithm, JA: JweKeyManagementAlgorithm + Clone, { - let issuer_url = issuer_metadata - .authorization_server - .clone() - .unwrap_or(issuer_metadata.credential_issuer.clone()); - let discovery_url = issuer_url - .join(AUTHORIZATION_METADATA_URL_SUFFIX) - .map_err(DiscoveryError::UrlParse)?; + let _span = trace_span!("discover_async").entered(); - http_client(Self::discovery_request(discovery_url)) + if grant_type.is_none() { + return Self::discover_async_inner( + &http_client, + &credential_issuer_metadata.credential_issuer, + ) + .await; + } + + let grant_type = grant_type.unwrap(); + let servers = match credential_issuer_metadata.authorization_servers { + Some(ref servers) => servers.clone(), + None => vec![], + }; + + for ref auth_server in servers { + let response = Self::discover_async_inner(&http_client, auth_server).await; + + match response { + Ok(response) if response.0.grant_types_supported().is_some() => { + if response + .0 + .grant_types_supported() + .unwrap() + .iter() + .any(|gt| *gt == grant_type) + { + return Ok(response.clone()); + } + } + _ => continue, + } + } + + Self::discover_async_inner(&http_client, &credential_issuer_metadata.credential_issuer) .await - .map_err(DiscoveryError::Request) - .and_then(|http_response| Self::discovery_response(&issuer_url, http_response)) } fn discovery_request(discovery_url: url::Url) -> HttpRequest { @@ -470,12 +681,112 @@ impl AuthorizationMetadata { #[cfg(test)] mod test { - use serde_json::json; - use crate::core::profiles::CoreProfilesMetadata; + use serde_json::json; use super::*; + #[test] + fn example_credential_issuer_metadata() { + let _: CredentialIssuerMetadata< + CoreProfilesMetadata, + CoreJsonWebKeyType, + CoreJweContentEncryptionAlgorithm, + CoreJweKeyManagementAlgorithm, + > = serde_json::from_value(json!({ + "credential_issuer": "https://credential-issuer.example.com", + "authorization_servers": [ "https://server.example.com" ], + "credential_endpoint": "https://credential-issuer.example.com", + "batch_credential_endpoint": "https://credential-issuer.example.com/batch_credential", + "deferred_credential_endpoint": "https://credential-issuer.example.com/deferred_credential", + "credential_response_encryption": { + "alg_values_supported" : [ + "ECDH-ES" + ], + "enc_values_supported" : [ + "A128GCM" + ], + "encryption_required": false + }, + "display": [ + { + "name": "Example University", + "locale": "en-US" + }, + { + "name": "Example Université", + "locale": "fr-FR" + } + ], + "credential_configurations_supported": { + "UniversityDegreeCredential": { + "format": "jwt_vc_json", + "scope": "UniversityDegree", + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "credential_signing_alg_values_supported": [ + "ES256" + ], + "credential_definition":{ + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "ES256" + ] + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://university.example.edu/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "background_image": { + "uri": "https://university.example.edu/public/background-image.png" + }, + "text_color": "#FFFFFF" + } + ] + } + } + })).unwrap(); + } + #[test] fn example_credential_metadata_jwt() { let _: CredentialMetadata = serde_json::from_value(json!({ @@ -484,7 +795,7 @@ mod test { "cryptographic_binding_methods_supported": [ "did:example" ], - "cryptographic_suites_supported": [ + "credential_signing_alg_values_supported": [ "ES256K" ], "credential_definition":{ @@ -519,9 +830,13 @@ mod test { } } }, - "proof_types_supported": [ - "jwt" - ], + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "ES256" + ] + } + }, "display": [ { "name": "University Credential", @@ -531,6 +846,9 @@ mod test { "alt_text": "a square logo of a university" }, "background_color": "#12107c", + "background_image": { + "uri": "https://university.example.edu/public/background-image.png" + }, "text_color": "#FFFFFF" } ] @@ -553,7 +871,7 @@ mod test { "cryptographic_binding_methods_supported": [ "did:example" ], - "cryptographic_suites_supported": [ + "credential_signing_alg_values_supported": [ "Ed25519Signature2018" ], "credentials_definition": { @@ -601,6 +919,9 @@ mod test { "alt_text": "a square logo of a university" }, "background_color": "#12107c", + "background_image": { + "uri": "https://university.example.edu/public/background-image.png" + }, "text_color": "#FFFFFF" } ] @@ -616,7 +937,7 @@ mod test { "cryptographic_binding_methods_supported": [ "mso" ], - "cryptographic_suites_supported": [ + "credential_signing_alg_values_supported": [ "ES256", "ES384", "ES512" ], "display": [ @@ -628,6 +949,9 @@ mod test { "alt_text": "a square figure of a mobile driving license" }, "background_color": "#12107c", + "background_image": { + "uri": "https://examplestate.com/public/background-image.png" + }, "text_color": "#FFFFFF" }, { @@ -638,6 +962,9 @@ mod test { "alt_text": "大学のロゴ" }, "background_color": "#12107c", + "background_image": { + "uri": "https://examplestate.com/public/background-image.png" + }, "text_color": "#FFFFFF" } ], @@ -668,7 +995,7 @@ mod test { "org.iso.18013.5.1.aamva": { "organ_donor": {} } - } + } })) .unwrap(); } diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 0000000..3f3d44c --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,67 @@ +#![allow(clippy::type_complexity)] + +use oauth2::{ErrorResponseType, StandardErrorResponse}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum NotificationRequestEvent { + #[serde(rename = "credential_accepted")] + CredentialAccepted, + #[serde(rename = "credential_failure")] + CredentialFailure, + #[serde(rename = "credential_deleted")] + CredentialDeleted, +} + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct NotificationRequest { + notification_id: String, + event: NotificationRequestEvent, + event_description: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum NotificationErrorCode { + #[serde(rename = "invalid_notification_id")] + InvalidNotificationId, + #[serde(rename = "invalid_notification_request")] + InvalidNotificationRequest, +} +impl ErrorResponseType for NotificationErrorCode {} +pub type NotificationErrorResponse = StandardErrorResponse; + +#[cfg(test)] +mod test { + use serde_json::json; + + use super::*; + + #[test] + fn example_notification_request() { + let _: NotificationRequest = serde_json::from_value(json!({ + "notification_id": "3fwe98js", + "event": "credential_accepted" + })) + .unwrap(); + } + + #[test] + fn example_notification_request_with_description() { + let _: NotificationRequest = serde_json::from_value(json!({ + "notification_id": "3fwe98js", + "event": "credential_failure", + "event_description": "Could not store the Credential. Out of storage." + })) + .unwrap(); + } + + #[test] + fn example_notification_error_response() { + let _: NotificationErrorResponse = serde_json::from_value(json!({ + "error": "invalid_notification_id" + })) + .unwrap(); + } +} diff --git a/src/profiles.rs b/src/profiles.rs index 1e90a85..e227819 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -5,7 +5,7 @@ use serde::{de::DeserializeOwned, Serialize}; pub trait Profile { type Metadata: CredentialMetadataProfile; type Offer: CredentialOfferProfile; - type Authorization: AuthorizationDetaislProfile; + type Authorization: AuthorizationDetailsProfile; type Credential: CredentialRequestProfile; } pub trait CredentialMetadataProfile: Clone + Debug + DeserializeOwned + Serialize { @@ -14,7 +14,7 @@ pub trait CredentialMetadataProfile: Clone + Debug + DeserializeOwned + Serializ fn to_request(&self) -> Self::Request; } pub trait CredentialOfferProfile: Debug + DeserializeOwned + Serialize {} -pub trait AuthorizationDetaislProfile: Debug + DeserializeOwned + Serialize {} +pub trait AuthorizationDetailsProfile: Debug + DeserializeOwned + Serialize {} pub trait CredentialRequestProfile: Debug + DeserializeOwned + Serialize { type Response: CredentialResponseProfile; } diff --git a/src/proof_of_possession.rs b/src/proof_of_possession.rs index c1a57b2..1fbdda6 100644 --- a/src/proof_of_possession.rs +++ b/src/proof_of_possession.rs @@ -11,6 +11,15 @@ use url::Url; const JWS_TYPE: &str = "openid4vci-proof+jwt"; +pub type ProofSigningAlgValuesSupported = Vec; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct KeyProofTypesSupported { + #[serde(rename = "$key$")] + key: KeyProofType, + proof_signing_alg_values_supported: Vec, +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum KeyProofType { #[serde(rename = "jwt")] diff --git a/src/pushed_authorization.rs b/src/pushed_authorization.rs index 58aa323..4bc765a 100644 --- a/src/pushed_authorization.rs +++ b/src/pushed_authorization.rs @@ -4,7 +4,7 @@ use crate::{ authorization::AuthorizationDetail, credential::RequestError, http_utils::{content_type_has_essence, MIME_TYPE_FORM_URLENCODED, MIME_TYPE_JSON}, - profiles::AuthorizationDetaislProfile, + profiles::AuthorizationDetailsProfile, types::ParUrl, }; use oauth2::{ @@ -17,6 +17,7 @@ use oauth2::{ }; use openidconnect::{core::CoreErrorResponseType, IssuerUrl, Nonce, StandardErrorResponse}; use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, skip_serializing_none}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ParRequestUri(pub String); @@ -40,6 +41,8 @@ impl ParRequestUri { pub type Error = StandardErrorResponse; +#[serde_as] +#[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ParAuthParams { client_id: ClientId, @@ -47,19 +50,12 @@ pub struct ParAuthParams { code_challenge: String, code_challenge_method: PkceCodeChallengeMethod, redirect_uri: RedirectUrl, - #[serde(skip_serializing_if = "Option::is_none")] response_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] client_assertion: Option, - #[serde(skip_serializing_if = "Option::is_none")] client_assertion_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] authorization_details: Option, - #[serde(skip_serializing_if = "Option::is_none")] wallet_issuer: Option, - #[serde(skip_serializing_if = "Option::is_none")] user_hint: Option, - #[serde(skip_serializing_if = "Option::is_none")] issuer_state: Option, } @@ -90,7 +86,7 @@ pub struct PushedAuthorizationResponse { pub struct PushedAuthorizationRequest<'a, AD> where - AD: AuthorizationDetaislProfile, + AD: AuthorizationDetailsProfile, { inner: oauth2::AuthorizationRequest<'a>, // TODO par_auth_url: ParUrl, @@ -103,7 +99,7 @@ where impl<'a, AD> PushedAuthorizationRequest<'a, AD> where - AD: AuthorizationDetaislProfile, + AD: AuthorizationDetailsProfile, { pub(crate) fn new( inner: oauth2::AuthorizationRequest<'a>, diff --git a/src/token.rs b/src/token.rs index 0357668..2298073 100644 --- a/src/token.rs +++ b/src/token.rs @@ -6,6 +6,10 @@ use openidconnect::{ ClientId, Nonce, RedirectUrl, StandardErrorResponse, StandardTokenResponse, }; use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, skip_serializing_none}; + +use crate::profiles::AuthorizationDetailsProfile; +use crate::{authorization::AuthorizationDetail, core::profiles::CoreProfilesAuthorizationDetails}; #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case", tag = "grant_type")] @@ -32,19 +36,45 @@ pub enum Request { }, } +#[serde_as] +#[skip_serializing_none] #[derive(Debug, Default, Deserialize, Serialize)] -pub struct ExtraResponseTokenFields { - #[serde(skip_serializing_if = "Option::is_none")] +pub struct ExtraResponseTokenFields +where + AD: AuthorizationDetailsProfile, +{ pub c_nonce: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub c_nonce_expires_in: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub authorization_pending: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub interval: Option, + #[serde(bound = "AD: AuthorizationDetailsProfile")] + pub authorization_details: Option>>, } -pub type Response = StandardTokenResponse; +pub type Response = StandardTokenResponse< + ExtraResponseTokenFields, + CoreTokenType, +>; + +/// The following additional error codes, defined in RFC8628, are +/// mentioned and can be used as follow: +/// ``` +/// use openidconnect::core::CoreErrorResponseType; +/// use oid4vci::token::Error; +/// +/// let auth_pending_err = Error::new( +/// CoreErrorResponseType::Extension("authorization_pending".to_string()), +/// None, +/// None, +/// ); +/// +/// let slow_down_err = Error::new( +/// CoreErrorResponseType::Extension("slow_down".to_string()), +/// None, +/// None, +/// ); +/// ``` pub type Error = StandardErrorResponse; -impl openidconnect::ExtraTokenFields for ExtraResponseTokenFields {} +impl openidconnect::ExtraTokenFields for ExtraResponseTokenFields where + AD: AuthorizationDetailsProfile +{ +} diff --git a/src/types.rs b/src/types.rs index 8f57c79..ce44609 100644 --- a/src/types.rs +++ b/src/types.rs @@ -271,6 +271,20 @@ new_url_type![ ParUrl ]; +new_url_type![ + /// + /// URL of the Credential Issuer's Notification Endpoint + /// + NotificationUrl +]; + +new_url_type![ + /// + /// URI where the Wallet can obtain an image + /// + ImageUrl +]; + new_type![ /// /// String value of a background color of the Credential represented as numerical color values defined in CSS Color Module Level 37 [CSS-Color].