Skip to content

Commit

Permalink
feat: add OID4VCI Credential Issuer, generic ApplicationState, API, e…
Browse files Browse the repository at this point in the history
…rror handling (#2)

* feat: add issuance structure

* feat: implement Credential aggregate handle

* test: dummy handle implementation

* feat: add CredentialTemplateCreated event

* feat: add agent_store

* test: add integration test

* feat: add jsonschema dep

* feat: add JSONschema's

* feat: add queries agent_api and agent_application

* refactor: move handlers to agent_issuance

* refactor: rename resources folder

* chore: add workspace

* refactor: move api code to crate

* ci: add docker files

* chore: clean up deps, define workspace deps, add basic crate descriptions

* style: set lines max_width to 120

* feat: add IssuanceData and openid4vci

* WIP

* feat: add axum_auth

* test: fix tests

* feat: add in memory application state

* fix: lil' fix

* style: fix clippy

* style: clean code

* fix: rename credential_query to issuance_data_query

* fix: replace snakecase

* fix: resolve review comments

* style: move fonfig() to config.rs

* fix: fix messages

* feat: add endpoints

* feat: prepare multisubject

* feat: implement thiserror for agent_issuance

* feat: prepare startup_events

* test: add postman collection

* fix: undo openapi changes

* fix: newline

* fix: temporary fix for unsafe indexing

* fix: clean

* fix: use workspace

* fix: apply review changes

---------

Co-authored-by: Daniel Mader <daniel.mader@impierce.com>
  • Loading branch information
nanderstabel and daniel-mader authored Nov 29, 2023
1 parent 1b944ee commit 1c8533c
Show file tree
Hide file tree
Showing 36 changed files with 3,528 additions and 514 deletions.
1,480 changes: 1,372 additions & 108 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@ members = [
]

[workspace.dependencies]
lazy_static = "1.4"
uuid = { version = "1.4", features = ["v4", "fast-rng", "serde"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0" }
thiserror = "1.0"
url = "2.5"
17 changes: 14 additions & 3 deletions agent_api_rest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@ edition = "2021"

[dependencies]
agent_issuance = { path = "../agent_issuance" }
agent_store = { path = "../agent_store" }

oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", branch = "feat/refactor-request" }
oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", branch = "feat/refactor-request" }

axum = "0.6"
axum-auth = "0.4"
axum-macros = "0.3"
serde_json = "1.0"
hyper = { version = "0.14", features = ["full"] }
serde.workspace = true
serde_json.workspace = true
uuid.workspace = true

[dev-dependencies]
hyper = { version = "0.14", features = ["full"] }
agent_store = { path = "../agent_store" }

lazy_static.workspace = true
mime = { version = "0.3" }
tokio = { version = "1.34", features = ["full"] }
tower = { version = "0.4" }
url.workspace = true

251 changes: 251 additions & 0 deletions agent_api_rest/postman/ssi-agent.postman_collection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
{
"info": {
"_postman_id": "53b46e18-de7f-4973-8304-8238844a71ce",
"name": "ssi-agent",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
"_exporter_id": "24972330"
},
"item": [
{
"name": "Issuance",
"item": [
{
"name": "subjects",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"const subjectId = jsonData?.id;",
"",
"if(subjectId){",
" pm.collectionVariables.set(\"subjectId\",subjectId)",
"}",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"value\": \"{{preAuthorizedCode}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://{{host}}/v1/subjects"
},
"response": []
},
{
"name": "credentials",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"subjectId\": \"{{subjectId}}\",\n \"credential\": {\n \"credentialSubject\": {\n \"id\": {},\n \"type\": \"AchievementSubject\",\n \"achievement\": {\n \"id\": \"https://example.com/achievements/21st-century-skills/teamwork\",\n \"type\": \"Achievement\",\n \"criteria\": {\n \"narrative\": \"Team members are nominated for this badge by their peers and recognized upon review by Example Corp management.\"\n },\n \"description\": \"This badge recognizes the development of the capacity to collaborate within a group environment.\",\n \"name\": \"Teamwork\"\n }\n }\n }\n}\n",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://{{host}}/v1/credentials"
},
"response": []
},
{
"name": "offers",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"subjectId\": \"{{subjectId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://{{host}}/v1/offers"
},
"response": []
}
]
},
{
"name": "oid4vci",
"item": [
{
"name": "oauth-authorization-server",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"const issuer = jsonData?.issuer;",
"const tokenEndpoint = jsonData?.token_endpoint;",
"",
"if(issuer){",
" pm.collectionVariables.set(\"issuer\",issuer)",
"}",
"",
"if(tokenEndpoint){",
" pm.collectionVariables.set(\"tokenEndpoint\",tokenEndpoint)",
"}",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": "http://{{host}}/.well-known/oauth-authorization-server"
},
"response": []
},
{
"name": "openid-credential-issuer",
"request": {
"method": "GET",
"header": [],
"url": "http://{{host}}/.well-known/openid-credential-issuer"
},
"response": []
},
{
"name": "token",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"const accessToken = jsonData?.access_token;",
"",
"if(accessToken){",
" pm.collectionVariables.set(\"accessToken\",accessToken)",
"}",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "grant_type",
"value": "urn:ietf:params:oauth:grant-type:pre-authorized_code",
"type": "text"
},
{
"key": "pre-authorized_code",
"value": "{{preAuthorizedCode}}",
"type": "text"
}
]
},
"url": "http://{{host}}/v1/oauth/token"
},
"response": []
},
{
"name": "credential",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{accessToken}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"format\": \"jwt_vc_json\",\n \"credential_definition\": {\n \"type\": [\n \"VerifiableCredential\",\n \"OpenBadgeCredential\"\n ]\n },\n \"proof\": {\n \"proof_type\": \"jwt\",\n \"jwt\": \"eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkVkRFNBIiwia2lkIjoiZGlkOmtleTp6Nk1rdWlSS3ExZktyekFYZVNOaUd3cnBKUFB1Z1k4QXhKWUE1Y3BDdlpDWUJEN0IjejZNa3VpUktxMWZLcnpBWGVTTmlHd3JwSlBQdWdZOEF4SllBNWNwQ3ZaQ1lCRDdCIn0.eyJpc3MiOiJkaWQ6a2V5Ono2TWt1aVJLcTFmS3J6QVhlU05pR3dycEpQUHVnWThBeEpZQTVjcEN2WkNZQkQ3QiIsImF1ZCI6Imh0dHA6Ly8xOTIuMTY4LjEuMTI3OjMwMzMvIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjE1NzEzMjQ4MDAsIm5vbmNlIjoidW5zYWZlX2Nfbm9uY2UifQ.wR2e4VUnVjG6IK9cntcqvc_8KEJQUd3SEjsPZwDYDlYqijZ4ZaQLxyHtzNmLkIS3FpChLrZrcvIUJrZxrWcKAg\"\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://{{host}}/v1/openid4vci/credential"
},
"response": []
}
]
}
],
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
],
"variable": [
{
"key": "host",
"value": "INITIAL_VALUE",
"type": "string"
},
{
"key": "preAuthorizedCode",
"value": "unique_subject_string",
"type": "string"
},
{
"key": "subjectId",
"value": "INITIAL_VALUE",
"type": "string"
},
{
"key": "issuer",
"value": "INITIAL_VALUE",
"type": "string"
},
{
"key": "tokenEndpoint",
"value": "INITIAL_VALUE",
"type": "string"
},
{
"key": "accessToken",
"value": "INITIAL_VALUE",
"type": "string"
}
]
}
56 changes: 56 additions & 0 deletions agent_api_rest/src/credential_issuer/credential.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use agent_issuance::{
command::IssuanceCommand,
handlers::{command_handler, query_handler},
model::aggregate::IssuanceData,
queries::IssuanceDataView,
state::ApplicationState,
};
use axum::{
extract::{Json, State},
http::StatusCode,
response::IntoResponse,
};
use axum_auth::AuthBearer;
use oid4vci::credential_request::CredentialRequest;

use crate::AGGREGATE_ID;

#[axum_macros::debug_handler]
pub(crate) async fn credential(
State(state): State<ApplicationState<IssuanceData, IssuanceDataView>>,
AuthBearer(access_token): AuthBearer,
Json(credential_request): Json<CredentialRequest>,
) -> impl IntoResponse {
let command = IssuanceCommand::CreateCredentialResponse {
access_token: access_token.clone(),
credential_request,
};

match command_handler(AGGREGATE_ID.to_string(), &state, command).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => {
println!("Error: {:#?}\n", err);
(StatusCode::BAD_REQUEST, err.to_string()).into_response()
}
};

match query_handler(AGGREGATE_ID.to_string(), &state).await {
Ok(Some(view)) => {
// TODO: This is a non-idiomatic way of finding the subject by using the access token. We should use a aggregate/query instead.
let subject = view
.subjects
.iter()
.find(|subject| subject.token_response.as_ref().unwrap().access_token == access_token);
if let Some(subject) = subject {
(StatusCode::OK, Json(subject.credential_response.clone())).into_response()
} else {
StatusCode::NOT_FOUND.into_response()
}
}
Ok(None) => StatusCode::NOT_FOUND.into_response(),
Err(err) => {
println!("Error: {:#?}\n", err);
(StatusCode::BAD_REQUEST, err.to_string()).into_response()
}
}
}
3 changes: 3 additions & 0 deletions agent_api_rest/src/credential_issuer/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod credential;
pub mod token;
pub mod well_known;
Loading

0 comments on commit 1c8533c

Please sign in to comment.