kych

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

commit 3f3b73974aeba8c26731bb3428867b26c9a525d4
parent d72b55d18e46edb5cc63bd84d3b8124ef7c289d3
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Mon, 19 Jan 2026 21:32:41 +0100

Simplify client model to redirect URI allowlist

Remove the client callback/webhook URL entirely and require clients to configure
allowed REDIRECT_URI values instead. Authorization now always resolves redirects
from the validated allowlist, removing the fallback callback concept and
aligning behavior with standard OAuth2 redirect handling.

Diffstat:
Mkych_oauth2_gateway/src/bin/client_management_cli.rs | 37+++++++++----------------------------
Mkych_oauth2_gateway/src/config.rs | 16+++++++++-------
Mkych_oauth2_gateway/src/db/clients.rs | 39++++++++++++++++-----------------------
Mkych_oauth2_gateway/src/handlers.rs | 22++++++----------------
4 files changed, 40 insertions(+), 74 deletions(-)

diff --git a/kych_oauth2_gateway/src/bin/client_management_cli.rs b/kych_oauth2_gateway/src/bin/client_management_cli.rs @@ -6,7 +6,7 @@ //! kych-client-management --config kych.conf list //! kych-client-management --config kych.conf show <client_id> //! kych-client-management --config kych.conf create --client-id <id> --secret <secret> ... -//! kych-client-management --config kych.conf update <client_id> --webhook-url <url> +//! kych-client-management --config kych.conf update <client_id> --redirect-uri <url> //! kych-client-management --config kych.conf sync //! kych-client-management --config kych.conf delete <client_id> @@ -45,10 +45,6 @@ enum Commands { #[arg(long)] secret: String, - /// Webhook URL for notifications - #[arg(long)] - webhook_url: String, - /// Swiyu verifier base URL #[arg(long)] verifier_url: String, @@ -59,7 +55,7 @@ enum Commands { /// Default redirect URI for OAuth2 flow #[arg(long)] - redirect_uri: Option<String>, + redirect_uri: String, /// Comma-separated list of accepted issuer DIDs #[arg(long)] @@ -70,9 +66,6 @@ enum Commands { client_id: String, #[arg(long)] - webhook_url: Option<String>, - - #[arg(long)] verifier_url: Option<String>, #[arg(long)] @@ -118,7 +111,6 @@ async fn main() -> Result<()> { Commands::Create { client_id, secret, - webhook_url, verifier_url, verifier_api_path, redirect_uri, @@ -128,17 +120,15 @@ async fn main() -> Result<()> { &pool, &client_id, &secret, - &webhook_url, &verifier_url, verifier_api_path.as_deref(), - redirect_uri.as_deref(), + &redirect_uri, accepted_issuer_dids.as_deref(), ) .await? } Commands::Update { client_id, - webhook_url, verifier_url, verifier_api_path, redirect_uri, @@ -147,7 +137,6 @@ async fn main() -> Result<()> { cmd_update_client( &pool, &client_id, - webhook_url.as_deref(), verifier_url.as_deref(), verifier_api_path.as_deref(), redirect_uri.as_deref(), @@ -173,10 +162,9 @@ fn print_client_details(client: &db::clients::Client) { println!("UUID: {}", client.id); println!("Client ID: {}", client.client_id); println!("Secret Hash: {}...", &client.secret_hash[..20.min(client.secret_hash.len())]); - println!("Webhook URL: {}", client.webhook_url); println!("Verifier URL: {}", client.verifier_url); println!("Verifier API Path: {}", client.verifier_management_api_path); - println!("Redirect URI: {}", client.redirect_uri.as_deref().unwrap_or("(not set)")); + println!("Redirect URI(s): {}", client.redirect_uri); println!("Accepted Issuer DIDs: {}", client.accepted_issuer_dids.as_deref().unwrap_or("(not set)")); println!("Created: {}", client.created_at.with_timezone(&Local)); println!("Updated: {}", client.updated_at.with_timezone(&Local)); @@ -220,17 +208,15 @@ async fn cmd_create_client( pool: &sqlx::PgPool, client_id: &str, secret: &str, - webhook_url: &str, verifier_url: &str, verifier_api_path: Option<&str>, - redirect_uri: Option<&str>, + redirect_uri: &str, accepted_issuer_dids: Option<&str>, ) -> Result<()> { let client = db::clients::register_client( pool, client_id, secret, - webhook_url, verifier_url, verifier_api_path, redirect_uri, @@ -249,15 +235,14 @@ async fn cmd_create_client( async fn cmd_update_client( pool: &sqlx::PgPool, client_id: &str, - webhook_url: Option<&str>, verifier_url: Option<&str>, verifier_api_path: Option<&str>, redirect_uri: Option<&str>, accepted_issuer_dids: Option<&str>, ) -> Result<()> { - if webhook_url.is_none() && verifier_url.is_none() && verifier_api_path.is_none() + if verifier_url.is_none() && verifier_api_path.is_none() && redirect_uri.is_none() && accepted_issuer_dids.is_none() { - anyhow::bail!("No fields to update. Specify at least one of: --webhook-url, --verifier-url, --verifier-api-path, --redirect-uri, --accepted-issuer-dids"); + anyhow::bail!("No fields to update. Specify at least one of: --verifier-url, --verifier-api-path, --redirect-uri, --accepted-issuer-dids"); } let client = db::clients::get_client_by_id(pool, client_id) @@ -267,7 +252,6 @@ async fn cmd_update_client( let updated = db::clients::update_client( pool, client.id, - webhook_url, verifier_url, verifier_api_path, redirect_uri, @@ -292,7 +276,6 @@ async fn cmd_delete_client(pool: &sqlx::PgPool, client_id: &str, skip_confirm: b println!("WARNING: This will delete client '{}' and ALL associated data:", client_id); println!(" - All sessions"); println!(" - All tokens"); - println!(" - All pending webhooks"); println!(); print!("Type 'yes' to confirm: "); @@ -339,10 +322,9 @@ async fn cmd_sync_clients(pool: &sqlx::PgPool, config: &Config, prune: bool) -> db::clients::update_client( pool, existing.id, - Some(&client_config.webhook_url), Some(&client_config.verifier_url), Some(&client_config.verifier_management_api_path), - client_config.redirect_uri.as_deref(), + Some(&client_config.redirect_uri), client_config.accepted_issuer_dids.as_deref(), ) .await @@ -356,10 +338,9 @@ async fn cmd_sync_clients(pool: &sqlx::PgPool, config: &Config, prune: bool) -> pool, &client_config.client_id, &client_config.client_secret, - &client_config.webhook_url, &client_config.verifier_url, Some(&client_config.verifier_management_api_path), - client_config.redirect_uri.as_deref(), + &client_config.redirect_uri, client_config.accepted_issuer_dids.as_deref(), ) .await diff --git a/kych_oauth2_gateway/src/config.rs b/kych_oauth2_gateway/src/config.rs @@ -56,6 +56,7 @@ pub struct CryptoConfig { pub nonce_bytes: usize, pub token_bytes: usize, pub authorization_code_bytes: usize, + pub authorization_code_ttl_minutes: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -63,10 +64,9 @@ pub struct ClientConfig { pub section_name: String, pub client_id: String, pub client_secret: String, - pub webhook_url: String, pub verifier_url: String, pub verifier_management_api_path: String, - pub redirect_uri: Option<String>, + pub redirect_uri: String, pub accepted_issuer_dids: Option<String>, } @@ -131,6 +131,11 @@ impl Config { .context("Missing AUTH_CODE_BYTES")? .parse() .context("Invalid AUTH_CODE_BYTES")?, + authorization_code_ttl_minutes: main_section + .get("AUTH_CODE_TTL_MINUTES") + .unwrap_or("10") + .parse() + .context("Invalid AUTH_CODE_TTL_MINUTES")?, }; let mut clients = Vec::new(); @@ -146,9 +151,6 @@ impl Config { let client_secret = properties.get("CLIENT_SECRET") .context(format!("Missing CLIENT_SECRET in section [{}]", section_name))? .to_string(); - let webhook_url = properties.get("WEBHOOK_URL") - .context(format!("Missing WEBHOOK_URL in section [{}]", section_name))? - .to_string(); let verifier_url = properties.get("VERIFIER_URL") .context(format!("Missing VERIFIER_URL in section [{}]", section_name))? .to_string(); @@ -157,7 +159,8 @@ impl Config { .to_string(); let redirect_uri = properties.get("REDIRECT_URI") .filter(|s| !s.is_empty()) - .map(|s| s.to_string()); + .context(format!("Missing REDIRECT_URI in section [{}]", section_name))? + .to_string(); let accepted_issuer_dids = properties.get("ACCEPTED_ISSUER_DIDS") .filter(|s| !s.is_empty()) .map(|s| s.to_string()); @@ -166,7 +169,6 @@ impl Config { section_name: section_name.to_string(), client_id, client_secret, - webhook_url, verifier_url, verifier_management_api_path, redirect_uri, diff --git a/kych_oauth2_gateway/src/db/clients.rs b/kych_oauth2_gateway/src/db/clients.rs @@ -11,10 +11,9 @@ pub struct Client { pub id: Uuid, pub client_id: String, pub secret_hash: String, - pub webhook_url: String, pub verifier_url: String, pub verifier_management_api_path: String, - pub redirect_uri: Option<String>, + pub redirect_uri: String, pub accepted_issuer_dids: Option<String>, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, @@ -25,10 +24,9 @@ pub async fn register_client( pool: &PgPool, client_id: &str, client_secret: &str, - webhook_url: &str, verifier_url: &str, verifier_management_api_path: Option<&str>, - redirect_uri: Option<&str>, + redirect_uri: &str, accepted_issuer_dids: Option<&str>, ) -> Result<Client> { let api_path = verifier_management_api_path @@ -39,15 +37,14 @@ pub async fn register_client( let client = sqlx::query_as::<_, Client>( r#" INSERT INTO oauth2gw.clients - (client_id, secret_hash, webhook_url, verifier_url, verifier_management_api_path, redirect_uri, accepted_issuer_dids) - VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING id, client_id, secret_hash, webhook_url, verifier_url, + (client_id, secret_hash, verifier_url, verifier_management_api_path, redirect_uri, accepted_issuer_dids) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id, client_id, secret_hash, verifier_url, verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at "# ) .bind(client_id) .bind(secret_hash) - .bind(webhook_url) .bind(verifier_url) .bind(api_path) .bind(redirect_uri) @@ -65,7 +62,7 @@ pub async fn get_client_by_id( ) -> Result<Option<Client>> { let client = sqlx::query_as::<_, Client>( r#" - SELECT id, client_id, secret_hash, webhook_url, verifier_url, + SELECT id, client_id, secret_hash, verifier_url, verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at FROM oauth2gw.clients WHERE client_id = $1 @@ -85,7 +82,7 @@ pub async fn get_client_by_uuid( ) -> Result<Option<Client>> { let client = sqlx::query_as::<_, Client>( r#" - SELECT id, client_id, secret_hash, webhook_url, verifier_url, + SELECT id, client_id, secret_hash, verifier_url, verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at FROM oauth2gw.clients WHERE id = $1 @@ -129,7 +126,7 @@ pub async fn authenticate_client( ) -> Result<Option<Client>> { let client = sqlx::query_as::<_, Client>( r#" - SELECT id, client_id, secret_hash, webhook_url, verifier_url, + SELECT id, client_id, secret_hash, verifier_url, verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at FROM oauth2gw.clients WHERE client_id = $1 @@ -155,7 +152,6 @@ pub async fn authenticate_client( pub async fn update_client( pool: &PgPool, id: Uuid, - webhook_url: Option<&str>, verifier_url: Option<&str>, verifier_management_api_path: Option<&str>, redirect_uri: Option<&str>, @@ -164,29 +160,26 @@ pub async fn update_client( let current = get_client_by_uuid(pool, id).await? .ok_or_else(|| anyhow::anyhow!("Client not found"))?; - let new_webhook_url = webhook_url.unwrap_or(&current.webhook_url); let new_verifier_url = verifier_url.unwrap_or(&current.verifier_url); let new_verifier_api_path = verifier_management_api_path .unwrap_or(&current.verifier_management_api_path); - let new_redirect_uri = redirect_uri.or(current.redirect_uri.as_deref()); + let new_redirect_uri = redirect_uri.unwrap_or(&current.redirect_uri); let new_accepted_issuer_dids = accepted_issuer_dids.or(current.accepted_issuer_dids.as_deref()); let client = sqlx::query_as::<_, Client>( r#" UPDATE oauth2gw.clients SET - webhook_url = $1, - verifier_url = $2, - verifier_management_api_path = $3, - redirect_uri = $4, - accepted_issuer_dids = $5, + verifier_url = $1, + verifier_management_api_path = $2, + redirect_uri = $3, + accepted_issuer_dids = $4, updated_at = CURRENT_TIMESTAMP - WHERE id = $6 - RETURNING id, client_id, secret_hash, webhook_url, verifier_url, + WHERE id = $5 + RETURNING id, client_id, secret_hash, verifier_url, verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at "# ) - .bind(new_webhook_url) .bind(new_verifier_url) .bind(new_verifier_api_path) .bind(new_redirect_uri) @@ -222,7 +215,7 @@ pub async fn delete_client( pub async fn list_clients(pool: &PgPool) -> Result<Vec<Client>> { let clients = sqlx::query_as::<_, Client>( r#" - SELECT id, client_id, secret_hash, webhook_url, verifier_url, + SELECT id, client_id, secret_hash, verifier_url, verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at FROM oauth2gw.clients ORDER BY created_at DESC diff --git a/kych_oauth2_gateway/src/handlers.rs b/kych_oauth2_gateway/src/handlers.rs @@ -862,26 +862,16 @@ pub async fn notification_webhook( let authorization_code = crypto::generate_authorization_code(state.config.crypto.authorization_code_bytes); // Construct GET request URL: redirect_uri?code=XXX&state=YYY - let redirect_uri = session_data.redirect_uri.as_ref() - .unwrap_or(&session_data.webhook_url); - let oauth_state = session_data.state.as_deref().unwrap_or(""); + let auth_code_ttl = state.config.crypto.authorization_code_ttl_minutes; - let webhook_url = format!( - "{}?code={}&state={}", - redirect_uri, - authorization_code, - oauth_state - ); - - // Update session, create auth code, and queue webhook (GET request, empty body) - match crate::db::sessions::verify_session_and_queue_notification( + // Update session and create auth code + match crate::db::sessions::verify_session_and_issue_code( &state.pool, session_data.session_id, new_status, &authorization_code, - 10, // 10 minutes for auth code expiry + auth_code_ttl, session_data.client_id, - &webhook_url, "", // Empty body for GET request swiyu_result.wallet_response.as_ref(), ) @@ -889,14 +879,14 @@ pub async fn notification_webhook( { Ok(code) => { tracing::info!( - "Session {} updated to {}, auth code created, webhook queued", + "Session {} updated to {}, auth code created", session_data.session_id, status_str ); tracing::debug!("Generated authorization code: {}", code); } Err(e) => { - tracing::error!("Failed to update session and queue notification: {}", e); + tracing::error!("Failed to update session with authorization code: {}", e); } }