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: add support for OpenID4VP #53

Merged
merged 10 commits into from
May 13, 2024
58 changes: 45 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ rust-version = "1.76.0"

[workspace.dependencies]
did_manager = { git = "https://git@github.com/impierce/did-manager.git", rev = "c70c0f1" }
siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "a932af7" }
oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "a932af7" }
oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "a932af7" }
oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = "a932af7" }
siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" }
oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" }
oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" }
oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" }
oid4vp = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" }

async-trait = "0.1"
axum = { version = "0.7", features = ["tracing"] }
Expand Down
1 change: 1 addition & 0 deletions agent_api_rest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ http-api-problem = "0.57"
hyper = { version = "1.2" }
oid4vc-core.workspace = true
oid4vci.workspace = true
oid4vp.workspace = true
serde.workspace = true
serde_json.workspace = true
siopv2.workspace = true
Expand Down
6 changes: 6 additions & 0 deletions agent_api_rest/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ paths:
nonce:
type: string
example: "0d520cbe176ab9e1f7888c70888020d84a69672a4baabd3ce1c6aaad8f6420c0"
state:
type: string
example: "84266fdbd31d4c2c6d0665f7e8380fa3"
presentation_definition_id:
type: string
example: "presentation_definition"
required:
- nonce
responses:
Expand Down
5 changes: 3 additions & 2 deletions agent_api_rest/postman/ssi-agent.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"}",
""
],
"type": "text/javascript"
"type": "text/javascript",
"packages": {}
}
}
],
Expand Down Expand Up @@ -251,7 +252,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"nonce\": \"this is a nonce\"\n}",
"raw": "{\n \"nonce\": \"this is a nonce\",\n \"presentation_definition_id\": \"presentation_definition\"\n}",
"options": {
"raw": {
"language": "json"
Expand Down
8 changes: 5 additions & 3 deletions agent_api_rest/src/issuance/credential_issuer/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ mod tests {

use crate::{
app,
issuance::{credential_issuer::token::tests::token, credentials::CredentialsRequest, offers::tests::offers},
issuance::{
credential_issuer::token::tests::token, credentials::CredentialsEndpointRequest, offers::tests::offers,
},
tests::{BASE_URL, OFFER_ID},
};

Expand Down Expand Up @@ -170,14 +172,14 @@ mod tests {

// The 'backend' server can either opt for an already signed credential...
let credentials_endpoint_request = if is_self_signed {
CredentialsRequest {
CredentialsEndpointRequest {
offer_id: offer_id.clone(),
credential: json!("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkI3o2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCIsInN1YiI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiJdLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9jcmVkZW50aWFscy8zNTI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1raWlleW9MTVNWc0pBWnY3SmplNXdXU2tERXltVWdreUY4a2JjcmpacFgzcWQiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsIm5hbWUiOiJUZWFtd29yayBCYWRnZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4iLCJpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIn19fQ.r7T_zOXP7E2k7eAPq5EF20shwrnPKK0mOCfNaB0phPEXVkYSG_sf6QygUDuJ8-P0yU4EEajgE0dxJuRfdMVDAQ"),
is_signed: true,
}
} else {
// ...or else, submitting the data that will be signed inside `UniCore`.
CredentialsRequest {
CredentialsEndpointRequest {
offer_id: offer_id.clone(),
credential: json!({
"credentialSubject": {
Expand Down
4 changes: 2 additions & 2 deletions agent_api_rest/src/issuance/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub(crate) async fn get_credentials(State(state): State<IssuanceState>, Path(cre

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CredentialsRequest {
pub struct CredentialsEndpointRequest {
pub offer_id: String,
pub credential: Value,
#[serde(default)]
Expand All @@ -43,7 +43,7 @@ pub(crate) async fn credentials(
) -> Response {
info!("Request Body: {}", payload);

let Ok(CredentialsRequest {
let Ok(CredentialsEndpointRequest {
offer_id,
credential: data,
is_signed,
Expand Down
4 changes: 2 additions & 2 deletions agent_api_rest/src/issuance/offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ use tracing::info;

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OffersRequest {
pub struct OffersEndpointRequest {
pub offer_id: String,
}

#[axum_macros::debug_handler]
pub(crate) async fn offers(State(state): State<IssuanceState>, Json(payload): Json<Value>) -> Response {
info!("Request Body: {}", payload);

let Ok(OffersRequest { offer_id }) = serde_json::from_value(payload) else {
let Ok(OffersEndpointRequest { offer_id }) = serde_json::from_value(payload) else {
return (StatusCode::BAD_REQUEST, "invalid payload").into_response();
};

Expand Down
46 changes: 37 additions & 9 deletions agent_api_rest/src/verification/authorization_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use axum::{
Json,
};
use hyper::header;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::info;

Expand All @@ -24,32 +25,58 @@ pub(crate) async fn get_authorization_requests(
// Get the authorization request if it exists.
match query_handler(&authorization_request_id, &state.query.authorization_request).await {
Ok(Some(AuthorizationRequestView {
siopv2_authorization_request: Some(siopv2_authorization_request),
authorization_request: Some(authorization_request),
..
})) => (StatusCode::OK, Json(siopv2_authorization_request)).into_response(),
})) => (StatusCode::OK, Json(authorization_request)).into_response(),
Ok(None) => StatusCode::NOT_FOUND.into_response(),
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}

#[derive(Deserialize, Serialize)]
pub struct AuthorizationRequestsEndpointRequest {
pub nonce: String,
pub state: Option<String>,
pub presentation_definition_id: Option<String>,
}

#[axum_macros::debug_handler]
pub(crate) async fn authorization_requests(
State(verification_state): State<VerificationState>,
Json(payload): Json<Value>,
) -> Response {
info!("Request Body: {}", payload);

let nonce = if let Some(nonce) = payload["nonce"].as_str() {
nonce
} else {
return (StatusCode::BAD_REQUEST, "nonce is required").into_response();
let Ok(AuthorizationRequestsEndpointRequest {
nonce,
state,
presentation_definition_id,
}) = serde_json::from_value(payload)
else {
return (StatusCode::BAD_REQUEST, "invalid payload").into_response();
};

let state = generate_random_string();
let state = state.unwrap_or(generate_random_string());

// TODO: This needs to be properly fixed instead of reading the presentation definitions from the file system
// everytime a request is made. `PresentationDefinition`'s should be implemented as a proper `Aggregate`. This
// current suboptimal solution requires the `./tmp:/app/agent_api_rest` volume to be mounted in the `docker-compose.yml`.
let presentation_definition = presentation_definition_id.map(|presentation_definition_id| {
let project_root_dir = env!("CARGO_MANIFEST_DIR");

serde_json::from_reader(
std::fs::File::open(format!(
"{project_root_dir}/../agent_verification/presentation_definitions/{presentation_definition_id}.json"
))
.unwrap(),
)
.unwrap()
});

let command = AuthorizationRequestCommand::CreateAuthorizationRequest {
nonce: nonce.to_string(),
state: state.clone(),
presentation_definition,
};

// Create the authorization request.
Expand All @@ -75,7 +102,7 @@ pub(crate) async fn authorization_requests(
// Return the credential.
match query_handler(&state, &verification_state.query.authorization_request).await {
Ok(Some(AuthorizationRequestView {
form_url_encoded_authorization_request,
form_url_encoded_authorization_request: Some(form_url_encoded_authorization_request),
..
})) => (
StatusCode::CREATED,
Expand Down Expand Up @@ -114,7 +141,8 @@ pub mod tests {
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
.body(Body::from(
serde_json::to_vec(&json!({
"nonce": "nonce"
"nonce": "nonce",
"presentation_definition_id": "presentation_definition"
}))
.unwrap(),
))
Expand Down
Loading