kych

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

commit 0780ea77a0f829e99a2c92b25c5bc4a67df9fca1
parent c90333cab497dcf2d801255023502464dd76b0ce
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Sun,  7 Dec 2025 16:57:53 +0100

oauth2_gateway: add verification deeplink to authorize response

Diffstat:
Moauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql | 1+
Moauth2_gateway/src/db/sessions.rs | 12+++++++++---
Moauth2_gateway/src/handlers.rs | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Moauth2_gateway/src/models.rs | 6++++++
Mswiyu-verifier/api_requests/post_management_api_verifications.sh | 2+-
5 files changed, 97 insertions(+), 10 deletions(-)

diff --git a/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql b/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql @@ -43,6 +43,7 @@ CREATE TABLE verification_sessions ( nonce VARCHAR(255) UNIQUE NOT NULL, scope TEXT NOT NULL, verification_url TEXT, + verification_deeplink TEXT, request_id VARCHAR(255), verifier_nonce VARCHAR(255), verifiable_credential JSONB, diff --git a/oauth2_gateway/src/db/sessions.rs b/oauth2_gateway/src/db/sessions.rs @@ -51,6 +51,7 @@ pub struct AuthorizeSessionData { pub expires_at: DateTime<Utc>, pub scope: String, pub verification_url: Option<String>, + pub verification_deeplink: Option<String>, pub request_id: Option<String>, pub verifier_nonce: Option<String>, // Client fields @@ -130,6 +131,7 @@ pub async fn get_session_for_authorize( s.expires_at, s.scope, s.verification_url, + s.verification_deeplink, s.request_id, s.verifier_nonce, c.verifier_url, @@ -149,6 +151,7 @@ pub async fn get_session_for_authorize( expires_at: row.get("expires_at"), scope: row.get("scope"), verification_url: row.get("verification_url"), + verification_deeplink: row.get("verification_deeplink"), request_id: row.get("request_id"), verifier_nonce: row.get("verifier_nonce"), verifier_url: row.get("verifier_url"), @@ -219,6 +222,7 @@ pub async fn update_session_authorized( pool: &PgPool, session_id: Uuid, verification_url: &str, + verification_deeplink: Option<&str>, request_id: &str, verifier_nonce: Option<&str>, ) -> Result<AuthorizedSessionResult> { @@ -227,14 +231,16 @@ pub async fn update_session_authorized( UPDATE oauth2gw.verification_sessions SET status = 'authorized', verification_url = $1, - request_id = $2, - verifier_nonce = $3, + verification_deeplink = $2, + request_id = $3, + verifier_nonce = $4, authorized_at = NOW() - WHERE id = $4 + WHERE id = $5 RETURNING request_id, verification_url "# ) .bind(verification_url) + .bind(verification_deeplink) .bind(request_id) .bind(verifier_nonce) .bind(session_id) diff --git a/oauth2_gateway/src/handlers.rs b/oauth2_gateway/src/handlers.rs @@ -123,9 +123,11 @@ pub async fn authorize( Query(params): Query<AuthorizeQuery>, ) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> { tracing::info!( - "Authorize request for client: {}, nonce: {}", + "Authorize request for client: {}, nonce: {}, state: {}, redirect_uri: {}", params.client_id, - nonce + nonce, + params.state, + params.redirect_uri ); if params.response_type != "code" { @@ -169,12 +171,35 @@ pub async fn authorize( )); } - // Check status for idempotency + // Check status for idempotency and completion match data.status { SessionStatus::Authorized => { - // Already authorized - return cached response tracing::info!( - "Session {} already authorized, returning cached response", + "Session {} already authorized, returning pending status", + data.session_id + ); + + let verification_id = data + .request_id + .and_then(|id| uuid::Uuid::parse_str(&id).ok()) + .unwrap_or(uuid::Uuid::nil()); + + return Ok(( + StatusCode::OK, + Json(AuthorizeResponse { + status: "pending".to_string(), + verification_id, + verification_url: data.verification_url.clone().unwrap_or_default(), + verification_deeplink: data.verification_deeplink, + state: params.state.clone(), + redirect_uri: None, + }), + )); + } + + SessionStatus::Verified => { + tracing::info!( + "Session {} verified, returning redirect URI", data.session_id ); @@ -183,11 +208,54 @@ pub async fn authorize( .and_then(|id| uuid::Uuid::parse_str(&id).ok()) .unwrap_or(uuid::Uuid::nil()); + let auth_code = crate::db::authorization_codes::get_code_by_session(&state.pool, data.session_id) + .await + .map_err(|e| { + tracing::error!("Failed to get authorization code: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error"))) + })?; + + let redirect_url = match auth_code { + Some(code) => { + let separator = if params.redirect_uri.contains('?') { "&" } else { "?" }; + format!("{}{}code={}&state={}", params.redirect_uri, separator, code, params.state) + } + None => { + tracing::error!("No authorization code found for verified session"); + return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))); + } + }; + return Ok(( StatusCode::OK, Json(AuthorizeResponse { + status: "completed".to_string(), verification_id, - verification_url: data.verification_url.unwrap_or_default(), + verification_url: data.verification_url.clone().unwrap_or_default(), + verification_deeplink: data.verification_deeplink, + state: params.state.clone(), + redirect_uri: Some(redirect_url), + }), + )); + } + + SessionStatus::Failed => { + tracing::warn!("Session {} failed", data.session_id); + + let verification_id = data + .request_id + .and_then(|id| uuid::Uuid::parse_str(&id).ok()) + .unwrap_or(uuid::Uuid::nil()); + + return Ok(( + StatusCode::OK, + Json(AuthorizeResponse { + status: "failed".to_string(), + verification_id, + verification_url: data.verification_url.clone().unwrap_or_default(), + verification_deeplink: data.verification_deeplink, + state: params.state.clone(), + redirect_uri: None, }), )); } @@ -195,6 +263,7 @@ pub async fn authorize( SessionStatus::Pending => { // Proceed with authorization } + _ => { tracing::warn!( "Session {} in invalid status: {:?}", @@ -268,6 +337,7 @@ pub async fn authorize( &state.pool, data.session_id, &swiyu_response.verification_url, + swiyu_response.verification_deeplink.as_deref(), &swiyu_response.id.to_string(), swiyu_response.request_nonce.as_deref(), ) @@ -289,8 +359,12 @@ pub async fn authorize( Ok(( StatusCode::OK, Json(AuthorizeResponse { + status: "pending".to_string(), verification_id: swiyu_response.id, verification_url: result.verification_url, + verification_deeplink: swiyu_response.verification_deeplink, + state: params.state.clone(), + redirect_uri: None, }), )) } diff --git a/oauth2_gateway/src/models.rs b/oauth2_gateway/src/models.rs @@ -17,9 +17,15 @@ pub struct AuthorizeQuery { #[derive(Debug, Deserialize, Serialize)] pub struct AuthorizeResponse { + pub status: String, #[serde(rename = "verificationId")] pub verification_id: Uuid, pub verification_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub verification_deeplink: Option<String>, + pub state: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub redirect_uri: Option<String>, } // Token endpoint diff --git a/swiyu-verifier/api_requests/post_management_api_verifications.sh b/swiyu-verifier/api_requests/post_management_api_verifications.sh @@ -25,7 +25,7 @@ base_name="${input_file%.json}" response_file="${base_name}_response.json" qr_code_file="${base_name}_qr_code.png" -curl -X POST http://localhost:8080/management/api/verifications \ +curl -v -X POST http://localhost:8080/management/api/verifications \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d @"$input_file" \