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:
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(¤t.verifier_url);
let new_verifier_api_path = verifier_management_api_path
.unwrap_or(¤t.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
"#