diff --git a/Cargo.lock b/Cargo.lock index 79046a59..9a809bd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,9 @@ dependencies = [ "futures", "http-api-problem", "hyper 1.4.1", + "identity_core", "identity_credential", + "identity_did", "jsonwebtoken", "lazy_static", "mime", @@ -94,7 +96,7 @@ dependencies = [ "siopv2", "tokio", "tower", - "tower-http 0.5.2", + "tower-http 0.6.1", "tracing", "tracing-test", "url", @@ -127,6 +129,7 @@ version = "0.1.0" dependencies = [ "agent_event_publisher_http", "agent_holder", + "agent_identity", "agent_issuance", "agent_shared", "agent_store", @@ -7790,15 +7793,14 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" dependencies = [ "bitflags 2.5.0", "bytes", "http 1.1.0", "http-body 1.0.0", - "http-body-util", "pin-project-lite", "tower-layer", "tower-service", @@ -7807,9 +7809,9 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" diff --git a/Cargo.toml b/Cargo.toml index 17bc2ae4..0c2a1829 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ identity_credential = { version = "1.3", default-features = false, features = [ "presentation", "domain-linkage", ] } +identity_did = { version = "1.3" } identity_iota = { version = "1.3" } identity_verification = { version = "1.3", default-features = false } jsonwebtoken = "9.3" @@ -53,7 +54,7 @@ serde_yaml = "0.9" thiserror = "1.0" tokio = { version = "1", features = ["full"] } tower = { version = "0.4" } -tower-http = { version = "0.5", features = ["cors", "trace"] } +tower-http = { version = "0.6", features = ["cors", "trace"] } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } tracing-test = { version = "0.2" } diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index 9390adbf..d0445b6d 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -17,7 +17,9 @@ axum-macros = "0.4" did_manager.workspace = true http-api-problem = "0.57" hyper = { version = "1.2" } +identity_core.workspace = true identity_credential.workspace = true +identity_did.workspace = true oid4vc-core.workspace = true oid4vci.workspace = true oid4vp.workspace = true diff --git a/agent_api_rest/postman/ssi-agent.postman_collection.json b/agent_api_rest/postman/ssi-agent.postman_collection.json index 08e9731a..cdef0a25 100644 --- a/agent_api_rest/postman/ssi-agent.postman_collection.json +++ b/agent_api_rest/postman/ssi-agent.postman_collection.json @@ -26,6 +26,16 @@ "type": "text/javascript", "packages": {} } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } } ], "request": { @@ -912,6 +922,13 @@ "type": "text/javascript", "packages": {} } + }, + { + "listen": "prerequest", + "script": { + "exec": [], + "type": "text/javascript" + } } ], "request": { @@ -971,6 +988,13 @@ "type": "text/javascript", "packages": {} } + }, + { + "listen": "prerequest", + "script": { + "exec": [], + "type": "text/javascript" + } } ], "request": { @@ -1043,6 +1067,308 @@ { "name": "Identity", "item": [ + { + "name": "Create a new Connection", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"alias\": \"My Connection\",\n \"domain\": \"http://example.org\",\n \"dids\": [\"did:example:123\"],\n \"credentialOfferEndpoint\": \"{{HOST}}/openid4vci/offers\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/v0/connections", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "connections" + ] + } + }, + "response": [] + }, + { + "name": "List all Connections", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "", + "if (jsonData && typeof jsonData === 'object') {", + " const connectionId = Object.keys(jsonData)[0];", + "", + " if (connectionId) {", + " pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);", + " }", + "}" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/connections", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "connections" + ] + } + }, + "response": [] + }, + { + "name": "Query Connection by Alias", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "", + "if (jsonData && typeof jsonData === 'object') {", + " const connectionId = Object.keys(jsonData)[0];", + "", + " if (connectionId) {", + " pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);", + " }", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/connections?alias=My+Connection", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "connections" + ], + "query": [ + { + "key": "alias", + "value": "My+Connection" + } + ] + } + }, + "response": [] + }, + { + "name": "Query Connection by DID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "", + "if (jsonData && typeof jsonData === 'object') {", + " const connectionId = Object.keys(jsonData)[0];", + "", + " if (connectionId) {", + " pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);", + " }", + "}" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/connections?did=did:example:123", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "connections" + ], + "query": [ + { + "key": "did", + "value": "did:example:123" + } + ] + } + }, + "response": [] + }, + { + "name": "Query Connection by Domain", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "", + "if (jsonData && typeof jsonData === 'object') {", + " const connectionId = Object.keys(jsonData)[0];", + "", + " if (connectionId) {", + " pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);", + " }", + "}" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/connections?domain=http://example.org", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "connections" + ], + "query": [ + { + "key": "domain", + "value": "http://example.org" + } + ] + } + }, + "response": [] + }, + { + "name": "Connection by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "test", + "value": "test-value", + "type": "text" + } + ] + }, + "url": { + "raw": "{{HOST}}/v0/connections/{{CONNECTION_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "connections", + "{{CONNECTION_ID}}" + ] + } + }, + "response": [] + }, { "name": "Create new Linked Verifiable Presentation Service", "request": { @@ -1237,6 +1563,11 @@ "key": "PRESENTATION_ID", "value": "INITIAL_VALUE", "type": "string" + }, + { + "key": "CONNECTION_ID", + "value": "INITIAL_VALUE", + "type": "string" } ] } \ No newline at end of file diff --git a/agent_api_rest/src/identity/connections/mod.rs b/agent_api_rest/src/identity/connections/mod.rs new file mode 100644 index 00000000..81bdda12 --- /dev/null +++ b/agent_api_rest/src/identity/connections/mod.rs @@ -0,0 +1,128 @@ +use crate::API_VERSION; +use agent_identity::{connection::command::ConnectionCommand, state::IdentityState}; +use agent_shared::handlers::{command_handler, query_handler}; +use axum::{ + extract::{Path, State}, + response::{IntoResponse, Response}, + Form, Json, +}; +use hyper::{header, StatusCode}; +use identity_core::common::Url; +use identity_did::DIDUrl; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::collections::HashMap; +use tracing::info; + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PostConnectionsEndpointRequest { + #[serde(default)] + pub alias: Option<String>, + #[serde(default)] + pub domain: Option<Url>, + #[serde(default)] + pub dids: Vec<DIDUrl>, + #[serde(default)] + pub credential_offer_endpoint: Option<Url>, +} + +#[axum_macros::debug_handler] +pub(crate) async fn post_connections( + State(state): State<IdentityState>, + Json(payload): Json<serde_json::Value>, +) -> Response { + // TODO: implement a body consuming extractor that logs the body so that we don't need to log it in each handler. + // This way we can also immediately deserialize the body here into a typed struct instead of deserializing into a + // `serde_json::Value` first. See: + // https://github.com/tokio-rs/axum/blob/main/examples/consume-body-in-extractor-or-middleware/src/main.rs + info!("Request Body: {}", payload); + + let Ok(PostConnectionsEndpointRequest { + alias, + domain, + dids, + credential_offer_endpoint, + }) = serde_json::from_value(payload) + else { + return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); + }; + + let connection_id = uuid::Uuid::new_v4().to_string(); + + let command = ConnectionCommand::AddConnection { + connection_id: connection_id.clone(), + alias, + domain, + dids, + credential_offer_endpoint, + }; + + if command_handler(&connection_id, &state.command.connection, command) + .await + .is_err() + { + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + // Return the connection. + match query_handler(&connection_id, &state.query.connection).await { + Ok(Some(connection)) => ( + StatusCode::CREATED, + [(header::LOCATION, &format!("{API_VERSION}/connections/{connection_id}"))], + Json(connection), + ) + .into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetConnectionsEndpointRequest { + #[serde(default)] + pub alias: Option<String>, + #[serde(default)] + pub domain: Option<Url>, + #[serde(default)] + pub did: Option<DIDUrl>, +} + +#[axum_macros::debug_handler] +pub(crate) async fn get_connections( + State(state): State<IdentityState>, + Form(GetConnectionsEndpointRequest { alias, domain, did }): Form<GetConnectionsEndpointRequest>, +) -> Response { + info!("Request Params - alias: {alias:?}, domain: {domain:?}, did: {did:?}"); + + match query_handler("all_connections", &state.query.all_connections).await { + Ok(Some(all_connections_view)) => { + let filtered_connections: HashMap<_, _> = all_connections_view + .connections + .into_iter() + .filter(|(_, connection)| { + alias + .as_ref() + .map_or(true, |alias| connection.alias.as_ref() == Some(alias)) + && domain + .as_ref() + .map_or(true, |domain| connection.domain.as_ref() == Some(domain)) + && did.as_ref().map_or(true, |did| connection.dids.contains(did)) + }) + .collect(); + + (StatusCode::OK, Json(filtered_connections)).into_response() + } + Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} + +#[axum_macros::debug_handler] +pub(crate) async fn get_connection(State(state): State<IdentityState>, Path(connection_id): Path<String>) -> Response { + match query_handler(&connection_id, &state.query.connection).await { + Ok(Some(connection_view)) => (StatusCode::OK, Json(connection_view)).into_response(), + Ok(None) => StatusCode::NOT_FOUND.into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} diff --git a/agent_api_rest/src/identity/mod.rs b/agent_api_rest/src/identity/mod.rs index c6f6f991..a10a6e28 100644 --- a/agent_api_rest/src/identity/mod.rs +++ b/agent_api_rest/src/identity/mod.rs @@ -1,3 +1,4 @@ +pub mod connections; pub mod services; pub mod well_known; @@ -6,6 +7,7 @@ use axum::{ routing::{get, post}, Router, }; +use connections::{get_connection, get_connections, post_connections}; use services::{linked_vp::linked_vp, service, services}; use well_known::{did::did, did_configuration::did_configuration}; @@ -16,6 +18,8 @@ pub fn router(identity_state: IdentityState) -> Router { .nest( API_VERSION, Router::new() + .route("/connections", get(get_connections).post(post_connections)) + .route("/connections/:connection_id", get(get_connection)) .route("/services", get(services)) .route("/services/:service_id", get(service)) .route("/services/linked-vp", post(linked_vp)), diff --git a/agent_api_rest/src/verification/relying_party/redirect.rs b/agent_api_rest/src/verification/relying_party/redirect.rs index af050e6d..6a13e3ed 100644 --- a/agent_api_rest/src/verification/relying_party/redirect.rs +++ b/agent_api_rest/src/verification/relying_party/redirect.rs @@ -1,7 +1,8 @@ use agent_shared::handlers::{command_handler, query_handler}; use agent_verification::{ - authorization_request::views::AuthorizationRequestView, connection::command::ConnectionCommand, - generic_oid4vc::GenericAuthorizationResponse, state::VerificationState, + authorization_request::{command::AuthorizationRequestCommand, views::AuthorizationRequestView}, + generic_oid4vc::GenericAuthorizationResponse, + state::VerificationState, }; use axum::{ extract::State, @@ -35,17 +36,19 @@ pub(crate) async fn redirect( _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), }; - let connection_id = authorization_request.client_id(); - - let command = ConnectionCommand::VerifyAuthorizationResponse { + let command = AuthorizationRequestCommand::VerifyAuthorizationResponse { authorization_request, authorization_response, }; // Verify the authorization response. - if command_handler(&connection_id, &verification_state.command.connection, command) - .await - .is_err() + if command_handler( + &authorization_request_id, + &verification_state.command.authorization_request, + command, + ) + .await + .is_err() { return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } @@ -153,7 +156,9 @@ pub mod tests { set_config().enable_event_publisher_http(); set_config().set_event_publisher_http_target_url(target_url.clone()); set_config().set_event_publisher_http_target_events(Events { - connection: vec![agent_shared::config::ConnectionEvent::SIOPv2AuthorizationResponseVerified], + authorization_request: vec![ + agent_shared::config::AuthorizationRequestEvent::SIOPv2AuthorizationResponseVerified, + ], ..Default::default() }); diff --git a/agent_application/docker/db/init.sql b/agent_application/docker/db/init.sql index fd70d4df..49be6ff2 100644 --- a/agent_application/docker/db/init.sql +++ b/agent_application/docker/db/init.sql @@ -10,6 +10,23 @@ CREATE TABLE events PRIMARY KEY (aggregate_type, aggregate_id, sequence) ); +CREATE TABLE connection +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + + +CREATE TABLE all_connections +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + CREATE TABLE document ( view_id text NOT NULL, @@ -154,13 +171,5 @@ CREATE TABLE all_authorization_requests PRIMARY KEY (view_id) ); -CREATE TABLE connection -( - view_id text NOT NULL, - version bigint CHECK (version >= 0) NOT NULL, - payload json NOT NULL, - PRIMARY KEY (view_id) -); - CREATE USER demo_user WITH ENCRYPTED PASSWORD 'demo_pass'; GRANT ALL PRIVILEGES ON DATABASE postgres TO demo_user; diff --git a/agent_event_publisher_http/Cargo.toml b/agent_event_publisher_http/Cargo.toml index a9811bf7..63628490 100644 --- a/agent_event_publisher_http/Cargo.toml +++ b/agent_event_publisher_http/Cargo.toml @@ -6,6 +6,7 @@ rust-version.workspace = true [dependencies] agent_holder = { path = "../agent_holder" } +agent_identity = { path = "../agent_identity" } agent_issuance = { path = "../agent_issuance" } agent_shared = { path = "../agent_shared" } agent_store = { path = "../agent_store" } diff --git a/agent_event_publisher_http/README.md b/agent_event_publisher_http/README.md index cdc0a663..c83c7253 100644 --- a/agent_event_publisher_http/README.md +++ b/agent_event_publisher_http/README.md @@ -21,6 +21,12 @@ event_publishers: ### Available events +#### `connection` + +``` +ConnectionAdded +``` + #### `document` ``` @@ -89,11 +95,6 @@ CredentialOfferRejected AuthorizationRequestCreated FormUrlEncodedAuthorizationRequestCreated AuthorizationRequestObjectSigned -``` - -#### `connection` - -``` SIOPv2AuthorizationResponseVerified OID4VPAuthorizationResponseVerified ``` diff --git a/agent_event_publisher_http/src/lib.rs b/agent_event_publisher_http/src/lib.rs index e18be595..0d9b4190 100644 --- a/agent_event_publisher_http/src/lib.rs +++ b/agent_event_publisher_http/src/lib.rs @@ -1,3 +1,4 @@ +use agent_identity::connection::aggregate::Connection; use agent_issuance::{ credential::aggregate::Credential, offer::aggregate::Offer, server_config::aggregate::ServerConfig, }; @@ -6,7 +7,7 @@ use agent_store::{ AuthorizationRequestEventPublisher, ConnectionEventPublisher, CredentialEventPublisher, EventPublisher, HolderCredentialEventPublisher, OfferEventPublisher, ReceivedOfferEventPublisher, ServerConfigEventPublisher, }; -use agent_verification::{authorization_request::aggregate::AuthorizationRequest, connection::aggregate::Connection}; +use agent_verification::authorization_request::aggregate::AuthorizationRequest; use async_trait::async_trait; use cqrs_es::{Aggregate, DomainEvent, EventEnvelope, Query}; use serde::Deserialize; @@ -17,6 +18,9 @@ use tracing::info; #[skip_serializing_none] #[derive(Debug, Deserialize, Default)] pub struct EventPublisherHttp { + // Identity + pub connection: Option<AggregateEventPublisherHttp<Connection>>, + // Issuance pub server_config: Option<AggregateEventPublisherHttp<ServerConfig>>, pub credential: Option<AggregateEventPublisherHttp<Credential>>, @@ -27,7 +31,6 @@ pub struct EventPublisherHttp { pub received_offer: Option<AggregateEventPublisherHttp<agent_holder::offer::aggregate::Offer>>, // Verification - pub connection: Option<AggregateEventPublisherHttp<Connection>>, pub authorization_request: Option<AggregateEventPublisherHttp<AuthorizationRequest>>, } @@ -40,6 +43,18 @@ impl EventPublisherHttp { return Ok(EventPublisherHttp::default()); } + let connection = (!event_publisher_http.events.connection.is_empty()).then(|| { + AggregateEventPublisherHttp::<Connection>::new( + event_publisher_http.target_url.clone(), + event_publisher_http + .events + .connection + .iter() + .map(ToString::to_string) + .collect(), + ) + }); + let server_config = (!event_publisher_http.events.server_config.is_empty()).then(|| { AggregateEventPublisherHttp::<ServerConfig>::new( event_publisher_http.target_url.clone(), @@ -100,18 +115,6 @@ impl EventPublisherHttp { ) }); - let connection = (!event_publisher_http.events.connection.is_empty()).then(|| { - AggregateEventPublisherHttp::<Connection>::new( - event_publisher_http.target_url.clone(), - event_publisher_http - .events - .connection - .iter() - .map(ToString::to_string) - .collect(), - ) - }); - let authorization_request = (!event_publisher_http.events.authorization_request.is_empty()).then(|| { AggregateEventPublisherHttp::<AuthorizationRequest>::new( event_publisher_http.target_url.clone(), @@ -141,6 +144,12 @@ impl EventPublisherHttp { } impl EventPublisher for EventPublisherHttp { + fn connection(&mut self) -> Option<ConnectionEventPublisher> { + self.connection + .take() + .map(|publisher| Box::new(publisher) as ConnectionEventPublisher) + } + fn server_config(&mut self) -> Option<ServerConfigEventPublisher> { self.server_config .take() @@ -171,12 +180,6 @@ impl EventPublisher for EventPublisherHttp { .map(|publisher| Box::new(publisher) as ReceivedOfferEventPublisher) } - fn connection(&mut self) -> Option<ConnectionEventPublisher> { - self.connection - .take() - .map(|publisher| Box::new(publisher) as ConnectionEventPublisher) - } - fn authorization_request(&mut self) -> Option<AuthorizationRequestEventPublisher> { self.authorization_request .take() diff --git a/agent_identity/Cargo.toml b/agent_identity/Cargo.toml index 2436e921..37156612 100644 --- a/agent_identity/Cargo.toml +++ b/agent_identity/Cargo.toml @@ -15,7 +15,7 @@ derivative = "2.2" did_manager.workspace = true identity_credential.workspace = true identity_core.workspace = true -identity_did = { version = "1.3" } +identity_did.workspace = true identity_document = { version = "1.3" } jsonwebtoken.workspace = true oid4vc-core.workspace = true diff --git a/agent_identity/src/connection/README.md b/agent_identity/src/connection/README.md new file mode 100644 index 00000000..2e126ff8 --- /dev/null +++ b/agent_identity/src/connection/README.md @@ -0,0 +1,9 @@ +# Connection + +This aggregate holds everything related to a connection: +- connection_id +- domain +- dids (list of associated DIDs) +- first_interacted +- last_interacted +- credential_offer_endpoint diff --git a/agent_identity/src/connection/aggregate.rs b/agent_identity/src/connection/aggregate.rs new file mode 100644 index 00000000..6ad490e3 --- /dev/null +++ b/agent_identity/src/connection/aggregate.rs @@ -0,0 +1,157 @@ +use async_trait::async_trait; +use cqrs_es::Aggregate; +use identity_core::common::{Timestamp, Url}; +use identity_did::DIDUrl; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tracing::info; + +use crate::services::IdentityServices; + +use super::{command::ConnectionCommand, error::ConnectionError, event::ConnectionEvent}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Connection { + pub connection_id: String, + pub alias: Option<String>, + pub domain: Option<Url>, + pub dids: Vec<DIDUrl>, + pub first_interacted: Option<Timestamp>, + pub last_interacted: Option<Timestamp>, + + // TODO: How do we want to make distinction between issuer, holder, and verifier capabilities of the `Connection`? + pub credential_offer_endpoint: Option<Url>, + // pub issuer_options: Option<IssuerOptions>, + // pub holder_options: Option<HolderOptions>, + // pub verifier_options: Option<VerifierOptions>, +} + +#[async_trait] +impl Aggregate for Connection { + type Command = ConnectionCommand; + type Event = ConnectionEvent; + type Error = ConnectionError; + type Services = Arc<IdentityServices>; + + fn aggregate_type() -> String { + "connection".to_string() + } + + async fn handle( + &self, + command: Self::Command, + _services: &Self::Services, + ) -> Result<Vec<Self::Event>, Self::Error> { + use ConnectionCommand::*; + use ConnectionEvent::*; + + info!("Handling command: {:?}", command); + + match command { + AddConnection { + connection_id, + alias, + domain, + dids, + credential_offer_endpoint, + } => Ok(vec![ConnectionAdded { + connection_id, + alias, + domain, + dids, + credential_offer_endpoint, + }]), + } + } + + fn apply(&mut self, event: Self::Event) { + use ConnectionEvent::*; + + info!("Applying event: {:?}", event); + + match event { + ConnectionAdded { + connection_id, + alias, + domain, + dids, + credential_offer_endpoint, + } => { + self.connection_id = connection_id; + self.alias = alias; + self.domain = domain; + self.dids = dids; + self.credential_offer_endpoint = credential_offer_endpoint; + } + } + } +} + +#[cfg(test)] +pub mod document_tests { + use super::test_utils::*; + use super::*; + use cqrs_es::test::TestFramework; + use rstest::rstest; + + type ConnectionTestFramework = TestFramework<Connection>; + + #[rstest] + #[serial_test::serial] + async fn test_add_connection( + connection_id: String, + alias: String, + domain: Url, + dids: Vec<DIDUrl>, + credential_offer_endpoint: Url, + ) { + ConnectionTestFramework::with(IdentityServices::default()) + .given_no_previous_events() + .when(ConnectionCommand::AddConnection { + connection_id: connection_id.clone(), + alias: Some(alias.clone()), + domain: Some(domain.clone()), + dids: dids.clone(), + credential_offer_endpoint: Some(credential_offer_endpoint.clone()), + }) + .then_expect_events(vec![ConnectionEvent::ConnectionAdded { + connection_id: connection_id.clone(), + alias: Some(alias), + domain: Some(domain.clone()), + dids: dids.clone(), + credential_offer_endpoint: Some(credential_offer_endpoint.clone()), + }]) + } +} + +#[cfg(feature = "test_utils")] +pub mod test_utils { + use identity_core::common::Url; + use identity_did::DIDUrl; + use rstest::fixture; + + #[fixture] + pub fn connection_id() -> String { + "connection_id".to_string() + } + + #[fixture] + pub fn alias() -> String { + "My Connection".to_string() + } + + #[fixture] + pub fn domain() -> Url { + "http://example.org".parse().unwrap() + } + + #[fixture] + pub fn dids() -> Vec<DIDUrl> { + vec!["did:example:123".parse().unwrap()] + } + + #[fixture] + pub fn credential_offer_endpoint() -> Url { + "http://example.org/openid4vci/offers".parse().unwrap() + } +} diff --git a/agent_identity/src/connection/command.rs b/agent_identity/src/connection/command.rs new file mode 100644 index 00000000..d6b0ee26 --- /dev/null +++ b/agent_identity/src/connection/command.rs @@ -0,0 +1,15 @@ +use identity_core::common::Url; +use identity_did::DIDUrl; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum ConnectionCommand { + AddConnection { + connection_id: String, + alias: Option<String>, + domain: Option<Url>, + dids: Vec<DIDUrl>, + credential_offer_endpoint: Option<Url>, + }, +} diff --git a/agent_identity/src/connection/error.rs b/agent_identity/src/connection/error.rs new file mode 100644 index 00000000..fa46229b --- /dev/null +++ b/agent_identity/src/connection/error.rs @@ -0,0 +1,4 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ConnectionError {} diff --git a/agent_verification/src/connection/event.rs b/agent_identity/src/connection/event.rs similarity index 53% rename from agent_verification/src/connection/event.rs rename to agent_identity/src/connection/event.rs index 0f08becc..558ea6f2 100644 --- a/agent_verification/src/connection/event.rs +++ b/agent_identity/src/connection/event.rs @@ -1,10 +1,17 @@ use cqrs_es::DomainEvent; +use identity_core::common::Url; +use identity_did::DIDUrl; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum ConnectionEvent { - SIOPv2AuthorizationResponseVerified { id_token: String, state: Option<String> }, - OID4VPAuthorizationResponseVerified { vp_token: String, state: Option<String> }, + ConnectionAdded { + connection_id: String, + alias: Option<String>, + domain: Option<Url>, + dids: Vec<DIDUrl>, + credential_offer_endpoint: Option<Url>, + }, } impl DomainEvent for ConnectionEvent { @@ -12,8 +19,7 @@ impl DomainEvent for ConnectionEvent { use ConnectionEvent::*; let event_type: &str = match self { - SIOPv2AuthorizationResponseVerified { .. } => "SIOPv2AuthorizationResponseVerified", - OID4VPAuthorizationResponseVerified { .. } => "OID4VPAuthorizationResponseVerified", + ConnectionAdded { .. } => "ConnectionAdded", }; event_type.to_string() } diff --git a/agent_verification/src/connection/mod.rs b/agent_identity/src/connection/mod.rs similarity index 79% rename from agent_verification/src/connection/mod.rs rename to agent_identity/src/connection/mod.rs index 7d8a943f..7cbc4ed7 100644 --- a/agent_verification/src/connection/mod.rs +++ b/agent_identity/src/connection/mod.rs @@ -2,4 +2,4 @@ pub mod aggregate; pub mod command; pub mod error; pub mod event; -pub mod queries; +pub mod views; diff --git a/agent_identity/src/connection/views/all_connections.rs b/agent_identity/src/connection/views/all_connections.rs new file mode 100644 index 00000000..4e623022 --- /dev/null +++ b/agent_identity/src/connection/views/all_connections.rs @@ -0,0 +1,23 @@ +use super::ConnectionView; +use crate::connection::views::Connection; +use cqrs_es::{EventEnvelope, View}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct AllConnectionsView { + #[serde(flatten)] + pub connections: HashMap<String, ConnectionView>, +} + +impl View<Connection> for AllConnectionsView { + fn update(&mut self, event: &EventEnvelope<Connection>) { + self.connections + // Get the entry for the aggregate_id + .entry(event.aggregate_id.clone()) + // or insert a new one if it doesn't exist + .or_default() + // update the view with the event + .update(event); + } +} diff --git a/agent_identity/src/connection/views/mod.rs b/agent_identity/src/connection/views/mod.rs new file mode 100644 index 00000000..c951dd09 --- /dev/null +++ b/agent_identity/src/connection/views/mod.rs @@ -0,0 +1,29 @@ +pub mod all_connections; + +use super::event::ConnectionEvent; +use crate::connection::aggregate::Connection; +use cqrs_es::{EventEnvelope, View}; + +pub type ConnectionView = Connection; + +impl View<Connection> for Connection { + fn update(&mut self, event: &EventEnvelope<Connection>) { + use ConnectionEvent::*; + + match &event.payload { + ConnectionAdded { + connection_id, + alias, + domain, + dids, + credential_offer_endpoint, + } => { + self.connection_id.clone_from(connection_id); + self.alias.clone_from(alias); + self.domain.clone_from(domain); + self.dids.clone_from(dids); + self.credential_offer_endpoint.clone_from(credential_offer_endpoint); + } + } + } +} diff --git a/agent_identity/src/lib.rs b/agent_identity/src/lib.rs index f2de33fd..cdf392f3 100644 --- a/agent_identity/src/lib.rs +++ b/agent_identity/src/lib.rs @@ -1,4 +1,5 @@ // Aggregates +pub mod connection; pub mod document; pub mod service; diff --git a/agent_identity/src/state.rs b/agent_identity/src/state.rs index c6400198..ec3c2f22 100644 --- a/agent_identity/src/state.rs +++ b/agent_identity/src/state.rs @@ -6,6 +6,9 @@ use did_manager::DidMethod; use std::sync::Arc; use tracing::{info, warn}; +use crate::connection::aggregate::Connection; +use crate::connection::views::all_connections::AllConnectionsView; +use crate::connection::views::ConnectionView; use crate::document::command::DocumentCommand; use crate::service::views::all_services::AllServicesView; use crate::{ @@ -22,6 +25,7 @@ pub struct IdentityState { /// The command handlers are used to execute commands on the aggregates. #[derive(Clone)] pub struct CommandHandlers { + pub connection: CommandHandler<Connection>, pub document: CommandHandler<Document>, pub service: CommandHandler<Service>, } @@ -30,17 +34,23 @@ pub struct CommandHandlers { /// that any type of repository that implements the `ViewRepository` trait can be used, but the corresponding `View` and /// `Aggregate` types must be the same. type Queries = ViewRepositories< + dyn ViewRepository<ConnectionView, Connection>, + dyn ViewRepository<AllConnectionsView, Connection>, dyn ViewRepository<DocumentView, Document>, dyn ViewRepository<ServiceView, Service>, dyn ViewRepository<AllServicesView, Service>, >; -pub struct ViewRepositories<D, S1, S2> +pub struct ViewRepositories<C1, C2, D, S1, S2> where + C1: ViewRepository<ConnectionView, Connection> + ?Sized, + C2: ViewRepository<AllConnectionsView, Connection> + ?Sized, D: ViewRepository<DocumentView, Document> + ?Sized, S1: ViewRepository<ServiceView, Service> + ?Sized, S2: ViewRepository<AllServicesView, Service> + ?Sized, { + pub connection: Arc<C1>, + pub all_connections: Arc<C2>, pub document: Arc<D>, pub service: Arc<S1>, pub all_services: Arc<S2>, @@ -49,6 +59,8 @@ where impl Clone for Queries { fn clone(&self) -> Self { ViewRepositories { + connection: self.connection.clone(), + all_connections: self.all_connections.clone(), document: self.document.clone(), service: self.service.clone(), all_services: self.all_services.clone(), diff --git a/agent_shared/src/config.rs b/agent_shared/src/config.rs index 4eb1d2e3..54512b4e 100644 --- a/agent_shared/src/config.rs +++ b/agent_shared/src/config.rs @@ -116,6 +116,8 @@ pub struct EventPublisherHttp { #[derive(Debug, Deserialize, Clone, Default)] pub struct Events { + #[serde(default)] + pub connection: Vec<ConnectionEvent>, #[serde(default)] pub document: Vec<DocumentEvent>, #[serde(default)] @@ -131,11 +133,14 @@ pub struct Events { #[serde(default)] pub received_offer: Vec<ReceivedOfferEvent>, #[serde(default)] - pub connection: Vec<ConnectionEvent>, - #[serde(default)] pub authorization_request: Vec<AuthorizationRequestEvent>, } +#[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] +pub enum ConnectionEvent { + ConnectionAdded, +} + #[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] pub enum DocumentEvent { DocumentCreated, @@ -185,17 +190,13 @@ pub enum ReceivedOfferEvent { CredentialOfferRejected, } -#[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] -pub enum ConnectionEvent { - SIOPv2AuthorizationResponseVerified, - OID4VPAuthorizationResponseVerified, -} - #[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] pub enum AuthorizationRequestEvent { AuthorizationRequestCreated, FormUrlEncodedAuthorizationRequestCreated, AuthorizationRequestObjectSigned, + SIOPv2AuthorizationResponseVerified, + OID4VPAuthorizationResponseVerified, } /// All DID methods supported by UniCore diff --git a/agent_store/src/in_memory.rs b/agent_store/src/in_memory.rs index 027dfe4f..6d90575b 100644 --- a/agent_store/src/in_memory.rs +++ b/agent_store/src/in_memory.rs @@ -121,15 +121,18 @@ pub async fn identity_state( event_publishers: Vec<Box<dyn EventPublisher>>, ) -> IdentityState { // Initialize the in-memory repositories. + let connection = Arc::new(MemRepository::default()); + let all_connections = Arc::new(MemRepository::default()); let document = Arc::new(MemRepository::default()); let service = Arc::new(MemRepository::default()); let all_services = Arc::new(MemRepository::default()); - // Create custom-queries for the offer aggregate. + let all_connections_query = ListAllQuery::new(all_connections.clone(), "all_connections"); let all_services_query = ListAllQuery::new(all_services.clone(), "all_services"); // Partition the event_publishers into the different aggregates. let Partitions { + connection_event_publishers, document_event_publishers, service_event_publishers, .. @@ -137,6 +140,15 @@ pub async fn identity_state( IdentityState { command: agent_identity::state::CommandHandlers { + connection: Arc::new( + connection_event_publishers.into_iter().fold( + AggregateHandler::new(identity_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(connection.clone())) + .append_query(all_connections_query), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), document: Arc::new( document_event_publishers.into_iter().fold( AggregateHandler::new(identity_services.clone()) @@ -156,6 +168,8 @@ pub async fn identity_state( ), }, query: agent_identity::state::ViewRepositories { + connection, + all_connections, document, service, all_services, @@ -307,7 +321,6 @@ pub async fn verification_state( // Initialize the in-memory repositories. let authorization_request = Arc::new(MemRepository::default()); let all_authorization_requests = Arc::new(MemRepository::default()); - let connection = Arc::new(MemRepository::default()); // Create custom-queries for the offer aggregate. let all_authorization_requests_query = @@ -316,7 +329,6 @@ pub async fn verification_state( // Partition the event_publishers into the different aggregates. let Partitions { authorization_request_event_publishers, - connection_event_publishers, .. } = partition_event_publishers(event_publishers); @@ -331,19 +343,10 @@ pub async fn verification_state( |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), - connection: Arc::new( - connection_event_publishers.into_iter().fold( - AggregateHandler::new(verification_services) - .append_query(SimpleLoggingQuery {}) - .append_query(generic_query(connection.clone())), - |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), - ), - ), }, query: agent_verification::state::ViewRepositories { authorization_request, all_authorization_requests, - connection, }, } } diff --git a/agent_store/src/lib.rs b/agent_store/src/lib.rs index 530dfcbb..2f158491 100644 --- a/agent_store/src/lib.rs +++ b/agent_store/src/lib.rs @@ -1,13 +1,14 @@ -use agent_identity::{document::aggregate::Document, service::aggregate::Service}; +use agent_identity::{connection::aggregate::Connection, document::aggregate::Document, service::aggregate::Service}; use agent_issuance::{ credential::aggregate::Credential, offer::aggregate::Offer, server_config::aggregate::ServerConfig, }; -use agent_verification::{authorization_request::aggregate::AuthorizationRequest, connection::aggregate::Connection}; +use agent_verification::authorization_request::aggregate::AuthorizationRequest; use cqrs_es::Query; pub mod in_memory; pub mod postgres; +pub type ConnectionEventPublisher = Box<dyn Query<Connection>>; pub type DocumentEventPublisher = Box<dyn Query<Document>>; pub type ServiceEventPublisher = Box<dyn Query<Service>>; pub type ServerConfigEventPublisher = Box<dyn Query<ServerConfig>>; @@ -17,11 +18,11 @@ pub type HolderCredentialEventPublisher = Box<dyn Query<agent_holder::credential pub type PresentationEventPublisher = Box<dyn Query<agent_holder::presentation::aggregate::Presentation>>; pub type ReceivedOfferEventPublisher = Box<dyn Query<agent_holder::offer::aggregate::Offer>>; pub type AuthorizationRequestEventPublisher = Box<dyn Query<AuthorizationRequest>>; -pub type ConnectionEventPublisher = Box<dyn Query<Connection>>; /// Contains all the event_publishers for each aggregate. #[derive(Default)] pub struct Partitions { + pub connection_event_publishers: Vec<ConnectionEventPublisher>, pub document_event_publishers: Vec<DocumentEventPublisher>, pub service_event_publishers: Vec<ServiceEventPublisher>, pub server_config_event_publishers: Vec<ServerConfigEventPublisher>, @@ -31,7 +32,6 @@ pub struct Partitions { pub presentation_event_publishers: Vec<PresentationEventPublisher>, pub received_offer_event_publishers: Vec<ReceivedOfferEventPublisher>, pub authorization_request_event_publishers: Vec<AuthorizationRequestEventPublisher>, - pub connection_event_publishers: Vec<ConnectionEventPublisher>, } /// An outbound event_publisher is a component that listens to events and dispatches them to the appropriate service. For each @@ -39,6 +39,9 @@ pub struct Partitions { /// `Some` with the appropriate query. // TODO: move this to a separate crate that will include all the logic for event_publishers, i.e. `agent_event_publisher`. pub trait EventPublisher { + fn connection(&mut self) -> Option<ConnectionEventPublisher> { + None + } fn document(&mut self) -> Option<DocumentEventPublisher> { None } @@ -66,9 +69,6 @@ pub trait EventPublisher { None } - fn connection(&mut self) -> Option<ConnectionEventPublisher> { - None - } fn authorization_request(&mut self) -> Option<AuthorizationRequestEventPublisher> { None } @@ -78,6 +78,9 @@ pub(crate) fn partition_event_publishers(event_publishers: Vec<Box<dyn EventPubl event_publishers .into_iter() .fold(Partitions::default(), |mut partitions, mut event_publisher| { + if let Some(connection) = event_publisher.connection() { + partitions.connection_event_publishers.push(connection); + } if let Some(document) = event_publisher.document() { partitions.document_event_publishers.push(document); } @@ -110,9 +113,6 @@ pub(crate) fn partition_event_publishers(event_publishers: Vec<Box<dyn EventPubl .authorization_request_event_publishers .push(authorization_request); } - if let Some(connection) = event_publisher.connection() { - partitions.connection_event_publishers.push(connection); - } partitions }) } @@ -170,6 +170,7 @@ mod test { vec![Box::new(FooEventPublisher), Box::new(BarEventPublisher)]; let Partitions { + connection_event_publishers, document_event_publishers, service_event_publishers, server_config_event_publishers, @@ -179,9 +180,9 @@ mod test { presentation_event_publishers, received_offer_event_publishers, authorization_request_event_publishers, - connection_event_publishers, } = partition_event_publishers(event_publishers); + assert_eq!(connection_event_publishers.len(), 2); assert_eq!(document_event_publishers.len(), 0); assert_eq!(service_event_publishers.len(), 0); assert_eq!(server_config_event_publishers.len(), 1); @@ -191,6 +192,5 @@ mod test { assert_eq!(presentation_event_publishers.len(), 0); assert_eq!(received_offer_event_publishers.len(), 0); assert_eq!(authorization_request_event_publishers.len(), 0); - assert_eq!(connection_event_publishers.len(), 2); } } diff --git a/agent_store/src/postgres.rs b/agent_store/src/postgres.rs index 06d987b3..0dbfe145 100644 --- a/agent_store/src/postgres.rs +++ b/agent_store/src/postgres.rs @@ -76,15 +76,18 @@ pub async fn identity_state( let pool = default_postgress_pool(&connection_string).await; // Initialize the postgres repositories. + let connection = Arc::new(PostgresViewRepository::new("connection", pool.clone())); + let all_connections = Arc::new(PostgresViewRepository::new("all_connections", pool.clone())); let document = Arc::new(PostgresViewRepository::new("document", pool.clone())); let service = Arc::new(PostgresViewRepository::new("service", pool.clone())); let all_services = Arc::new(PostgresViewRepository::new("all_services", pool.clone())); - // Create custom-queries for the offer aggregate. + let all_connections_query = ListAllQuery::new(all_connections.clone(), "all_connections"); let all_services_query = ListAllQuery::new(all_services.clone(), "all_services"); // Partition the event_publishers into the different aggregates. let Partitions { + connection_event_publishers, document_event_publishers, service_event_publishers, .. @@ -92,6 +95,15 @@ pub async fn identity_state( IdentityState { command: agent_identity::state::CommandHandlers { + connection: Arc::new( + connection_event_publishers.into_iter().fold( + AggregateHandler::new(pool.clone(), identity_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(connection.clone())) + .append_query(all_connections_query), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), document: Arc::new( document_event_publishers.into_iter().fold( AggregateHandler::new(pool.clone(), identity_services.clone()) @@ -111,6 +123,8 @@ pub async fn identity_state( ), }, query: agent_identity::state::ViewRepositories { + connection, + all_connections, document, service, all_services, @@ -280,7 +294,6 @@ pub async fn verification_state( // Initialize the postgres repositories. let authorization_request = Arc::new(PostgresViewRepository::new("authorization_request", pool.clone())); let all_authorization_requests = Arc::new(PostgresViewRepository::new("all_authorization_requests", pool.clone())); - let connection = Arc::new(PostgresViewRepository::new("connection", pool.clone())); // Create custom-queries for the offer aggregate. let all_authorization_requests_query = @@ -289,7 +302,6 @@ pub async fn verification_state( // Partition the event_publishers into the different aggregates. let Partitions { authorization_request_event_publishers, - connection_event_publishers, .. } = partition_event_publishers(event_publishers); @@ -304,19 +316,10 @@ pub async fn verification_state( |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), - connection: Arc::new( - connection_event_publishers.into_iter().fold( - AggregateHandler::new(pool, verification_services) - .append_query(SimpleLoggingQuery {}) - .append_query(generic_query(connection.clone())), - |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), - ), - ), }, query: agent_verification::state::ViewRepositories { authorization_request, all_authorization_requests, - connection, }, } } diff --git a/agent_verification/src/authorization_request/README.md b/agent_verification/src/authorization_request/README.md new file mode 100644 index 00000000..6de0ade7 --- /dev/null +++ b/agent_verification/src/authorization_request/README.md @@ -0,0 +1,9 @@ +# Authorization Request + +This aggregate holds everything related to an Authorization Request: +- authorization_request +- form_url_encoded_authorization_request +- signed_authorization_request_object +- id_token +- vp_token +- state diff --git a/agent_verification/src/authorization_request/aggregate.rs b/agent_verification/src/authorization_request/aggregate.rs index 76d5cb3d..9d2ea720 100644 --- a/agent_verification/src/authorization_request/aggregate.rs +++ b/agent_verification/src/authorization_request/aggregate.rs @@ -1,13 +1,16 @@ use super::{command::AuthorizationRequestCommand, error::AuthorizationRequestError, event::AuthorizationRequestEvent}; use crate::{ - generic_oid4vc::{GenericAuthorizationRequest, OID4VPAuthorizationRequest, SIOPv2AuthorizationRequest}, + generic_oid4vc::{ + GenericAuthorizationRequest, GenericAuthorizationResponse, OID4VPAuthorizationRequest, + SIOPv2AuthorizationRequest, + }, services::VerificationServices, }; use agent_shared::config::{config, get_preferred_signing_algorithm}; use async_trait::async_trait; use cqrs_es::Aggregate; use oid4vc_core::{authorization_request::ByReference, scope::Scope}; -use oid4vp::authorization_request::ClientIdScheme; +use oid4vp::{authorization_request::ClientIdScheme, Oid4vpParams}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tracing::info; @@ -17,6 +20,9 @@ pub struct AuthorizationRequest { pub authorization_request: Option<GenericAuthorizationRequest>, pub form_url_encoded_authorization_request: Option<String>, pub signed_authorization_request_object: Option<String>, + pub id_token: Option<String>, + pub vp_token: Option<String>, + pub state: Option<String>, } #[async_trait] @@ -127,6 +133,45 @@ impl Aggregate for AuthorizationRequest { signed_authorization_request_object, }]) } + VerifyAuthorizationResponse { + // TODO: use this once `RelyingPartyManager` uses the official SIOPv2 validation logic. + authorization_request: _, + authorization_response, + } => { + let relying_party = &services.relying_party; + + match authorization_response { + GenericAuthorizationResponse::SIOPv2(authorization_response) => { + let _ = relying_party + .validate_response(&authorization_response) + .await + .map_err(InvalidSIOPv2AuthorizationResponse)?; + + let id_token = authorization_response.extension.id_token.clone(); + + Ok(vec![SIOPv2AuthorizationResponseVerified { + id_token, + state: authorization_response.state, + }]) + } + GenericAuthorizationResponse::OID4VP(oid4vp_authorization_response) => { + let _ = relying_party + .validate_response(&oid4vp_authorization_response) + .await + .map_err(InvalidOID4VPAuthorizationResponse)?; + + let vp_token = match oid4vp_authorization_response.extension.oid4vp_parameters { + Oid4vpParams::Params { vp_token, .. } => vp_token, + Oid4vpParams::Jwt { .. } => return Err(UnsupportedJwtParameterError), + }; + + Ok(vec![OID4VPAuthorizationResponseVerified { + vp_token, + state: oid4vp_authorization_response.state, + }]) + } + } + } } } @@ -151,6 +196,14 @@ impl Aggregate for AuthorizationRequest { self.signed_authorization_request_object .replace(signed_authorization_request_object); } + SIOPv2AuthorizationResponseVerified { id_token, state } => { + self.id_token.replace(id_token); + self.state = state; + } + OID4VPAuthorizationResponseVerified { vp_token, state } => { + self.vp_token.replace(vp_token); + self.state = state; + } } } } @@ -165,10 +218,16 @@ pub mod tests { use agent_shared::config::set_config; use agent_shared::config::SupportedDidMethod; use cqrs_es::test::TestFramework; + use identity_credential::credential::Jwt; + use identity_credential::presentation::Presentation; use jsonwebtoken::Algorithm; use lazy_static::lazy_static; use oid4vc_core::Subject as _; use oid4vc_core::{client_metadata::ClientMetadataResource, SubjectSyntaxType}; + use oid4vc_manager::managers::presentation::create_presentation_submission; + use oid4vc_manager::ProviderManager; + use oid4vci::VerifiableCredentialJwt; + use oid4vp::oid4vp::AuthorizationResponseInput; use oid4vp::PresentationDefinition; use rstest::rstest; use serde_json::json; @@ -255,6 +314,145 @@ pub mod tests { }]); } + #[rstest] + #[serial_test::serial] + async fn test_verify_authorization_response( + // "id_token" represents the `SIOPv2` flow, and "vp_token" represents the `OID4VP` flow. + #[values("id_token", "vp_token")] response_type: &str, + // TODO: add `did:web`, check for other tests as well. Probably should be moved to E2E test. + #[values(SupportedDidMethod::Key, SupportedDidMethod::Jwk, SupportedDidMethod::IotaRms)] + verifier_did_method: SupportedDidMethod, + #[values(SupportedDidMethod::Key, SupportedDidMethod::Jwk, SupportedDidMethod::IotaRms)] + provider_did_method: SupportedDidMethod, + ) { + set_config().set_preferred_did_method(verifier_did_method.clone()); + + let verification_services = VerificationServices::default(); + let siopv2_client_metadata = verification_services.siopv2_client_metadata.clone(); + let oid4vp_client_metadata = verification_services.oid4vp_client_metadata.clone(); + + let authorization_request = authorization_request( + response_type, + &verifier_did_method.to_string(), + siopv2_client_metadata, + oid4vp_client_metadata, + ) + .await; + + let authorization_response = + authorization_response(&provider_did_method.to_string(), &authorization_request).await; + let token = authorization_response.token(); + + AuthorizationRequestTestFramework::with(verification_services) + .given_no_previous_events() + .when(AuthorizationRequestCommand::VerifyAuthorizationResponse { + authorization_request, + authorization_response, + }) + .then_expect_events(vec![match response_type { + "id_token" => AuthorizationRequestEvent::SIOPv2AuthorizationResponseVerified { + id_token: token, + state: Some("state".to_string()), + }, + "vp_token" => AuthorizationRequestEvent::OID4VPAuthorizationResponseVerified { + vp_token: token, + state: Some("state".to_string()), + }, + _ => unreachable!("Invalid response type."), + }]); + } + + async fn authorization_response( + did_method: &str, + authorization_request: &GenericAuthorizationRequest, + ) -> GenericAuthorizationResponse { + let provider_manager = ProviderManager::new( + Arc::new(futures::executor::block_on(async { + Subject { + secret_manager: Arc::new(tokio::sync::Mutex::new(secret_manager().await)), + } + })), + vec![did_method], + vec![Algorithm::EdDSA], + ) + .unwrap(); + + let default_did_method = provider_manager.default_subject_syntax_types()[0].to_string(); + + match authorization_request { + GenericAuthorizationRequest::SIOPv2(siopv2_authorization_request) => GenericAuthorizationResponse::SIOPv2( + provider_manager + .generate_response(siopv2_authorization_request, Default::default()) + .await + .unwrap(), + ), + GenericAuthorizationRequest::OID4VP(oid4vp_authorization_request) => { + // TODO: implement test fixture for subject and issuer instead of using the same did as verifier. + // Fixtures can be implemented using the `rstest` crate as described here: https://docs.rs/rstest/latest/rstest/attr.fixture.html + let issuer_did = verifier_did(&default_did_method).await; + let subject_did = issuer_did.clone(); + + // Create a new verifiable credential. + let verifiable_credential = VerifiableCredentialJwt::builder() + .sub(&subject_did) + .iss(&issuer_did) + .iat(0) + .exp(9999999999i64) + .verifiable_credential(serde_json::json!({ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "TestCredential" + ], + "issuanceDate": "2022-01-01T00:00:00Z", + "issuer": issuer_did, + "credentialSubject": { + "id": subject_did, + "givenName": "Ferris", + "familyName": "Crabman", + "email": "ferris.crabman@crabmail.com", + "birthdate": "1985-05-21" + } + })) + .build() + .unwrap(); + + // Encode the verifiable credential as a JWT. + let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkI3o2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCIsInN1YiI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlRlc3RDcmVkZW50aWFsIl0sImlzc3VhbmNlRGF0ZSI6IjIwMjItMDEtMDFUMDA6MDA6MDBaIiwiaXNzdWVyIjoiZGlkOmtleTp6Nk1raWlleW9MTVNWc0pBWnY3SmplNXdXU2tERXltVWdreUY4a2JjcmpacFgzcWQiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZ2l2ZW5OYW1lIjoiRmVycmlzIiwiZmFtaWx5TmFtZSI6IkNyYWJtYW4iLCJlbWFpbCI6ImZlcnJpcy5jcmFibWFuQGNyYWJtYWlsLmNvbSIsImJpcnRoZGF0ZSI6IjE5ODUtMDUtMjEifX19.6guSHngBj_QQYom3kXKmxKrHExoyW1eObBsBg8ACYn-H30YD6eub56zsWnnMzw8IznGDYAguuo3V1D37-A_vCQ".to_string(); + + // Create presentation submission using the presentation definition and the verifiable credential. + let presentation_submission = create_presentation_submission( + &PRESENTATION_DEFINITION, + &[serde_json::to_value(&verifiable_credential).unwrap()], + ) + .unwrap(); + + // Create a verifiable presentation using the JWT. + let verifiable_presentation = + Presentation::builder(subject_did.parse().unwrap(), identity_core::common::Object::new()) + .credential(Jwt::from(jwt)) + .build() + .unwrap(); + + GenericAuthorizationResponse::OID4VP( + provider_manager + .generate_response( + oid4vp_authorization_request, + AuthorizationResponseInput { + verifiable_presentation, + presentation_submission, + }, + ) + .await + .unwrap(), + ) + } + } + } + pub async fn verifier_did(did_method: &str) -> String { VERIFIER.identifier(did_method, Algorithm::EdDSA).await.unwrap() } diff --git a/agent_verification/src/authorization_request/command.rs b/agent_verification/src/authorization_request/command.rs index c48ab99b..0601c8b2 100644 --- a/agent_verification/src/authorization_request/command.rs +++ b/agent_verification/src/authorization_request/command.rs @@ -1,6 +1,8 @@ use oid4vp::PresentationDefinition; use serde::Deserialize; +use crate::generic_oid4vc::{GenericAuthorizationRequest, GenericAuthorizationResponse}; + #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum AuthorizationRequestCommand { @@ -10,4 +12,8 @@ pub enum AuthorizationRequestCommand { presentation_definition: Option<PresentationDefinition>, }, SignAuthorizationRequestObject, + VerifyAuthorizationResponse { + authorization_request: GenericAuthorizationRequest, + authorization_response: GenericAuthorizationResponse, + }, } diff --git a/agent_verification/src/authorization_request/error.rs b/agent_verification/src/authorization_request/error.rs index af2077aa..1f8ac2c9 100644 --- a/agent_verification/src/authorization_request/error.rs +++ b/agent_verification/src/authorization_request/error.rs @@ -8,4 +8,10 @@ pub enum AuthorizationRequestError { MissingAuthorizationRequest, #[error("Failed to sign authorization request: {0}")] AuthorizationRequestSigningError(#[source] anyhow::Error), + #[error("Invalid SIOPv2 authorization response: {0}")] + InvalidSIOPv2AuthorizationResponse(#[source] anyhow::Error), + #[error("Invalid OID4VP authorization response: {0}")] + InvalidOID4VPAuthorizationResponse(#[source] anyhow::Error), + #[error("`jwt` parameter is not supported yet")] + UnsupportedJwtParameterError, } diff --git a/agent_verification/src/authorization_request/event.rs b/agent_verification/src/authorization_request/event.rs index 2d36fee5..2f66ec4e 100644 --- a/agent_verification/src/authorization_request/event.rs +++ b/agent_verification/src/authorization_request/event.rs @@ -13,6 +13,14 @@ pub enum AuthorizationRequestEvent { AuthorizationRequestObjectSigned { signed_authorization_request_object: String, }, + SIOPv2AuthorizationResponseVerified { + id_token: String, + state: Option<String>, + }, + OID4VPAuthorizationResponseVerified { + vp_token: String, + state: Option<String>, + }, } impl DomainEvent for AuthorizationRequestEvent { @@ -23,6 +31,8 @@ impl DomainEvent for AuthorizationRequestEvent { AuthorizationRequestCreated { .. } => "AuthorizationRequestCreated", FormUrlEncodedAuthorizationRequestCreated { .. } => "FormUrlEncodedAuthorizationRequestCreated", AuthorizationRequestObjectSigned { .. } => "AuthorizationRequestObjectSigned", + SIOPv2AuthorizationResponseVerified { .. } => "SIOPv2AuthorizationResponseVerified", + OID4VPAuthorizationResponseVerified { .. } => "OID4VPAuthorizationResponseVerified", }; event_type.to_string() } diff --git a/agent_verification/src/authorization_request/views/mod.rs b/agent_verification/src/authorization_request/views/mod.rs index 055757a3..10e258b2 100644 --- a/agent_verification/src/authorization_request/views/mod.rs +++ b/agent_verification/src/authorization_request/views/mod.rs @@ -25,6 +25,14 @@ impl View<AuthorizationRequest> for AuthorizationRequest { self.signed_authorization_request_object .replace(signed_authorization_request_object.clone()); } + SIOPv2AuthorizationResponseVerified { id_token, state } => { + self.id_token.replace(id_token.clone()); + self.state.clone_from(state); + } + OID4VPAuthorizationResponseVerified { vp_token, state } => { + self.vp_token.replace(vp_token.clone()); + self.state.clone_from(state); + } } } } diff --git a/agent_verification/src/connection/aggregate.rs b/agent_verification/src/connection/aggregate.rs deleted file mode 100644 index 3a37b438..00000000 --- a/agent_verification/src/connection/aggregate.rs +++ /dev/null @@ -1,264 +0,0 @@ -use super::{command::ConnectionCommand, error::ConnectionError, event::ConnectionEvent}; -use crate::{generic_oid4vc::GenericAuthorizationResponse, services::VerificationServices}; -use async_trait::async_trait; -use cqrs_es::Aggregate; -use oid4vp::Oid4vpParams; -use serde::{Deserialize, Serialize}; -use std::{sync::Arc, vec}; -use tracing::info; - -#[derive(Debug, Serialize, Deserialize, Default)] -pub struct Connection { - // TODO: Does user data need to be stored in UniCore at all? - id_token: Option<String>, - vp_token: Option<String>, - state: Option<String>, -} - -#[async_trait] -impl Aggregate for Connection { - type Command = ConnectionCommand; - type Event = ConnectionEvent; - type Error = ConnectionError; - type Services = Arc<VerificationServices>; - - fn aggregate_type() -> String { - "connection".to_string() - } - - async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result<Vec<Self::Event>, Self::Error> { - use ConnectionCommand::*; - use ConnectionError::*; - use ConnectionEvent::*; - - info!("Handling command: {:?}", command); - - match command { - VerifyAuthorizationResponse { - // TODO: use this once `RelyingPartyManager` uses the official SIOPv2 validation logic. - authorization_request: _, - authorization_response, - } => { - let relying_party = &services.relying_party; - - match authorization_response { - GenericAuthorizationResponse::SIOPv2(authorization_response) => { - let _ = relying_party - .validate_response(&authorization_response) - .await - .map_err(InvalidSIOPv2AuthorizationResponse)?; - - let id_token = authorization_response.extension.id_token.clone(); - - Ok(vec![SIOPv2AuthorizationResponseVerified { - id_token, - state: authorization_response.state, - }]) - } - GenericAuthorizationResponse::OID4VP(oid4vp_authorization_response) => { - let _ = relying_party - .validate_response(&oid4vp_authorization_response) - .await - .map_err(InvalidOID4VPAuthorizationResponse)?; - - let vp_token = match oid4vp_authorization_response.extension.oid4vp_parameters { - Oid4vpParams::Params { vp_token, .. } => vp_token, - Oid4vpParams::Jwt { .. } => return Err(UnsupportedJwtParameterError), - }; - - Ok(vec![OID4VPAuthorizationResponseVerified { - vp_token, - state: oid4vp_authorization_response.state, - }]) - } - } - } - } - } - - fn apply(&mut self, event: Self::Event) { - use ConnectionEvent::*; - - info!("Applying event: {:?}", event); - - match event { - SIOPv2AuthorizationResponseVerified { id_token, state } => { - self.id_token.replace(id_token); - self.state = state; - } - OID4VPAuthorizationResponseVerified { vp_token, state } => { - self.vp_token.replace(vp_token); - self.state = state; - } - } - } -} - -#[cfg(test)] -pub mod tests { - use std::sync::Arc; - - use agent_secret_manager::secret_manager; - use agent_secret_manager::subject::Subject; - use agent_shared::config::SupportedDidMethod; - use cqrs_es::test::TestFramework; - use identity_credential::credential::Jwt; - use identity_credential::presentation::Presentation; - - use agent_shared::config::set_config; - use jsonwebtoken::Algorithm; - use oid4vc_manager::managers::presentation::create_presentation_submission; - use oid4vc_manager::ProviderManager; - use oid4vci::VerifiableCredentialJwt; - use oid4vp::oid4vp::AuthorizationResponseInput; - use rstest::rstest; - - use crate::authorization_request::aggregate::tests::{ - authorization_request, verifier_did, PRESENTATION_DEFINITION, - }; - use crate::generic_oid4vc::GenericAuthorizationRequest; - use agent_secret_manager::service::Service as _; - - use super::*; - - type ConnectionTestFramework = TestFramework<Connection>; - - #[rstest] - #[serial_test::serial] - async fn test_verify_authorization_response( - // "id_token" represents the `SIOPv2` flow, and "vp_token" represents the `OID4VP` flow. - #[values("id_token", "vp_token")] response_type: &str, - // TODO: add `did:web`, check for other tests as well. Probably should be moved to E2E test. - #[values(SupportedDidMethod::Key, SupportedDidMethod::Jwk, SupportedDidMethod::IotaRms)] - verifier_did_method: SupportedDidMethod, - #[values(SupportedDidMethod::Key, SupportedDidMethod::Jwk, SupportedDidMethod::IotaRms)] - provider_did_method: SupportedDidMethod, - ) { - set_config().set_preferred_did_method(verifier_did_method.clone()); - - let verification_services = VerificationServices::default(); - let siopv2_client_metadata = verification_services.siopv2_client_metadata.clone(); - let oid4vp_client_metadata = verification_services.oid4vp_client_metadata.clone(); - - let authorization_request = authorization_request( - response_type, - &verifier_did_method.to_string(), - siopv2_client_metadata, - oid4vp_client_metadata, - ) - .await; - - let authorization_response = - authorization_response(&provider_did_method.to_string(), &authorization_request).await; - let token = authorization_response.token(); - - ConnectionTestFramework::with(verification_services) - .given_no_previous_events() - .when(ConnectionCommand::VerifyAuthorizationResponse { - authorization_request, - authorization_response, - }) - .then_expect_events(vec![match response_type { - "id_token" => ConnectionEvent::SIOPv2AuthorizationResponseVerified { - id_token: token, - state: Some("state".to_string()), - }, - "vp_token" => ConnectionEvent::OID4VPAuthorizationResponseVerified { - vp_token: token, - state: Some("state".to_string()), - }, - _ => unreachable!("Invalid response type."), - }]); - } - - async fn authorization_response( - did_method: &str, - authorization_request: &GenericAuthorizationRequest, - ) -> GenericAuthorizationResponse { - let provider_manager = ProviderManager::new( - Arc::new(futures::executor::block_on(async { - Subject { - secret_manager: Arc::new(tokio::sync::Mutex::new(secret_manager().await)), - } - })), - vec![did_method], - vec![Algorithm::EdDSA], - ) - .unwrap(); - - let default_did_method = provider_manager.default_subject_syntax_types()[0].to_string(); - - match authorization_request { - GenericAuthorizationRequest::SIOPv2(siopv2_authorization_request) => GenericAuthorizationResponse::SIOPv2( - provider_manager - .generate_response(siopv2_authorization_request, Default::default()) - .await - .unwrap(), - ), - GenericAuthorizationRequest::OID4VP(oid4vp_authorization_request) => { - // TODO: implement test fixture for subject and issuer instead of using the same did as verifier. - // Fixtures can be implemented using the `rstest` crate as described here: https://docs.rs/rstest/latest/rstest/attr.fixture.html - let issuer_did = verifier_did(&default_did_method).await; - let subject_did = issuer_did.clone(); - - // Create a new verifiable credential. - let verifiable_credential = VerifiableCredentialJwt::builder() - .sub(&subject_did) - .iss(&issuer_did) - .iat(0) - .exp(9999999999i64) - .verifiable_credential(serde_json::json!({ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "type": [ - "VerifiableCredential", - "TestCredential" - ], - "issuanceDate": "2022-01-01T00:00:00Z", - "issuer": issuer_did, - "credentialSubject": { - "id": subject_did, - "givenName": "Ferris", - "familyName": "Crabman", - "email": "ferris.crabman@crabmail.com", - "birthdate": "1985-05-21" - } - })) - .build() - .unwrap(); - - // Encode the verifiable credential as a JWT. - let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkI3o2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCIsInN1YiI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlRlc3RDcmVkZW50aWFsIl0sImlzc3VhbmNlRGF0ZSI6IjIwMjItMDEtMDFUMDA6MDA6MDBaIiwiaXNzdWVyIjoiZGlkOmtleTp6Nk1raWlleW9MTVNWc0pBWnY3SmplNXdXU2tERXltVWdreUY4a2JjcmpacFgzcWQiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZ2l2ZW5OYW1lIjoiRmVycmlzIiwiZmFtaWx5TmFtZSI6IkNyYWJtYW4iLCJlbWFpbCI6ImZlcnJpcy5jcmFibWFuQGNyYWJtYWlsLmNvbSIsImJpcnRoZGF0ZSI6IjE5ODUtMDUtMjEifX19.6guSHngBj_QQYom3kXKmxKrHExoyW1eObBsBg8ACYn-H30YD6eub56zsWnnMzw8IznGDYAguuo3V1D37-A_vCQ".to_string(); - - // Create presentation submission using the presentation definition and the verifiable credential. - let presentation_submission = create_presentation_submission( - &PRESENTATION_DEFINITION, - &[serde_json::to_value(&verifiable_credential).unwrap()], - ) - .unwrap(); - - // Create a verifiable presentation using the JWT. - let verifiable_presentation = - Presentation::builder(subject_did.parse().unwrap(), identity_core::common::Object::new()) - .credential(Jwt::from(jwt)) - .build() - .unwrap(); - - GenericAuthorizationResponse::OID4VP( - provider_manager - .generate_response( - oid4vp_authorization_request, - AuthorizationResponseInput { - verifiable_presentation, - presentation_submission, - }, - ) - .await - .unwrap(), - ) - } - } - } -} diff --git a/agent_verification/src/connection/command.rs b/agent_verification/src/connection/command.rs deleted file mode 100644 index 07a590e2..00000000 --- a/agent_verification/src/connection/command.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::generic_oid4vc::{GenericAuthorizationRequest, GenericAuthorizationResponse}; -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -pub enum ConnectionCommand { - VerifyAuthorizationResponse { - authorization_request: GenericAuthorizationRequest, - authorization_response: GenericAuthorizationResponse, - }, -} diff --git a/agent_verification/src/connection/error.rs b/agent_verification/src/connection/error.rs deleted file mode 100644 index 9ec29a54..00000000 --- a/agent_verification/src/connection/error.rs +++ /dev/null @@ -1,11 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ConnectionError { - #[error("Invalid SIOPv2 authorization response: {0}")] - InvalidSIOPv2AuthorizationResponse(#[source] anyhow::Error), - #[error("Invalid OID4VP authorization response: {0}")] - InvalidOID4VPAuthorizationResponse(#[source] anyhow::Error), - #[error("`jwt` parameter is not supported yet")] - UnsupportedJwtParameterError, -} diff --git a/agent_verification/src/connection/queries.rs b/agent_verification/src/connection/queries.rs deleted file mode 100644 index 8dc9cf15..00000000 --- a/agent_verification/src/connection/queries.rs +++ /dev/null @@ -1,32 +0,0 @@ -use cqrs_es::{EventEnvelope, View}; -use oid4vc_core::authorization_request::Object; -use serde::{Deserialize, Serialize}; -use siopv2::siopv2::SIOPv2; - -use super::aggregate::Connection; - -pub type SIOPv2AuthorizationRequest = oid4vc_core::authorization_request::AuthorizationRequest<Object<SIOPv2>>; - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct ConnectionView { - id_token: Option<String>, - vp_token: Option<String>, - state: Option<String>, -} - -impl View<Connection> for ConnectionView { - fn update(&mut self, event: &EventEnvelope<Connection>) { - use crate::connection::event::ConnectionEvent::*; - - match &event.payload { - SIOPv2AuthorizationResponseVerified { id_token, state } => { - self.id_token.replace(id_token.clone()); - self.state.clone_from(state); - } - OID4VPAuthorizationResponseVerified { vp_token, state } => { - self.vp_token.replace(vp_token.clone()); - self.state.clone_from(state); - } - } - } -} diff --git a/agent_verification/src/lib.rs b/agent_verification/src/lib.rs index 37d478cc..43aabae2 100644 --- a/agent_verification/src/lib.rs +++ b/agent_verification/src/lib.rs @@ -1,5 +1,4 @@ pub mod authorization_request; -pub mod connection; pub mod generic_oid4vc; pub mod services; pub mod state; diff --git a/agent_verification/src/state.rs b/agent_verification/src/state.rs index 48eb6851..09ed29be 100644 --- a/agent_verification/src/state.rs +++ b/agent_verification/src/state.rs @@ -5,8 +5,6 @@ use std::sync::Arc; use crate::authorization_request::aggregate::AuthorizationRequest; use crate::authorization_request::views::all_authorization_requests::AllAuthorizationRequestsView; use crate::authorization_request::views::AuthorizationRequestView; -use crate::connection::aggregate::Connection; -use crate::connection::queries::ConnectionView; #[derive(Clone)] pub struct VerificationState { @@ -18,7 +16,6 @@ pub struct VerificationState { #[derive(Clone)] pub struct CommandHandlers { pub authorization_request: CommandHandler<AuthorizationRequest>, - pub connection: CommandHandler<Connection>, } /// This type is used to define the queries that are used to query the view repositories. We make use of `dyn` here, so /// that any type of repository that implements the `ViewRepository` trait can be used, but the corresponding `View` and @@ -26,18 +23,15 @@ pub struct CommandHandlers { type Queries = ViewRepositories< dyn ViewRepository<AuthorizationRequestView, AuthorizationRequest>, dyn ViewRepository<AllAuthorizationRequestsView, AuthorizationRequest>, - dyn ViewRepository<ConnectionView, Connection>, >; -pub struct ViewRepositories<AR1, AR2, C> +pub struct ViewRepositories<AR1, AR2> where AR1: ViewRepository<AuthorizationRequestView, AuthorizationRequest> + ?Sized, AR2: ViewRepository<AllAuthorizationRequestsView, AuthorizationRequest> + ?Sized, - C: ViewRepository<ConnectionView, Connection> + ?Sized, { pub authorization_request: Arc<AR1>, pub all_authorization_requests: Arc<AR2>, - pub connection: Arc<C>, } impl Clone for Queries { @@ -45,7 +39,6 @@ impl Clone for Queries { ViewRepositories { authorization_request: self.authorization_request.clone(), all_authorization_requests: self.all_authorization_requests.clone(), - connection: self.connection.clone(), } } }