commit 3270a115d8670a2de5f57f279ee4973ffaf97799
parent 250ea4db7a7789c35ce0056eebdc591ee436589c
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date: Wed, 5 Nov 2025 00:09:08 +0100
oauth2_gateway: implemented notification endpoint with mock exchange client
Diffstat:
3 files changed, 180 insertions(+), 21 deletions(-)
diff --git a/oauth2_gateway/src/handlers.rs b/oauth2_gateway/src/handlers.rs
@@ -158,7 +158,7 @@ pub async fn authorize(
return Err((StatusCode::BAD_GATEWAY, Json(ErrorResponse::new("verifier_error"))));
}
- let swiyu_verification: SwiyuVerificationResponse = swiyu_response
+ let swiyu_verification: SwiyuManagementResponse = swiyu_response
.json()
.await
.map_err(|e| {
@@ -173,7 +173,7 @@ pub async fn authorize(
);
// Update session with verification data
- crate::db::sessions::update_session_authorized(
+ crate::db::sessions::set_session_authorized(
&state.pool,
session.id,
&swiyu_verification.verification_url,
@@ -299,22 +299,159 @@ pub async fn info(
(StatusCode::OK, Json(credential))
}
-// POST /notification/{request_id}
+// POST /notification
pub async fn notification_webhook(
- State(_state): State<AppState>,
- Path(request_id): Path<String>,
- Json(request): Json<NotificationRequest>,
-) -> impl IntoResponse {
+ State(state): State<AppState>,
+ Json(webhook): Json<NotificationRequest>,
+) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
tracing::info!(
- "Webhook notification received for request_id: {}, nonce: {}, complete: {}",
- request_id,
- request.nonce,
- request.verification_complete
+ "Webhook received from Swiyu: verification_id={}, timestamp={}",
+ webhook.verification_id,
+ webhook.timestamp
);
- // TODO: POST the Exchange at {exchange_base_url}/oauth2gw/kyc/notify/{clientId}
+ // Look up session by request_id (Swiyu's verification_id)
+ let session = crate::db::sessions::get_session_by_request_id(&state.pool, &webhook.verification_id.to_string())
+ .await
+ .map_err(|e| {
+ tracing::error!("Database error looking up session: {}", e);
+ (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))
+ })?;
+
+ let session = match session {
+ Some(s) => s,
+ None => {
+ tracing::warn!("Session not found for verification_id: {}", webhook.verification_id);
+ return Err((StatusCode::NOT_FOUND, Json(ErrorResponse::new("session_not_found"))));
+ }
+ };
+
+ tracing::info!("Found session {} for client_id {}", session.id, session.client_id);
+
+ // Get client info to know which verifier to call
+ let client = crate::db::clients::get_client_by_uuid(&state.pool, session.client_id)
+ .await
+ .map_err(|e| {
+ tracing::error!("Database error looking up client: {}", e);
+ (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))
+ })?;
+
+ let client = match client {
+ Some(c) => c,
+ None => {
+ tracing::error!("Client {} not found for session {}", session.client_id, session.id);
+ return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error"))));
+ }
+ };
+
+ // Call Swiyu Verifier to get verification status
+ let swiyu_url = format!(
+ "{}{}/{}",
+ client.verifier_base_url,
+ client.verifier_management_api_path,
+ webhook.verification_id
+ );
+ tracing::info!("Fetching verification status from: {}", swiyu_url);
+
+ let http_client = reqwest::Client::new();
+ let swiyu_response = http_client
+ .get(&swiyu_url)
+ .send()
+ .await
+ .map_err(|e| {
+ tracing::error!("Failed to call Swiyu Verifier API: {}", e);
+ (StatusCode::SERVICE_UNAVAILABLE, Json(ErrorResponse::new("verifier_unavailable")))
+ })?;
+
+ if !swiyu_response.status().is_success() {
+ let status = swiyu_response.status();
+ let error_body = swiyu_response.text().await.unwrap_or_default();
+ tracing::error!("Swiyu Verifier returned error {}: {}", status, error_body);
+ return Err((StatusCode::BAD_GATEWAY, Json(ErrorResponse::new("verifier_error"))));
+ }
+
+ let swiyu_status: SwiyuManagementResponse = swiyu_response
+ .json()
+ .await
+ .map_err(|e| {
+ tracing::error!("Failed to parse Swiyu response: {}", e);
+ (StatusCode::BAD_GATEWAY, Json(ErrorResponse::new("verifier_invalid_response")))
+ })?;
+
+ tracing::info!(
+ "Swiyu verification status: {:?} for verification_id={}",
+ swiyu_status.state,
+ webhook.verification_id
+ );
+
+ // Map Swiyu status to our SessionStatus
+ let new_status = match swiyu_status.state {
+ SwiyuVerificationStatus::Success => crate::db::sessions::SessionStatus::Verified,
+ SwiyuVerificationStatus::Failed => crate::db::sessions::SessionStatus::Failed,
+ SwiyuVerificationStatus::Pending => {
+ tracing::warn!("Received webhook but verification still pending");
+ return Ok(StatusCode::OK);
+ }
+ };
+
+ // Update session status with timestamp
+ crate::db::sessions::update_session_status_with_timestamp(&state.pool, session.id, new_status.clone())
+ .await
+ .map_err(|e| {
+ tracing::error!("Failed to update session status: {}", e);
+ (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))
+ })?;
+
+ tracing::info!("Updated session {} status to {:?}", session.id, new_status);
+
+ // TODO: Log notification event to database
+
+ // Notify Exchange of verification result
+ let notification_payload = crate::models::ExchangeNotification {
+ nonce: session.nonce.clone(),
+ status: match new_status {
+ crate::db::sessions::SessionStatus::Verified => "verified".to_string(),
+ crate::db::sessions::SessionStatus::Failed => "failed".to_string(),
+ _ => unreachable!("new_status can only be Verified or Failed at this point"),
+ },
+ verification_id: webhook.verification_id,
+ timestamp: chrono::Utc::now().to_rfc3339(),
+ };
+
+ tracing::info!(
+ "Notifying Exchange at {} with status: {}",
+ client.notification_url,
+ notification_payload.status
+ );
+
+ let exchange_response = http_client
+ .post(&client.notification_url)
+ .json(¬ification_payload)
+ .send()
+ .await;
+
+ match exchange_response {
+ Ok(resp) if resp.status().is_success() => {
+ tracing::info!("Successfully notified Exchange at {}", client.notification_url);
+ }
+ Ok(resp) => {
+ let status = resp.status();
+ let error_body = resp.text().await.unwrap_or_default();
+ tracing::warn!(
+ "Exchange notification endpoint returned error {}: {}",
+ status,
+ error_body
+ );
+ // Note: We don't fail the webhook handler if Exchange notification fails
+ // The session is already updated, Exchange can poll if needed
+ }
+ Err(e) => {
+ tracing::error!("Failed to notify Exchange at {}: {}", client.notification_url, e);
+ // Note: We don't fail the webhook handler if Exchange notification fails
+ }
+ }
- StatusCode::OK
+ Ok(StatusCode::OK)
}
#[cfg(test)]
diff --git a/oauth2_gateway/src/main.rs b/oauth2_gateway/src/main.rs
@@ -44,7 +44,7 @@ async fn main() -> Result<()> {
.route("/authorize/{nonce}", get(handlers::authorize))
.route("/token", post(handlers::token))
.route("/info", get(handlers::info))
- .route("/notification/{client_id}", post(handlers::notification_webhook))
+ .route("/notification", post(handlers::notification_webhook))
.layer(TraceLayer::new_for_http())
.with_state(state);
diff --git a/oauth2_gateway/src/models.rs b/oauth2_gateway/src/models.rs
@@ -45,12 +45,20 @@ pub struct VerifiableCredential {
pub data: serde_json::Value,
}
-// Notification webhook
+// Notification webhook from Swiyu Verifier
#[derive(Debug, Deserialize, Serialize)]
pub struct NotificationRequest {
+ pub verification_id: Uuid,
+ pub timestamp: String,
+}
+
+// Notification payload sent to Exchange
+#[derive(Debug, Serialize)]
+pub struct ExchangeNotification {
pub nonce: String,
- #[serde(default)]
- pub verification_complete: bool,
+ pub status: String, // "verified" or "failed"
+ pub verification_id: Uuid,
+ pub timestamp: String,
}
// Error response
@@ -202,14 +210,28 @@ pub struct FormatAlgorithm {
pub kb_jwt_alg_values: Vec<String>,
}
-/// Response from Swiyu Verifier after creating verification
+/// Response from Swiyu Verifier Management API
+/// Used for both POST /management/api/verifications and GET /management/api/verifications/{id}
#[derive(Debug, Serialize, Deserialize)]
-pub struct SwiyuVerificationResponse {
- #[serde(rename = "verificationId")]
+pub struct SwiyuManagementResponse {
pub id: Uuid,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub request_nonce: Option<String>,
+ pub state: SwiyuVerificationStatus,
pub verification_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_deeplink: Option<String>,
+ pub presentation_definition: PresentationDefinition,
#[serde(skip_serializing_if = "Option::is_none")]
- pub state: Option<String>,
+ pub dcql_query: Option<serde_json::Value>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub wallet_response: Option<serde_json::Value>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
+#[serde(rename_all = "UPPERCASE")]
+pub enum SwiyuVerificationStatus {
+ Pending,
+ Success,
+ Failed,
}