kych

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

commit 2fc87ed6b66536ed62be9f2ee028eedf409dee93
parent 75cf00cfbe6d004077fc13d6948983e33e425e68
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Fri, 12 Dec 2025 19:34:38 +0100

oauth2_gateway: add clients conf file, fix client cli print

Diffstat:
Aoauth2_gateway/clients.conf.example | 24++++++++++++++++++++++++
Moauth2_gateway/config.ini.example | 20++++++++++++++++----
Moauth2_gateway/oauth2_gatewaydb/install_db.sh | 3---
Moauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql | 6++++++
Moauth2_gateway/src/bin/client_management_cli.rs | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Moauth2_gateway/src/db/clients.rs | 32+++++++++++++++++++++++---------
6 files changed, 241 insertions(+), 45 deletions(-)

diff --git a/oauth2_gateway/clients.conf.example b/oauth2_gateway/clients.conf.example @@ -0,0 +1,24 @@ +# OAuth2 Gateway Clients Configuration +# +# Use the CLI to sync this file to the database: +# oauth2-gateway-client sync clients.conf +# +# Each section defines a client configuration + +[client_example] +client_id = 1 +client_secret = 2 +webhook_url = +verifier_url = +verifier_management_api_path = /management/api/verifications +redirect_uri = +accepted_issuer_dids = did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527 + +# [another_client] +# client_id = client_staging_01 +# client_secret = another_secret +# webhook_url = https://staging.example.com/webhook +# verifier_url = https://verifier-staging.example.com +# verifier_management_api_path = /api/v1/verifications +# redirect_uri = https://staging.example.com/callback +# accepted_issuer_dids = did:key:staging1 diff --git a/oauth2_gateway/config.ini.example b/oauth2_gateway/config.ini.example @@ -1,7 +1,19 @@ [server] -# host = -# port = -# socket_path = +host = +port = +#socket_path = [database] -# url = +url = + +[crypto] +nonce_bytes = 32 +token_bytes = 32 +authorization_code_bytes = 32 + +[webhook_worker] +retry_delay_server_error = 60 +retry_delay_forbidden = 60 +retry_delay_other = 3600 +fallback_poll_secs = 300 +batch_size = 100 diff --git a/oauth2_gateway/oauth2_gatewaydb/install_db.sh b/oauth2_gateway/oauth2_gatewaydb/install_db.sh @@ -28,6 +28,3 @@ psql -d $DB_NAME -c "GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA oauth2gw TO psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES IN SCHEMA oauth2gw GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $DB_USER;" psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES IN SCHEMA oauth2gw GRANT USAGE, SELECT ON SEQUENCES TO $DB_USER;" -# insert test data -psql -d $DB_NAME -c "INSERT INTO oauth2gw.clients (client_id, secret_hash, webhook_url, verifier_url, verifier_management_api_path) VALUES ('test-exchange', 'test-secret-hash', 'http://localhost:9090/kyc/webhook', 'http://localhost:8080', '/management/api/verifications');" - diff --git a/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql b/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql @@ -16,6 +16,8 @@ CREATE TABLE IF NOT EXISTS clients ( webhook_url TEXT NOT NULL, verifier_url TEXT NOT NULL, verifier_management_api_path VARCHAR(255) DEFAULT '/management/api/verifications', + redirect_uri TEXT, + accepted_issuer_dids TEXT, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); @@ -25,6 +27,10 @@ COMMENT ON COLUMN clients.client_id IS 'ID used for client identification before oauth2 gateway'; COMMENT ON COLUMN clients.secret_hash IS 'hash of shared secret used for client authentication before oauth2 gateway'; +COMMENT ON COLUMN clients.redirect_uri + IS 'Default OAuth2 redirect URI for this client'; +COMMENT ON COLUMN clients.accepted_issuer_dids + IS 'Comma-separated list of accepted DID issuers for credential verification'; COMMENT ON COLUMN clients.webhook_url IS 'Client URL where oauth2 gateway will callback'; COMMENT ON COLUMN clients.verifier_url diff --git a/oauth2_gateway/src/bin/client_management_cli.rs b/oauth2_gateway/src/bin/client_management_cli.rs @@ -12,8 +12,10 @@ use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; +use ini::Ini; use oauth2_gateway::db; use std::env; +use std::collections::HashSet; #[derive(Parser, Debug)] #[command(name = "client-mgmt")] @@ -54,6 +56,14 @@ enum Commands { /// Verifier management API path (default: /management/api/verifications) #[arg(long)] verifier_api_path: Option<String>, + + /// Default redirect URI for OAuth2 flow + #[arg(long)] + redirect_uri: Option<String>, + + /// Comma-separated list of accepted issuer DIDs + #[arg(long)] + accepted_issuer_dids: Option<String>, }, Update { @@ -67,6 +77,22 @@ enum Commands { #[arg(long)] verifier_api_path: Option<String>, + + #[arg(long)] + redirect_uri: Option<String>, + + #[arg(long)] + accepted_issuer_dids: Option<String>, + }, + + /// Sync clients from a configuration file + Sync { + /// Path to clients.conf file + config_file: String, + + /// Remove clients not in config file + #[arg(long)] + prune: bool, }, /// Delete a client (WARNING: cascades to all sessions) @@ -101,6 +127,8 @@ async fn main() -> Result<()> { webhook_url, verifier_url, verifier_api_path, + redirect_uri, + accepted_issuer_dids, } => { cmd_create_client( &pool, @@ -109,6 +137,8 @@ async fn main() -> Result<()> { &webhook_url, &verifier_url, verifier_api_path.as_deref(), + redirect_uri.as_deref(), + accepted_issuer_dids.as_deref(), ) .await? } @@ -117,6 +147,8 @@ async fn main() -> Result<()> { webhook_url, verifier_url, verifier_api_path, + redirect_uri, + accepted_issuer_dids, } => { cmd_update_client( &pool, @@ -124,9 +156,14 @@ async fn main() -> Result<()> { webhook_url.as_deref(), verifier_url.as_deref(), verifier_api_path.as_deref(), + redirect_uri.as_deref(), + accepted_issuer_dids.as_deref(), ) .await? } + Commands::Sync { config_file, prune } => { + cmd_sync_clients(&pool, &config_file, prune).await? + } Commands::Delete { client_id, yes } => { cmd_delete_client(&pool, &client_id, yes).await? } @@ -135,6 +172,22 @@ async fn main() -> Result<()> { Ok(()) } +fn print_client_details(client: &db::clients::Client) { + use chrono::Local; + + println!("{}", "=".repeat(60)); + 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!("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)); +} + async fn cmd_list_clients(pool: &sqlx::PgPool) -> Result<()> { let clients = db::clients::list_clients(pool).await?; @@ -143,17 +196,19 @@ async fn cmd_list_clients(pool: &sqlx::PgPool) -> Result<()> { return Ok(()); } - for client in clients { - println!("{}", "-".repeat(80)); - println!( - "UUID: {}\nCLIENT_ID: {}\nWEBHOOK_URL: {} \nCREATED: {}\n", - client.id, - client.client_id, - client.webhook_url, - client.created_at.format("%Y-%m-%d %H:%M") - ); + let total = clients.len(); + + for (i, client) in clients.iter().enumerate() { + if i > 0 { + println!(); + } + print_client_details(&client); } + println!(); + println!("{}", "=".repeat(60)); + println!("Total clients: {}", total); + Ok(()) } @@ -162,16 +217,7 @@ async fn cmd_show_client(pool: &sqlx::PgPool, client_id: &str) -> Result<()> { .await? .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; - println!("Client Details"); - println!("{}", "=".repeat(60)); - 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!("Created: {}", client.created_at); - println!("Updated: {}", client.updated_at); + print_client_details(&client); Ok(()) } @@ -183,6 +229,8 @@ async fn cmd_create_client( webhook_url: &str, verifier_url: &str, verifier_api_path: Option<&str>, + redirect_uri: Option<&str>, + accepted_issuer_dids: Option<&str>, ) -> Result<()> { let client = db::clients::register_client( pool, @@ -191,15 +239,15 @@ async fn cmd_create_client( webhook_url, verifier_url, verifier_api_path, + redirect_uri, + accepted_issuer_dids, ) .await .context("Failed to create client")?; println!("Client created successfully."); println!(); - println!("UUID: {}", client.id); - println!("Client ID: {}", client.client_id); - println!("Webhook URL: {}", client.webhook_url); + print_client_details(&client); Ok(()) } @@ -210,9 +258,12 @@ async fn cmd_update_client( 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() { - anyhow::bail!("No fields to update. Specify at least one of: --webhook-url, --verifier-url, --verifier-api-path"); + if webhook_url.is_none() && 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"); } let client = db::clients::get_client_by_id(pool, client_id) @@ -225,17 +276,15 @@ async fn cmd_update_client( webhook_url, verifier_url, verifier_api_path, + redirect_uri, + accepted_issuer_dids, ) .await .context("Failed to update client")?; println!("Client updated successfully."); println!(); - println!("UUID: {}", updated.id); - println!("Client ID: {}", updated.client_id); - println!("Webhook URL: {}", updated.webhook_url); - println!("Verifier URL: {}", updated.verifier_url); - println!("Verifier API Path: {}", updated.verifier_management_api_path); + print_client_details(&updated); Ok(()) } @@ -275,3 +324,97 @@ async fn cmd_delete_client(pool: &sqlx::PgPool, client_id: &str, skip_confirm: b Ok(()) } + +async fn cmd_sync_clients(pool: &sqlx::PgPool, config_file: &str, prune: bool) -> Result<()> { + println!("Loading clients from: {}", config_file); + + let ini = Ini::load_from_file(config_file) + .context("Failed to load configuration file")?; + + let mut synced_client_ids = HashSet::new(); + let mut created_count = 0; + let mut updated_count = 0; + + for (section_name, properties) in ini.iter() { + let section_name = match section_name { + Some(name) => name, + None => continue, + }; + + println!("\nProcessing section: [{}]", section_name); + + let client_id = properties.get("client_id") + .ok_or_else(|| anyhow::anyhow!("Missing client_id in section [{}]", section_name))?; + let client_secret = properties.get("client_secret") + .ok_or_else(|| anyhow::anyhow!("Missing client_secret in section [{}]", section_name))?; + let webhook_url = properties.get("webhook_url") + .ok_or_else(|| anyhow::anyhow!("Missing webhook_url in section [{}]", section_name))?; + let verifier_url = properties.get("verifier_url") + .ok_or_else(|| anyhow::anyhow!("Missing verifier_url in section [{}]", section_name))?; + + let verifier_api_path = properties.get("verifier_management_api_path"); + let redirect_uri = properties.get("redirect_uri"); + let accepted_issuer_dids = properties.get("accepted_issuer_dids"); + + synced_client_ids.insert(client_id.to_string()); + + let existing_client = db::clients::get_client_by_id(pool, client_id).await?; + + match existing_client { + Some(existing) => { + println!(" Client '{}' already exists, updating...", client_id); + db::clients::update_client( + pool, + existing.id, + Some(webhook_url), + Some(verifier_url), + verifier_api_path, + redirect_uri, + accepted_issuer_dids, + ) + .await + .context(format!("Failed to update client '{}'", client_id))?; + updated_count += 1; + println!(" Updated client '{}'", client_id); + } + None => { + println!(" Creating new client '{}'...", client_id); + db::clients::register_client( + pool, + client_id, + client_secret, + webhook_url, + verifier_url, + verifier_api_path, + redirect_uri, + accepted_issuer_dids, + ) + .await + .context(format!("Failed to create client '{}'", client_id))?; + created_count += 1; + println!(" Created client '{}'", client_id); + } + } + } + + if prune { + println!("\nPruning clients not in configuration file..."); + let all_clients = db::clients::list_clients(pool).await?; + let mut pruned_count = 0; + + for client in all_clients { + if !synced_client_ids.contains(&client.client_id) { + println!(" Deleting client '{}'...", client.client_id); + db::clients::delete_client(pool, client.id).await?; + pruned_count += 1; + } + } + println!("Pruned {} client(s)", pruned_count); + } + + println!("\nSync complete:"); + println!(" Created: {}", created_count); + println!(" Updated: {}", updated_count); + + Ok(()) +} diff --git a/oauth2_gateway/src/db/clients.rs b/oauth2_gateway/src/db/clients.rs @@ -14,6 +14,8 @@ pub struct Client { pub webhook_url: String, pub verifier_url: String, pub verifier_management_api_path: String, + pub redirect_uri: Option<String>, + pub accepted_issuer_dids: Option<String>, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, } @@ -26,6 +28,8 @@ pub async fn register_client( webhook_url: &str, verifier_url: &str, verifier_management_api_path: Option<&str>, + redirect_uri: Option<&str>, + accepted_issuer_dids: Option<&str>, ) -> Result<Client> { let api_path = verifier_management_api_path .unwrap_or("/management/api/verifications"); @@ -35,10 +39,10 @@ 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) - VALUES ($1, $2, $3, $4, $5) + (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, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at "# ) .bind(client_id) @@ -46,6 +50,8 @@ pub async fn register_client( .bind(webhook_url) .bind(verifier_url) .bind(api_path) + .bind(redirect_uri) + .bind(accepted_issuer_dids) .fetch_one(pool) .await?; @@ -60,7 +66,7 @@ pub async fn get_client_by_id( let client = sqlx::query_as::<_, Client>( r#" SELECT id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at FROM oauth2gw.clients WHERE client_id = $1 "# @@ -80,7 +86,7 @@ pub async fn get_client_by_uuid( let client = sqlx::query_as::<_, Client>( r#" SELECT id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at FROM oauth2gw.clients WHERE id = $1 "# @@ -124,7 +130,7 @@ pub async fn authenticate_client( let client = sqlx::query_as::<_, Client>( r#" SELECT id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at FROM oauth2gw.clients WHERE client_id = $1 "# @@ -152,6 +158,8 @@ pub async fn update_client( webhook_url: Option<&str>, verifier_url: Option<&str>, verifier_management_api_path: Option<&str>, + redirect_uri: Option<&str>, + accepted_issuer_dids: Option<&str>, ) -> Result<Client> { let current = get_client_by_uuid(pool, id).await? .ok_or_else(|| anyhow::anyhow!("Client not found"))?; @@ -160,6 +168,8 @@ pub async fn update_client( 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_accepted_issuer_dids = accepted_issuer_dids.or(current.accepted_issuer_dids.as_deref()); let client = sqlx::query_as::<_, Client>( r#" @@ -168,15 +178,19 @@ pub async fn update_client( webhook_url = $1, verifier_url = $2, verifier_management_api_path = $3, + redirect_uri = $4, + accepted_issuer_dids = $5, updated_at = CURRENT_TIMESTAMP - WHERE id = $4 + WHERE id = $6 RETURNING id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + 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) + .bind(new_accepted_issuer_dids) .bind(id) .fetch_one(pool) .await?; @@ -209,7 +223,7 @@ 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, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at FROM oauth2gw.clients ORDER BY created_at DESC "#