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 PresentationDefinition by value #72

Merged
merged 11 commits into from
Jun 11, 2024
104 changes: 102 additions & 2 deletions agent_api_rest/postman/ssi-agent.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credential\": {\n \"credentialSubject\": {\n \"type\":[\n \"AchievementSubject\"\n ],\n \"achievement\":{\n \"id\":\"https://demo.edubadges.nl/public/assertions/6pEB--n-SwiZPtWXMCB2jQ\",\n \"name\":\"Edubadge account complete\",\n \"type\":[\n \"Achievement\"\n ],\n \"image\":{\n \"id\":\"https://api-demo.edubadges.nl/media/uploads/badges/issuer_badgeclass_548517aa-cbab-4a7b-a971-55cdcce0e2a5.png\"\n },\n \"criteria\":{\n \"narrative\":\"To qualify for this edubadge:\\r\\n\\r\\n* you successfully created an eduID,\\r\\n* you successfully linked your institution to your eduID,\\r\\n* you can store and manage them safely in your backpack.\"\n },\n \"description\":\"### Welcome to edubadges. Let your life long learning begin! ###\\r\\n\\r\\nYou are now ready to collect all your edubadges in your backpack. In your backpack you can store and manage them safely.\\r\\n\\r\\nShare them anytime you like and with whom you like.\\r\\n\\r\\nEdubadges are visual representations of your knowledge, skills and competences.\"\n }\n }\n }\n}",
"raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credential\": {\n \"credentialSubject\": {\n \"first_name\": \"Ferris\",\n \"last_name\": \"Crabman\",\n \"dob\": \"1982-01-01\"\n }\n }\n}",
"options": {
"raw": {
"language": "json"
Expand Down Expand Up @@ -214,7 +214,107 @@
"name": "Verification",
"item": [
{
"name": "authorization_requests",
"name": "siopv2_authorization_requests",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const location = pm.response.headers.get(\"LOCATION\");",
"",
"if(location){",
" pm.collectionVariables.set(\"AUTHORIZATION_REQUEST_LOCATION\",location)",
"}",
"",
"const authorization_request = responseBody;",
"",
"const decodedString = decodeURIComponent(authorization_request);",
"",
"// Regular expression to match the value of request_uri",
"const regex = /request_uri=([^&]+)/;",
"",
"// Executing the regular expression on the input string",
"const match = regex.exec(decodedString);",
"",
"// Extracting the value of request_uri",
"const requestUri = match ? decodeURIComponent(match[1]) : null;",
"",
"pm.collectionVariables.set(\"REQUEST_URI\",requestUri)",
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"nonce\": \"this is a nonce\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://{{HOST}}/v1/authorization_requests"
},
"response": []
},
{
"name": "oid4vp_authorization_requests_with_presentation_definition",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const location = pm.response.headers.get(\"LOCATION\");",
"",
"if(location){",
" pm.collectionVariables.set(\"AUTHORIZATION_REQUEST_LOCATION\",location)",
"}",
"",
"const authorization_request = responseBody;",
"",
"const decodedString = decodeURIComponent(authorization_request);",
"",
"// Regular expression to match the value of request_uri",
"const regex = /request_uri=([^&]+)/;",
"",
"// Executing the regular expression on the input string",
"const match = regex.exec(decodedString);",
"",
"// Extracting the value of request_uri",
"const requestUri = match ? decodeURIComponent(match[1]) : null;",
"",
"pm.collectionVariables.set(\"REQUEST_URI\",requestUri)",
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"nonce\": \"this is a nonce\",\n \"presentation_definition\": {\n \"id\":\"Verifiable Presentation request for sign-on\",\n \"input_descriptors\":[\n {\n \"id\":\"Request for Verifiable Credential\",\n \"constraints\":{\n \"fields\":[\n {\n \"path\":[\n \"$.vc.type\"\n ],\n \"filter\":{\n \"type\":\"array\",\n \"contains\":{\n \"const\":\"VerifiableCredential\"\n }\n }\n }\n ]\n }\n }\n ]\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://{{HOST}}/v1/authorization_requests"
},
"response": []
},
{
"name": "oid4vp_authorization_requests_with_presentation_definition_id",
"event": [
{
"listen": "test",
Expand Down
78 changes: 52 additions & 26 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 oid4vp::PresentationDefinition;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::info;
Expand All @@ -37,7 +38,15 @@ pub(crate) async fn get_authorization_requests(
pub struct AuthorizationRequestsEndpointRequest {
pub nonce: String,
pub state: Option<String>,
pub presentation_definition_id: Option<String>,
#[serde(flatten)]
pub presentation_definition: Option<PresentationDefinitionResource>,
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PresentationDefinitionResource {
PresentationDefinitionId(String),
PresentationDefinition(PresentationDefinition),
}

#[axum_macros::debug_handler]
Expand All @@ -50,27 +59,32 @@ pub(crate) async fn authorization_requests(
let Ok(AuthorizationRequestsEndpointRequest {
nonce,
state,
presentation_definition_id,
presentation_definition,
}) = serde_json::from_value(payload)
else {
return (StatusCode::BAD_REQUEST, "invalid payload").into_response();
};

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 presentation_definition = presentation_definition.map(|presentation_definition| {
match presentation_definition {
// 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`.
PresentationDefinitionResource::PresentationDefinitionId(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()
}
PresentationDefinitionResource::PresentationDefinition(presentation_definition) => presentation_definition,
}
});

let command = AuthorizationRequestCommand::CreateAuthorizationRequest {
Expand Down Expand Up @@ -129,23 +143,32 @@ pub mod tests {
http::{self, Request},
Router,
};
use serde_json::json;
use rstest::rstest;
use tower::Service;

pub async fn authorization_requests(app: &mut Router) -> String {
pub async fn authorization_requests(app: &mut Router, by_value: bool) -> String {
let request_body = AuthorizationRequestsEndpointRequest {
nonce: "nonce".to_string(),
state: None,
presentation_definition: Some(if by_value {
PresentationDefinitionResource::PresentationDefinition(
serde_json::from_str(include_str!(
"../../../agent_verification/presentation_definitions/presentation_definition.json"
))
.unwrap(),
)
} else {
PresentationDefinitionResource::PresentationDefinitionId("presentation_definition".to_string())
}),
};

let response = app
.call(
Request::builder()
.method(http::Method::POST)
.uri("/v1/authorization_requests")
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
.body(Body::from(
serde_json::to_vec(&json!({
"nonce": "nonce",
"presentation_definition_id": "presentation_definition"
}))
.unwrap(),
))
.body(Body::from(serde_json::to_vec(&request_body).unwrap()))
.unwrap(),
)
.await
Expand Down Expand Up @@ -188,9 +211,12 @@ pub mod tests {
form_url_encoded_authorization_request
}

#[rstest]
#[case::with_presentation_definition_by_value(true)]
#[case::with_presentation_definition_id(false)]
#[tokio::test]
#[tracing_test::traced_test]
async fn test_authorization_requests_endpoint() {
async fn test_authorization_requests_endpoint(#[case] by_value: bool) {
let issuance_state = in_memory::issuance_state(Default::default()).await;
let verification_state = in_memory::verification_state(
test_verification_services(&config!("default_did_method").unwrap_or("did:key".to_string())),
Expand All @@ -199,6 +225,6 @@ pub mod tests {
.await;
let mut app = app((issuance_state, verification_state));

authorization_requests(&mut app).await;
authorization_requests(&mut app, by_value).await;
}
}
2 changes: 1 addition & 1 deletion agent_api_rest/src/verification/relying_party/redirect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ pub mod tests {

let mut app = app((issuance_state, verification_state));

let form_url_encoded_authorization_request = authorization_requests(&mut app).await;
let form_url_encoded_authorization_request = authorization_requests(&mut app, false).await;

// Extract the state from the form_url_encoded_authorization_request.
let state = form_url_encoded_authorization_request
Expand Down
2 changes: 1 addition & 1 deletion agent_api_rest/src/verification/relying_party/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pub mod tests {
.await;
let mut app = app((issuance_state, verification_state));

let form_url_encoded_authorization_request = authorization_requests(&mut app).await;
let form_url_encoded_authorization_request = authorization_requests(&mut app, false).await;

// Extract the state from the form_url_encoded_authorization_request.
let state = form_url_encoded_authorization_request
Expand Down