Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion agent_api_rest/src/issuance/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ pub struct CredentialsEndpointRequest {
pub is_signed: bool,
pub credential_configuration_id: String,
pub expires_at: CredentialExpiry,
#[serde(default)]
pub delivery_options: DeliveryOptions,
}

Expand Down Expand Up @@ -302,6 +301,9 @@ pub mod tests {
},
"credentialConfigurationId": CREDENTIAL_CONFIGURATION_ID,
"expiresAt": "never",
"deliveryOptions": {
"recipientEmail": null
}
}))
.unwrap(),
))
Expand Down
15 changes: 9 additions & 6 deletions agent_api_rest/src/issuance/offers/send.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::handlers::command_handler;
use agent_issuance::{offer::command::OfferCommand, state::IssuanceState};
use agent_issuance::{offer::aggregate::DeliveryMethod, offer::command::OfferCommand, state::IssuanceState};
use axum::{
extract::State,
response::{IntoResponse, Response},
Expand All @@ -8,26 +8,29 @@ use axum::{
use http_api_problem::ApiError;
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use url::Url;

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SendOfferEndpointRequest {
pub offer_id: String,
pub target_url: Url,
#[serde(flatten)]
pub delivery_method: DeliveryMethod,
}

#[axum_macros::debug_handler]
pub(crate) async fn send(
State(state): State<IssuanceState>,
Json(SendOfferEndpointRequest { offer_id, target_url }): Json<SendOfferEndpointRequest>,
Json(SendOfferEndpointRequest {
offer_id,
delivery_method,
}): Json<SendOfferEndpointRequest>,
) -> Result<Response, ApiError> {
let command = OfferCommand::SendCredentialOffer {
offer_id: offer_id.clone(),
target_url,
delivery_method,
};

// Send the Credential Offer to the `target_url`.
// Send the Credential Offer to the `target_url` or to the email recipient.
command_handler(&offer_id, &state.command.offer, command).await?;

Ok(StatusCode::OK.into_response())
Expand Down
2 changes: 1 addition & 1 deletion agent_application/example.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ event_publishers:
subjects:
- name: "email.commands"
events:
offer: [TxCodeGenerated]
offer: [TxCodeGenerated, CredentialOfferEmailSent]

# These display parameters are interpreted by identity wallets.
display:
Expand Down
12 changes: 5 additions & 7 deletions agent_event_publisher_nats/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ event_publishers:
nats:
enabled: true
nats_url: "nats://localhost:4222" # NATS server URL
subject: "email.commands" # NATS subject to publish to
events:
offer:
- "TxCodeGenerated" # Event types to publish
subjects:
- name: "email.commands" # NATS subject to publish to
events:
offer: [TxCodeGenerated, CredentialOfferEmailSent] # Event types to publish
```

## Usage
Expand Down Expand Up @@ -56,8 +56,6 @@ For example:

### Available events

Currently, the nats publisher only listens to one event:

#### `offer`

TxCodeGenerated
TxCodeGenerated, CredentialOfferEmailSent
12 changes: 9 additions & 3 deletions agent_holder/src/offer/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,9 @@ pub mod tests {
}},
"credentialConfigurationId": "001",
"expiresAt": "never",
"deliveryOptions": {
"recipientEmail": null
}
}))
.unwrap(),
))
Expand All @@ -392,9 +395,12 @@ pub mod tests {
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
.body(Body::from(
serde_json::to_vec(&json!({
"offerId": received_offer_id,
"credentialConfigurationIds": ["001"],
}))
"offerId": received_offer_id,
"credentialConfigurationIds": ["001"],
"deliveryOptions": {
"recipientEmail": null
}
}))
.unwrap(),
))
.unwrap(),
Expand Down
79 changes: 59 additions & 20 deletions agent_issuance/src/offer/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::Arc;
use tracing::{debug, info};
use url::Url;

use crate::offer::command::OfferCommand;
use crate::offer::error::OfferError::{self, *};
Expand Down Expand Up @@ -52,6 +53,21 @@ pub struct DeliveryOptions {
pub recipient_email: Option<String>,
}

// Delivery methods for sending the credential offer. Not to be confused
// with the DeliveryOptions struct, which is used when creating the offer.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum DeliveryMethod {
TargetUrl {
#[serde(rename = "targetUrl")]
target_url: Url,
},
Email {
#[serde(rename = "recipientEmail")]
recipient_email: String,
},
}

#[async_trait]
impl Aggregate for Offer {
type Command = OfferCommand;
Expand Down Expand Up @@ -111,6 +127,9 @@ impl Aggregate for Offer {

// If TxCode constraints are provided, generate a transaction code.
let tx_code = tx_code_constraints.as_ref().map(generate_tx_code);
if tx_code.is_some() && delivery_options.recipient_email.is_none() {
return Err(MissingRecipientEmailError);
}

let credential_offer = CredentialOffer::CredentialOffer(Box::new(CredentialOfferParameters {
credential_issuer: credential_issuer.clone(),
Expand All @@ -137,7 +156,7 @@ impl Aggregate for Offer {
offer_id: offer_id.clone(),
grant_types,
credential_offer_uri,
credential_offer,
credential_offer: credential_offer.clone(),
pre_authorized_code,
access_token,
status: Status::Created,
Expand All @@ -146,7 +165,7 @@ impl Aggregate for Offer {
},
FormUrlEncodedCredentialOfferCreated {
offer_id: offer_id.clone(),
form_url_encoded_credential_offer,
form_url_encoded_credential_offer: form_url_encoded_credential_offer.clone(),
status: Status::Pending,
},
];
Expand Down Expand Up @@ -205,28 +224,47 @@ impl Aggregate for Offer {

Ok(events)
}
SendCredentialOffer { offer_id, target_url } => {
let client = reqwest::Client::new();
let target = self
SendCredentialOffer {
offer_id,
delivery_method,
} => {
let form_url_encoded_credential_offer = self
.form_url_encoded_credential_offer
.as_ref()
.ok_or_else(|| MissingCredentialOfferError)?
.replace("openid-credential-offer://", target_url.as_str());

info!("Sending credential offer to: {}", target);

client
.get(target)
.send()
.await
.and_then(|response| response.error_for_status())
.map_err(SendCredentialOfferError)?;
.clone();

Ok(vec![CredentialOfferSent {
offer_id,
target_url,
status: Status::Pending,
}])
match delivery_method {
DeliveryMethod::TargetUrl { target_url } => {
let client = reqwest::Client::new();
let target = form_url_encoded_credential_offer
.replace("openid-credential-offer://", target_url.as_str());

info!("Sending credential offer to: {}", target);

client
.get(target)
.send()
.await
.and_then(|response| response.error_for_status())
.map_err(SendCredentialOfferError)?;

Ok(vec![CredentialOfferSent {
offer_id,
target_url,
status: Status::Pending,
}])
}
DeliveryMethod::Email { recipient_email } => {
info!("Sending credential offer via email to: {}", recipient_email);
Ok(vec![CredentialOfferEmailSent {
offer_id,
recipient_email,
form_url_encoded_credential_offer,
status: Status::Pending,
}])
}
}
}
CreateTokenResponse {
offer_id,
Expand Down Expand Up @@ -371,6 +409,7 @@ impl Aggregate for Offer {
self.status = status;
}
CredentialOfferSent { .. } => {}
CredentialOfferEmailSent { .. } => {}
CredentialRequestVerified { subject_id, .. } => {
self.subject_id.replace(subject_id);
}
Expand Down
5 changes: 2 additions & 3 deletions agent_issuance/src/offer/command.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::offer::aggregate::DeliveryOptions;
use crate::offer::aggregate::{DeliveryMethod, DeliveryOptions};
use oid4vci::{
credential_issuer::{
authorization_server_metadata::AuthorizationServerMetadata,
Expand All @@ -9,7 +9,6 @@ use oid4vci::{
token_request::TokenRequest,
};
use serde::Deserialize;
use url::Url;

#[derive(Debug, Deserialize)]
#[serde(untagged)]
Expand All @@ -28,7 +27,7 @@ pub enum OfferCommand {
},
SendCredentialOffer {
offer_id: String,
target_url: Url,
delivery_method: DeliveryMethod,
},

// OpenID4VCI Pre-Authorized Code Flow
Expand Down
6 changes: 6 additions & 0 deletions agent_issuance/src/offer/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ pub enum OfferEvent {
target_url: Url,
status: Status,
},
CredentialOfferEmailSent {
offer_id: String,
recipient_email: String,
form_url_encoded_credential_offer: String,
status: Status,
},
TokenResponseCreated {
offer_id: String,
token_response: TokenResponse,
Expand Down
9 changes: 9 additions & 0 deletions agent_issuance/src/offer/views/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ impl View<Offer> for Offer {
self.offer_id.clone_from(offer_id);
self.status.clone_from(status);
}
CredentialOfferEmailSent {
offer_id,
recipient_email: _recipient_email,
form_url_encoded_credential_offer: _form_url_encoded_credential_offer,
status,
} => {
self.offer_id.clone_from(offer_id);
self.status.clone_from(status);
}
CredentialRequestVerified { offer_id, subject_id } => {
self.offer_id.clone_from(offer_id);
self.subject_id.replace(subject_id.clone());
Expand Down
3 changes: 2 additions & 1 deletion agent_shared/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ pub enum OfferEvent {
CredentialRequestVerified,
CredentialResponseCreated,
TxCodeGenerated,
CredentialOfferEmailSent,
}

#[derive(Debug, Serialize, Deserialize, Clone, strum::Display)]
Expand Down Expand Up @@ -1009,7 +1010,7 @@ mod tests {
"sd-jwt_alg_values": ["ES256"],
"kb-jwt_alg_values": ["ES256"]
}
}
},
})
);

Expand Down