kych

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

commit 5d65d064190aa64ddaf01cb9f99e4451fea36ece
parent aa1be322bd6184be11df33dd0f293c0f90ed31dd
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Mon,  8 Dec 2025 21:11:42 +0100

oauth2_gateway: store redirect uri and state in session, fix webhook

Diffstat:
Moauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql | 6++++++
Moauth2_gateway/src/db/sessions.rs | 26++++++++++++++++++++++----
Moauth2_gateway/src/handlers.rs | 15++++++++++++++-
3 files changed, 42 insertions(+), 5 deletions(-)

diff --git a/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql b/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql @@ -39,6 +39,8 @@ CREATE TABLE verification_sessions ( client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE, nonce VARCHAR(255) UNIQUE NOT NULL, scope TEXT NOT NULL, + redirect_uri TEXT, + state TEXT, verification_url TEXT, verification_deeplink TEXT, request_id VARCHAR(255), @@ -70,6 +72,10 @@ COMMENT ON COLUMN verification_sessions.nonce IS 'Cryptographically secure 256-bit random value used as OAuth2 authorization code'; COMMENT ON COLUMN verification_sessions.scope IS 'Space-delimited requested verification attributes (e.g., "first_name last_name")'; +COMMENT ON COLUMN verification_sessions.redirect_uri + IS 'OAuth2 redirect_uri from /authorize request where authorization code will be sent'; +COMMENT ON COLUMN verification_sessions.state + IS 'OAuth2 state parameter from /authorize request for CSRF protection'; COMMENT ON COLUMN verification_sessions.verification_url IS 'URL for user wallet to complete verification (populated after /authorize)'; COMMENT ON COLUMN verification_sessions.request_id diff --git a/oauth2_gateway/src/db/sessions.rs b/oauth2_gateway/src/db/sessions.rs @@ -30,6 +30,8 @@ pub struct VerificationSession { pub client_id: Uuid, pub nonce: String, pub scope: String, + pub redirect_uri: Option<String>, + pub state: Option<String>, pub verification_url: Option<String>, pub request_id: Option<String>, pub verifier_nonce: Option<String>, @@ -50,6 +52,8 @@ pub struct AuthorizeSessionData { pub status: SessionStatus, pub expires_at: DateTime<Utc>, pub scope: String, + pub redirect_uri: Option<String>, + pub state: Option<String>, pub verification_url: Option<String>, pub verification_deeplink: Option<String>, pub request_id: Option<String>, @@ -66,6 +70,8 @@ pub struct NotificationSessionData { pub session_id: Uuid, pub nonce: String, pub status: SessionStatus, + pub redirect_uri: Option<String>, + pub state: Option<String>, // Client fields pub client_id: Uuid, pub webhook_url: String, @@ -93,9 +99,9 @@ pub async fn create_session( FROM oauth2gw.clients c WHERE c.client_id = $3 RETURNING - id, client_id, nonce, scope, verification_url, request_id, - verifier_nonce, status, created_at, authorized_at, - verified_at, completed_at, failed_at, expires_at + id, client_id, nonce, scope, redirect_uri, state, verification_url, + request_id, verifier_nonce, status, created_at, authorized_at, + verified_at, completed_at, failed_at, expires_at "# ) .bind(nonce) @@ -118,11 +124,13 @@ pub async fn get_session_for_authorize( nonce: &str, client_id: &str, scope: &str, + redirect_uri: &str, + state: &str, ) -> Result<Option<AuthorizeSessionData>> { let result = sqlx::query( r#" UPDATE oauth2gw.verification_sessions s - SET scope = $3 + SET scope = $3, redirect_uri = $4, state = $5 FROM oauth2gw.clients c WHERE s.client_id = c.id AND s.nonce = $1 @@ -132,6 +140,8 @@ pub async fn get_session_for_authorize( s.status, s.expires_at, s.scope, + s.redirect_uri, + s.state, s.verification_url, s.verification_deeplink, s.request_id, @@ -143,6 +153,8 @@ pub async fn get_session_for_authorize( .bind(nonce) .bind(client_id) .bind(scope) + .bind(redirect_uri) + .bind(state) .fetch_optional(pool) .await?; @@ -153,6 +165,8 @@ pub async fn get_session_for_authorize( status: row.get("status"), expires_at: row.get("expires_at"), scope: row.get("scope"), + redirect_uri: row.get("redirect_uri"), + state: row.get("state"), verification_url: row.get("verification_url"), verification_deeplink: row.get("verification_deeplink"), request_id: row.get("request_id"), @@ -183,6 +197,8 @@ pub async fn get_session_for_notification( s.id AS session_id, s.nonce, s.status, + s.redirect_uri, + s.state, c.id AS client_id, c.webhook_url, c.verifier_url, @@ -199,6 +215,8 @@ pub async fn get_session_for_notification( session_id: row.get("session_id"), nonce: row.get("nonce"), status: row.get("status"), + redirect_uri: row.get("redirect_uri"), + state: row.get("state"), client_id: row.get("client_id"), webhook_url: row.get("webhook_url"), verifier_url: row.get("verifier_url"), diff --git a/oauth2_gateway/src/handlers.rs b/oauth2_gateway/src/handlers.rs @@ -143,6 +143,8 @@ pub async fn authorize( &nonce, &params.client_id, &params.scope, + &params.redirect_uri, + &params.state, ) .await .map_err(|e| { @@ -676,6 +678,17 @@ pub async fn notification_webhook( // Generate authorization code let authorization_code = crypto::generate_nonce(); + // Construct webhook URL from redirect_uri and state + let webhook_url = if let Some(redirect_uri) = &session_data.redirect_uri { + if let Some(state) = &session_data.state { + format!("{}?state={}", redirect_uri, state) + } else { + redirect_uri.clone() + } + } else { + session_data.webhook_url.clone() + }; + // Build webhook body for client notification let client_notification = ClientNotification { nonce: session_data.nonce.clone(), @@ -701,7 +714,7 @@ pub async fn notification_webhook( &authorization_code, 10, // 10 minutes for auth code expiry session_data.client_id, - &session_data.webhook_url, + &webhook_url, &webhook_body, swiyu_result.wallet_response.as_ref(), )