Skip to content

Commit

Permalink
feat: add support for PresentationDefinition by value (#72)
Browse files Browse the repository at this point in the history
* feat: add `offer_id` to Offer events

* feat: add `offer_id` to all Offer events

* feat: add polling strategy for JIT credentials

* feat: add `POLLING_INTERVAL_MS` const

* fix: fix failing tests due to blocking thread

* feat: add support for `presentation_definition` in `authorization_requests` endpoint

* test: update Postman collection

* fix: remove `presentation_definition_id` from SIOPv2 request in Postman collection

* test: remove CORS header from Postman Collection

* fix: remove `expect("hello")`
  • Loading branch information
nanderstabel authored Jun 11, 2024
1 parent ff9a207 commit 7ac769e
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 30 deletions.
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

0 comments on commit 7ac769e

Please sign in to comment.