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:
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,
¶ms.client_id,
¶ms.scope,
+ ¶ms.redirect_uri,
+ ¶ms.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(),
)