Skip to content

Commit

Permalink
Resolve interoperability issues. (#20)
Browse files Browse the repository at this point in the history
* Add helper functions for initiating flow
* Address some nuances of authorization server metadata discovery
* Resolve issues with authorization details and credential configuration parsing
* Add support for multi-credential response in latest draft
* Add e2e manual test
  • Loading branch information
cobward authored Sep 12, 2024
1 parent 7a0f9d3 commit c5018b6
Show file tree
Hide file tree
Showing 37 changed files with 2,793 additions and 1,585 deletions.
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ license = "Apache-2.0 OR MIT"
description = "OpenID for Verifiable Credentials Issuance"
repository = "https://github.com/spruceid/oidc4vci-rs/"

[features]
reqwest = ["oauth2/reqwest"]

[dependencies]
# TODO feature-gate
isomdl = { git = "https://github.com/spruceid/isomdl", rev = "90ce218" }
Expand All @@ -31,10 +34,13 @@ base64 = "0.21.4"
serde_urlencoded = "0.7.1"
anyhow = "1.0.86"
sha2 = "0.10.8"
form_urlencoded = "1.2.1"
percent-encoding = "2.3.1"

[dev-dependencies]
assert-json-diff = "2.0.2"
did-jwk = "0.2.0"
did-method-key = "0.3.0"
ssi-verification-methods = "0.1.1"
tokio = { version = "1.25.0", features = ["macros"] }
oid4vci = { path = ".", features = ["reqwest"] }
106 changes: 65 additions & 41 deletions src/authorization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use url::Url;

use crate::{
profiles::AuthorizationDetailsProfile,
profiles::AuthorizationDetailsObjectProfile,
types::{IssuerState, IssuerUrl, UserHint},
};

Expand All @@ -29,9 +29,9 @@ impl<'a> AuthorizationRequest<'a> {
self
}

pub fn set_authorization_details<AD: AuthorizationDetailsProfile>(
pub fn set_authorization_details<AD: AuthorizationDetailsObjectProfile>(
mut self,
authorization_details: Vec<AuthorizationDetail<AD>>,
authorization_details: Vec<AuthorizationDetailsObject<AD>>,
) -> Result<Self, serde_json::Error> {
self.inner = self.inner.add_extra_param(
"authorization_details",
Expand Down Expand Up @@ -70,20 +70,41 @@ impl<'a> AuthorizationRequest<'a> {
}

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct AuthorizationDetail<AD>
pub struct AuthorizationDetailsObject<AD>
where
AD: AuthorizationDetailsProfile,
AD: AuthorizationDetailsObjectProfile,
{
r#type: AuthorizationDetailType,
#[serde(flatten, bound = "AD: AuthorizationDetailsProfile")]
addition_profile_fields: AD,
#[serde(skip_serializing_if = "Option::is_none")]
locations: Option<Vec<IssuerUrl>>,
r#type: AuthorizationDetailsObjectType,
#[serde(flatten, bound = "AD: AuthorizationDetailsObjectProfile")]
additional_profile_fields: AD,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
locations: Vec<IssuerUrl>,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorizationDetailType {
impl<AD> AuthorizationDetailsObject<AD>
where
AD: AuthorizationDetailsObjectProfile,
{
pub fn new(additional_profile_fields: AD) -> Self {
Self {
r#type: AuthorizationDetailsObjectType::OpenidCredential,
additional_profile_fields,
locations: Vec::new(),
}
}

field_getters_setters![
pub self [self] ["authorization detail value"] {
set_additional_profile_fields -> additional_profile_fields[AD],
set_locations -> locations[Vec<IssuerUrl>],
}
];
}

#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub enum AuthorizationDetailsObjectType {
#[default]
#[serde(rename = "openid_credential")]
OpenidCredential,
}

Expand All @@ -97,7 +118,7 @@ mod test {
use crate::{
core::{
metadata::CredentialIssuerMetadata,
profiles::{w3c, CoreProfilesAuthorizationDetails, ValueAuthorizationDetails},
profiles::{jwt_vc_json, CoreProfilesAuthorizationDetailsObject},
},
metadata::AuthorizationServerMetadata,
types::CredentialUrl,
Expand All @@ -107,7 +128,7 @@ mod test {

#[test]
fn example_authorization_details() {
let _: Vec<AuthorizationDetail<CoreProfilesAuthorizationDetails>> =
let _: Vec<AuthorizationDetailsObject<CoreProfilesAuthorizationDetailsObject>> =
serde_json::from_value(json!([
{
"type": "openid_credential",
Expand All @@ -125,7 +146,7 @@ mod test {

#[test]
fn example_authorization_details_credential_configuration_id() {
let _: Vec<AuthorizationDetail<CoreProfilesAuthorizationDetails>> =
let _: Vec<AuthorizationDetailsObject<CoreProfilesAuthorizationDetailsObject>> =
serde_json::from_value(json!([
{
"type": "openid_credential",
Expand All @@ -137,23 +158,21 @@ mod test {

#[test]
fn example_authorization_details_credential_configuration_id_deny() {
assert!(
serde_json::from_value::<Vec<AuthorizationDetail<CoreProfilesAuthorizationDetails>>>(
json!([
{
"type": "openid_credential",
"format": "jwt_vc_json",
"credential_configuration_id": "UniversityDegreeCredential"
}
])
)
.is_err()
);
assert!(serde_json::from_value::<
Vec<AuthorizationDetailsObject<CoreProfilesAuthorizationDetailsObject>>,
>(json!([
{
"type": "openid_credential",
"format": "jwt_vc_json",
"credential_configuration_id": "UniversityDegreeCredential"
}
]))
.is_err());
}

#[test]
fn example_authorization_details_locations() {
let _: Vec<AuthorizationDetail<CoreProfilesAuthorizationDetails>> =
let _: Vec<AuthorizationDetailsObject<CoreProfilesAuthorizationDetailsObject>> =
serde_json::from_value(json!([
{
"type": "openid_credential",
Expand All @@ -174,7 +193,7 @@ mod test {

#[test]
fn example_authorization_details_multiple() {
let _: Vec<crate::core::authorization::AuthorizationDetail> =
let _: Vec<crate::core::authorization::AuthorizationDetailsObject> =
serde_json::from_value(json!([
{
"type":"openid_credential",
Expand Down Expand Up @@ -230,17 +249,22 @@ mod test {
PkceCodeVerifier::new("challengechallengechallengechallengechallenge".into());
let pkce_challenge = PkceCodeChallenge::from_code_verifier_sha256(&pkce_verifier);
let state = CsrfToken::new("state".into());
let authorization_details = vec![AuthorizationDetail {
r#type: AuthorizationDetailType::OpenidCredential,
addition_profile_fields: CoreProfilesAuthorizationDetails::Value(
ValueAuthorizationDetails::JWTVC(w3c::jwt::AuthorizationDetails::new(
w3c::CredentialDefinition::new(vec![
"VerifiableCredential".into(),
"UniversityDegreeCredential".into(),
]),
)),
),
locations: None,
let authorization_detail = jwt_vc_json::AuthorizationDetailsObjectWithFormat::default()
.set_credential_definition(
jwt_vc_json::authorization_detail::CredentialDefinition::default().set_type(vec![
"VerifiableCredential".into(),
"UniversityDegreeCredential".into(),
]),
);
let authorization_details = vec![AuthorizationDetailsObject {
r#type: AuthorizationDetailsObjectType::OpenidCredential,
additional_profile_fields: CoreProfilesAuthorizationDetailsObject::WithFormat {
inner: crate::core::profiles::AuthorizationDetailsObjectWithFormat::JwtVcJson(
authorization_detail,
),
_credential_identifier: (),
},
locations: vec![],
}];
let req = client
.authorize_url(move || state)
Expand Down
50 changes: 30 additions & 20 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::marker::PhantomData;

use oauth2::{
basic::{BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse},
AccessToken, AuthUrl, AuthorizationCode, ClientId, CodeTokenRequest, ConfigurationError,
Expand All @@ -10,13 +12,17 @@ use crate::{
credential,
credential_response_encryption::CredentialResponseEncryptionMetadata,
metadata::{
credential_issuer::{CredentialIssuerMetadataDisplay, CredentialMetadata},
credential_issuer::{CredentialConfiguration, CredentialIssuerMetadataDisplay},
AuthorizationServerMetadata, CredentialIssuerMetadata,
},
pre_authorized_code::PreAuthorizedCodeTokenRequest,
profiles::Profile,
pushed_authorization::PushedAuthorizationRequest,
token,
types::{BatchCredentialUrl, CredentialUrl, DeferredCredentialUrl, IssuerUrl, ParUrl},
types::{
BatchCredentialUrl, CredentialUrl, DeferredCredentialUrl, IssuerUrl, ParUrl,
PreAuthorizedCode,
},
};

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -53,7 +59,7 @@ where
batch_credential_endpoint: Option<BatchCredentialUrl>,
deferred_credential_endpoint: Option<DeferredCredentialUrl>,
credential_response_encryption: Option<CredentialResponseEncryptionMetadata>,
credential_configurations_supported: Vec<CredentialMetadata<C::Configuration>>,
credential_configurations_supported: Vec<CredentialConfiguration<C::CredentialConfiguration>>,
display: Option<Vec<CredentialIssuerMetadataDisplay>>,
}

Expand All @@ -68,23 +74,15 @@ where
set_batch_credential_endpoint -> batch_credential_endpoint[Option<BatchCredentialUrl>],
set_deferred_credential_endpoint -> deferred_credential_endpoint[Option<DeferredCredentialUrl>],
set_credential_response_encryption -> credential_response_encryption[Option<CredentialResponseEncryptionMetadata>],
set_credential_configurations_supported -> credential_configurations_supported[Vec<CredentialMetadata<C::Configuration>>],
set_credential_configurations_supported -> credential_configurations_supported[Vec<CredentialConfiguration<C::CredentialConfiguration>>],
set_display -> display[Option<Vec<CredentialIssuerMetadataDisplay>>],
}
];

pub fn from_credential_offer() -> Result<Self, Error> {
todo!()
}

pub async fn from_credential_offer_async() -> Result<Self, Error> {
todo!()
}

pub fn from_issuer_metadata(
client_id: ClientId,
redirect_uri: RedirectUrl,
credential_issuer_metadata: CredentialIssuerMetadata<C::Configuration>,
credential_issuer_metadata: CredentialIssuerMetadata<C::CredentialConfiguration>,
authorization_metadata: AuthorizationServerMetadata,
) -> Self {
let inner = Self::new_inner_client(
Expand Down Expand Up @@ -158,24 +156,36 @@ where
self.inner.exchange_code(code)
}

// pub fn exchange_pre_authorized_code(&self, pre_authorized_code: PreAuthorizedCode) -> () {
// todo!()
// }
pub fn exchange_pre_authorized_code(
&self,
pre_authorized_code: PreAuthorizedCode,
) -> PreAuthorizedCodeTokenRequest<'_, BasicErrorResponse, token::Response> {
PreAuthorizedCodeTokenRequest {
auth_type: self.inner.auth_type(),
client_id: Some(self.inner.client_id()),
client_secret: None,
code: pre_authorized_code,
extra_params: Vec::new(),
token_url: self.inner.token_uri(),
tx_code: None,
_phantom: PhantomData,
}
}

pub fn request_credential(
&self,
access_token: AccessToken,
profile_fields: C::Credential,
) -> credential::RequestBuilder<C::Credential> {
profile_fields: C::CredentialRequest,
) -> credential::RequestBuilder<C::CredentialRequest> {
let body = credential::Request::new(profile_fields);
credential::RequestBuilder::new(body, self.credential_endpoint().clone(), access_token)
}

pub fn batch_request_credential(
&self,
access_token: AccessToken,
profile_fields: Vec<C::Credential>,
) -> Result<credential::BatchRequestBuilder<C::Credential>, Error> {
profile_fields: Vec<C::CredentialRequest>,
) -> Result<credential::BatchRequestBuilder<C::CredentialRequest>, Error> {
let Some(endpoint) = self.batch_credential_endpoint() else {
return Err(Error::BcrUnsupported);
};
Expand Down
66 changes: 8 additions & 58 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@ pub mod profiles;
pub mod metadata {
use crate::metadata;

use super::profiles::CoreProfilesConfiguration;
use super::profiles::CoreProfilesCredentialConfiguration;

pub type CredentialIssuerMetadata =
metadata::CredentialIssuerMetadata<CoreProfilesConfiguration>;
metadata::CredentialIssuerMetadata<CoreProfilesCredentialConfiguration>;
}

pub mod credential {
use crate::credential;

use super::profiles::CoreProfilesRequest;
use super::profiles::CoreProfilesCredentialRequest;

pub type Request = credential::Request<CoreProfilesRequest>;
pub type BatchRequest = credential::BatchRequest<CoreProfilesRequest>;
pub type Request = credential::Request<CoreProfilesCredentialRequest>;
pub type BatchRequest = credential::BatchRequest<CoreProfilesCredentialRequest>;
}

pub mod authorization {
use crate::authorization;

use super::profiles::CoreProfilesAuthorizationDetails;
use super::profiles::CoreProfilesAuthorizationDetailsObject;

pub type AuthorizationDetail =
authorization::AuthorizationDetail<CoreProfilesAuthorizationDetails>;
pub type AuthorizationDetailsObject =
authorization::AuthorizationDetailsObject<CoreProfilesAuthorizationDetailsObject>;
}

pub mod client {
Expand All @@ -35,53 +35,3 @@ pub mod client {

pub type Client = client::Client<CoreProfiles>;
}

#[cfg(test)]
mod test {
use profiles::w3c::{self, CredentialDefinitionLD};

use crate::{
credential_response_encryption::CredentialUrl,
metadata::credential_issuer::CredentialMetadata, types::IssuerUrl,
};

use super::*;

#[test]
fn serialize_issuer_metadata_jwtvc() {
let metadata = super::metadata::CredentialIssuerMetadata::new(
IssuerUrl::from_url("https://example.com".parse().unwrap()),
CredentialUrl::from_url("https://example.com/credential".parse().unwrap()),
)
.set_credential_configurations_supported(vec![CredentialMetadata::new(
"credential1".into(),
profiles::CoreProfilesConfiguration::JWTVC(w3c::jwt::Configuration::new(
w3c::CredentialDefinition::new(vec!["type1".into()]),
)),
)]);
serde_json::to_vec(&metadata).unwrap();
}

#[test]
fn serialize_issuer_metadata_ldpvc() {
let metadata = super::metadata::CredentialIssuerMetadata::new(
IssuerUrl::from_url("https://example.com".parse().unwrap()),
CredentialUrl::from_url("https://example.com/credential".parse().unwrap()),
)
.set_credential_configurations_supported(vec![CredentialMetadata::new(
"credential1".into(),
profiles::CoreProfilesConfiguration::LDVC(w3c::ldp::Configuration::new(
vec![serde_json::Value::String(
"http://example.com/context".into(),
)],
CredentialDefinitionLD::new(
w3c::CredentialDefinition::new(vec!["type1".into()]),
vec![serde_json::Value::String(
"http://example.com/context".into(),
)],
),
)),
)]);
serde_json::to_vec(&metadata).unwrap();
}
}
Loading

0 comments on commit c5018b6

Please sign in to comment.