kych

OAuth 2.0 API for Swiyu to enable Taler integration of Swiyu for KYC (experimental)
Log | Files | Refs

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:
Moauth2_gateway/src/handlers.rs | 163++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Moauth2_gateway/src/main.rs | 2+-
Moauth2_gateway/src/models.rs | 36+++++++++++++++++++++++++++++-------
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(&notification_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, }