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:
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(¤t.webhook_url);
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_redirect_uri = redirect_uri.unwrap_or(¤t.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);
}
}