kych

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

commit 6fd2c52c2eb7c210c761e31834cd2500ff193c55
parent 3f3b73974aeba8c26731bb3428867b26c9a525d4
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Mon, 19 Jan 2026 21:35:14 +0100

Cleanup deps and add configurable auth code TTL

Remove dotenv and unused development dependencies, delete obsolete tests, and
introduce a configurable authorization code TTL via AUTH_CODE_TTL_MINUTES
(default 10). Update example configuration accordingly and ensure the gateway
builds cleanly.

Diffstat:
Mkych_oauth2_gateway/Cargo.toml | 10----------
Dkych_oauth2_gateway/env.example | 8--------
Akych_oauth2_gateway/kych.conf.example | 28++++++++++++++++++++++++++++
Mkych_oauth2_gateway/src/db/sessions.rs | 11+++++------
Dkych_oauth2_gateway/tests/client_cli.rs | 271-------------------------------------------------------------------------------
Dkych_oauth2_gateway/tests/db.rs | 443-------------------------------------------------------------------------------
6 files changed, 33 insertions(+), 738 deletions(-)

diff --git a/kych_oauth2_gateway/Cargo.toml b/kych_oauth2_gateway/Cargo.toml @@ -19,9 +19,7 @@ path = "src/bin/client_management_cli.rs" [dependencies] # Web framework axum = "0.8.6" -axum-test = "18.1.0" tokio = { version = "1.48.0", features = ["full"] } -tower = "0.5" tower-http = { version = "0.6.6", features = ["trace", "fs"] } # Serialization @@ -46,9 +44,6 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter", "local-time # Error handling anyhow = "1.0.100" -# Environment -dotenvy = "0.15" - # Cryptography rand = "0.8.5" bcrypt = "0.15" @@ -62,8 +57,3 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chro # Templates askama = "0.12" - -[dev-dependencies] -tempfile = "3.8" -wiremock = "0.6" -serial_test = "3.2.0" diff --git a/kych_oauth2_gateway/env.example b/kych_oauth2_gateway/env.example @@ -1,8 +0,0 @@ -DB_PORT="" -DB_NAME="" -DB_USER="" -DB_PASS="" - -DATABASE_URL="" -TEST_DATABASE_URL="" - diff --git a/kych_oauth2_gateway/kych.conf.example b/kych_oauth2_gateway/kych.conf.example @@ -0,0 +1,28 @@ +[kych-oauth2-gateway] +#HOST = +#PORT = +UNIXPATH = +UNIXPATH_MODE = 666 +DATABASE = +NONCE_BYTES = 32 +TOKEN_BYTES = 32 +AUTH_CODE_BYTES = 32 +AUTH_CODE_TTL_MINUTES = 10 + +# ---- Clients (one section per client) ---- + +[client_example] +CLIENT_ID = 1 +CLIENT_SECRET = secret +VERIFIER_URL = https://swiyu-verifier9999.ch +VERIFIER_MANAGEMENT_API_PATH = /management/api/verifications +REDIRECT_URI = https://kych-oauth2-gateway-client.com/kych-providers/kych-redirect +ACCEPTED_ISSUER_DIDS = did:tdw:trust_this_issuer + +# [client_2] +# CLIENT_ID = client_staging_01 +# CLIENT_SECRET = another_secret +# 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/kych_oauth2_gateway/src/db/sessions.rs b/kych_oauth2_gateway/src/db/sessions.rs @@ -75,7 +75,7 @@ pub struct NotificationSessionData { pub state: Option<String>, // Client fields pub client_id: Uuid, - pub webhook_url: String, + pub allowed_redirect_uris: Option<String>, pub verifier_url: String, pub verifier_management_api_path: String, } @@ -212,7 +212,7 @@ pub async fn get_session_for_notification( s.redirect_uri, s.state, c.id AS client_id, - c.webhook_url, + c.redirect_uri AS allowed_redirect_uris, c.verifier_url, c.verifier_management_api_path "# @@ -230,7 +230,7 @@ pub async fn get_session_for_notification( redirect_uri: row.get("redirect_uri"), state: row.get("state"), client_id: row.get("client_id"), - webhook_url: row.get("webhook_url"), + allowed_redirect_uris: row.get("allowed_redirect_uris"), verifier_url: row.get("verifier_url"), verifier_management_api_path: row.get("verifier_management_api_path"), } @@ -324,15 +324,14 @@ pub async fn update_session_authorized( /// Atomically update session to verified and create authorization code /// /// Returns the generated authorization code on success. -pub async fn verify_session_and_queue_notification( +pub async fn verify_session_and_issue_code( pool: &PgPool, session_id: Uuid, status: SessionStatus, authorization_code: &str, code_expires_in_minutes: i64, _client_id: Uuid, - _webhook_url: &str, - _webhook_body: &str, + _callback_body: &str, verifiable_credential: Option<&serde_json::Value>, ) -> Result<String> { let timestamp_field = match status { diff --git a/kych_oauth2_gateway/tests/client_cli.rs b/kych_oauth2_gateway/tests/client_cli.rs @@ -1,271 +0,0 @@ -//! Integration tests for client-mgmt CLI - -use oauth2_gateway::db; -use sqlx::PgPool; - -async fn setup_pool() -> PgPool { - let database_url = std::env::var("TEST_DATABASE_URL") - .unwrap_or_else(|_| "postgresql://oauth2gw:password@localhost:5432/oauth2gw".to_string()); - db::create_pool(&database_url).await.unwrap() -} - -async fn cleanup_clients(pool: &PgPool) { - sqlx::query("DELETE FROM oauth2gw.clients") - .execute(pool) - .await - .unwrap(); -} - -#[tokio::test] -#[serial_test::serial] -async fn test_cli_client_create_and_list() { - let pool = setup_pool().await; - cleanup_clients(&pool).await; - - // Create a client - let client = db::clients::register_client( - &pool, - "test-cli-client", - "secret123", - "https://example.com/webhook", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - assert_eq!(client.client_id, "test-cli-client"); - assert_eq!(client.webhook_url, "https://example.com/webhook"); - - // List clients - let clients = db::clients::list_clients(&pool).await.unwrap(); - assert_eq!(clients.len(), 1); - assert_eq!(clients[0].client_id, "test-cli-client"); - - cleanup_clients(&pool).await; -} - -#[tokio::test] -#[serial_test::serial] -async fn test_cli_client_show() { - let pool = setup_pool().await; - cleanup_clients(&pool).await; - - // Create a client - let created = db::clients::register_client( - &pool, - "show-test-client", - "secret456", - "https://example.com/hook", - "https://verifier.example.com", - Some("/custom/api/path"), - ) - .await - .unwrap(); - - // Show client by client_id - let found = db::clients::get_client_by_id(&pool, "show-test-client") - .await - .unwrap() - .unwrap(); - - assert_eq!(found.id, created.id); - assert_eq!(found.client_id, "show-test-client"); - assert_eq!(found.verifier_management_api_path, "/custom/api/path"); - - cleanup_clients(&pool).await; -} - -#[tokio::test] -#[serial_test::serial] -async fn test_cli_client_update() { - let pool = setup_pool().await; - cleanup_clients(&pool).await; - - // Create a client - let created = db::clients::register_client( - &pool, - "update-test-client", - "secret789", - "https://old-webhook.com", - "https://old-verifier.com", - None, - ) - .await - .unwrap(); - - // Update the client - let updated = db::clients::update_client( - &pool, - created.id, - Some("https://new-webhook.com"), - Some("https://new-verifier.com"), - Some("/new/api/path"), - ) - .await - .unwrap(); - - assert_eq!(updated.webhook_url, "https://new-webhook.com"); - assert_eq!(updated.verifier_url, "https://new-verifier.com"); - assert_eq!(updated.verifier_management_api_path, "/new/api/path"); - - cleanup_clients(&pool).await; -} - -#[tokio::test] -#[serial_test::serial] -async fn test_cli_client_update_partial() { - let pool = setup_pool().await; - cleanup_clients(&pool).await; - - // Create a client - let created = db::clients::register_client( - &pool, - "partial-update-client", - "secret", - "https://webhook.com", - "https://verifier.com", - None, - ) - .await - .unwrap(); - - // Update only webhook_url - let updated = db::clients::update_client( - &pool, - created.id, - Some("https://updated-webhook.com"), - None, - None, - ) - .await - .unwrap(); - - assert_eq!(updated.webhook_url, "https://updated-webhook.com"); - assert_eq!(updated.verifier_url, "https://verifier.com"); // unchanged - assert_eq!(updated.verifier_management_api_path, "/management/api/verifications"); // default - - cleanup_clients(&pool).await; -} - -#[tokio::test] -#[serial_test::serial] -async fn test_cli_client_delete() { - let pool = setup_pool().await; - cleanup_clients(&pool).await; - - // Create a client - let created = db::clients::register_client( - &pool, - "delete-test-client", - "secret", - "https://webhook.com", - "https://verifier.com", - None, - ) - .await - .unwrap(); - - // Verify it exists - let found = db::clients::get_client_by_id(&pool, "delete-test-client") - .await - .unwrap(); - assert!(found.is_some()); - - // Delete the client - let deleted = db::clients::delete_client(&pool, created.id).await.unwrap(); - assert!(deleted); - - // Verify it's gone - let not_found = db::clients::get_client_by_id(&pool, "delete-test-client") - .await - .unwrap(); - assert!(not_found.is_none()); - - cleanup_clients(&pool).await; -} - -#[tokio::test] -#[serial_test::serial] -async fn test_cli_client_not_found() { - let pool = setup_pool().await; - cleanup_clients(&pool).await; - - // Try to find non-existent client - let not_found = db::clients::get_client_by_id(&pool, "nonexistent-client") - .await - .unwrap(); - assert!(not_found.is_none()); - - cleanup_clients(&pool).await; -} - -#[tokio::test] -#[serial_test::serial] -async fn test_cli_client_duplicate_id() { - let pool = setup_pool().await; - cleanup_clients(&pool).await; - - // Create first client - db::clients::register_client( - &pool, - "duplicate-client", - "secret1", - "https://webhook1.com", - "https://verifier1.com", - None, - ) - .await - .unwrap(); - - // Try to create duplicate - let result = db::clients::register_client( - &pool, - "duplicate-client", - "secret2", - "https://webhook2.com", - "https://verifier2.com", - None, - ) - .await; - - assert!(result.is_err()); - - cleanup_clients(&pool).await; -} - -#[tokio::test] -#[serial_test::serial] -async fn test_cli_list_empty() { - let pool = setup_pool().await; - cleanup_clients(&pool).await; - - let clients = db::clients::list_clients(&pool).await.unwrap(); - assert!(clients.is_empty()); -} - -#[tokio::test] -#[serial_test::serial] -async fn test_cli_list_multiple_clients() { - let pool = setup_pool().await; - cleanup_clients(&pool).await; - - // Create multiple clients - for i in 1..=3 { - db::clients::register_client( - &pool, - &format!("client-{}", i), - &format!("secret-{}", i), - &format!("https://webhook{}.com", i), - &format!("https://verifier{}.com", i), - None, - ) - .await - .unwrap(); - } - - let clients = db::clients::list_clients(&pool).await.unwrap(); - assert_eq!(clients.len(), 3); - - cleanup_clients(&pool).await; -} diff --git a/kych_oauth2_gateway/tests/db.rs b/kych_oauth2_gateway/tests/db.rs @@ -1,443 +0,0 @@ -// Database tests for OAuth2 Gateway -// Requires TEST_DATABASE_URL environment variable or uses default connection. - -use oauth2_gateway::db; -use sqlx::PgPool; -use serial_test::serial; - -fn get_test_database_url() -> String { - std::env::var("TEST_DATABASE_URL") - .unwrap_or_else(|_| "postgresql://oauth2gw:password@localhost:5432/oauth2gw".to_string()) -} - -async fn setup_test_db() -> PgPool { - let pool = db::create_pool(&get_test_database_url()) - .await - .expect("Failed to connect to test database"); - clean_test_data(&pool).await; - pool -} - -async fn clean_test_data(pool: &PgPool) { - let _ = sqlx::query("DELETE FROM oauth2gw.notification_pending_webhooks").execute(pool).await; - let _ = sqlx::query("DELETE FROM oauth2gw.authorization_codes").execute(pool).await; - let _ = sqlx::query("DELETE FROM oauth2gw.access_tokens").execute(pool).await; - let _ = sqlx::query("DELETE FROM oauth2gw.verification_sessions").execute(pool).await; - let _ = sqlx::query("DELETE FROM oauth2gw.clients").execute(pool).await; -} - -async fn teardown_test_db(pool: &PgPool) { - clean_test_data(pool).await; -} - -#[tokio::test] -#[serial] -async fn test_client_registration() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "test-exchange-1", - "secret123", - "https://exchange.example.com/kyc/webhook", - "https://verifier.swiyu.io", - None, - ) - .await - .expect("Failed to register client"); - - assert_eq!(client.client_id, "test-exchange-1"); - assert_eq!(client.webhook_url, "https://exchange.example.com/kyc/webhook"); - assert_eq!(client.verifier_url, "https://verifier.swiyu.io"); - assert_eq!(client.verifier_management_api_path, "/management/api/verifications"); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_client_lookup_by_client_id() { - let pool = setup_test_db().await; - - let registered = db::clients::register_client( - &pool, - "lookup-test", - "secret456", - "https://example.com/webhook", - "https://verifier.example.com", - Some("/custom/path"), - ) - .await - .unwrap(); - - let found = db::clients::get_client_by_id(&pool, "lookup-test") - .await - .unwrap() - .expect("Client not found"); - - assert_eq!(found.id, registered.id); - assert_eq!(found.verifier_management_api_path, "/custom/path"); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_client_not_found() { - let pool = setup_test_db().await; - - let result = db::clients::get_client_by_id(&pool, "nonexistent") - .await - .unwrap(); - - assert!(result.is_none()); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_session_creation() { - let pool = setup_test_db().await; - - let _client = db::clients::register_client( - &pool, - "session-client", - "secret", - "https://example.com/webhook", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - "session-client", - "nonce-abc123", - "first_name last_name age_over_18", - 15, - ) - .await - .expect("Failed to create session") - .expect("Session should be created"); - - assert_eq!(session.nonce, "nonce-abc123"); - assert_eq!(session.scope, "first_name last_name age_over_18"); - assert_eq!(session.status, db::sessions::SessionStatus::Pending); - assert!(session.verification_url.is_none()); - assert!(session.request_id.is_none()); - assert!(session.verifier_nonce.is_none()); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_get_session_for_authorize() { - let pool = setup_test_db().await; - - let _client = db::clients::register_client( - &pool, - "authorize-client", - "secret", - "https://example.com/webhook", - "https://verifier.example.com", - Some("/custom/api/path"), - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - "authorize-client", - "authorize-test-nonce", - "first_name last_name", - 15, - ) - .await - .unwrap() - .unwrap(); - - // Fetch session with client data - let data = db::sessions::get_session_for_authorize( - &pool, - "authorize-test-nonce", - "authorize-client", - ) - .await - .unwrap() - .expect("Session should be found"); - - assert_eq!(data.session_id, session.id); - assert_eq!(data.status, db::sessions::SessionStatus::Pending); - assert_eq!(data.scope, "first_name last_name"); - assert_eq!(data.verifier_url, "https://verifier.example.com"); - assert_eq!(data.verifier_management_api_path, "/custom/api/path"); - assert!(data.verification_url.is_none()); - - // Wrong client_id should return None - let not_found = db::sessions::get_session_for_authorize( - &pool, - "authorize-test-nonce", - "wrong-client", - ) - .await - .unwrap(); - assert!(not_found.is_none()); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_authorization_code_creation_and_exchange() { - let pool = setup_test_db().await; - - let _client = db::clients::register_client( - &pool, - "code-client", - "secret", - "https://example.com/webhook", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - "code-client", - "code-nonce", - "scope", - 15, - ) - .await - .unwrap() - .unwrap(); - - // Create authorization code - let code = db::authorization_codes::create_authorization_code( - &pool, - session.id, - "auth-code-xyz123", - 10, - ) - .await - .unwrap(); - - assert_eq!(code.code, "auth-code-xyz123"); - assert_eq!(code.session_id, session.id); - assert!(!code.used); - - // Exchange code - first time should mark as used - let exchange1 = db::authorization_codes::get_code_for_token_exchange( - &pool, - "auth-code-xyz123", - ) - .await - .unwrap() - .expect("Code should be found"); - - assert!(!exchange1.was_already_used); // First use - assert_eq!(exchange1.session_id, session.id); - assert!(exchange1.existing_token.is_none()); - - // Exchange code - second time should show as already used - let exchange2 = db::authorization_codes::get_code_for_token_exchange( - &pool, - "auth-code-xyz123", - ) - .await - .unwrap() - .expect("Code should still be found"); - - assert!(exchange2.was_already_used); // Already used - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_create_token_and_complete_session() { - let pool = setup_test_db().await; - - let _client = db::clients::register_client( - &pool, - "complete-client", - "secret", - "https://example.com/webhook", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - "complete-client", - "complete-nonce", - "scope", - 15, - ) - .await - .unwrap() - .unwrap(); - - // Create token and complete session atomically - let token = db::tokens::create_token_and_complete_session( - &pool, - session.id, - "atomic-token-abc", - 3600, - ) - .await - .unwrap(); - - assert_eq!(token.token, "atomic-token-abc"); - assert_eq!(token.session_id, session.id); - - // Verify session was updated to completed - let data = db::sessions::get_session_for_authorize( - &pool, - "complete-nonce", - "complete-client", - ) - .await - .unwrap() - .unwrap(); - - assert_eq!(data.status, db::sessions::SessionStatus::Completed); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_notification_webhook_queue() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "webhook-client", - "secret", - "https://example.com/webhook", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - "webhook-client", - "webhook-nonce", - "scope", - 15, - ) - .await - .unwrap() - .unwrap(); - - // Create authorization code first (needed for webhook join) - db::authorization_codes::create_authorization_code( - &pool, - session.id, - "webhook-code", - 10, - ) - .await - .unwrap(); - - // Insert pending webhook - let serial = db::notification_webhooks::insert_pending_webhook( - &pool, - session.id, - client.id, - "https://example.com/webhook", - r#"{"nonce":"webhook-nonce","status":"verified"}"#, - ) - .await - .unwrap(); - - assert!(serial > 0); - - // Fetch pending webhooks - let pending = db::notification_webhooks::get_pending_webhooks(&pool, 100) - .await - .unwrap(); - - assert_eq!(pending.len(), 1); - assert_eq!(pending[0].session_id, session.id); - assert_eq!(pending[0].code, "webhook-code"); - assert_eq!(pending[0].url, "https://example.com/webhook"); - - // Delete webhook after "successful delivery" - let deleted = db::notification_webhooks::delete_webhook(&pool, serial) - .await - .unwrap(); - assert!(deleted); - - // Should be empty now - let pending_after = db::notification_webhooks::get_pending_webhooks(&pool, 100) - .await - .unwrap(); - assert!(pending_after.is_empty()); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_get_token_with_session() { - let pool = setup_test_db().await; - - let _client = db::clients::register_client( - &pool, - "info-client", - "secret", - "https://example.com/webhook", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - "info-client", - "info-nonce", - "scope", - 15, - ) - .await - .unwrap() - .unwrap(); - - // Create token - db::tokens::create_token_and_complete_session( - &pool, - session.id, - "info-token-xyz", - 3600, - ) - .await - .unwrap(); - - // Fetch token with session data - let data = db::tokens::get_token_with_session(&pool, "info-token-xyz") - .await - .unwrap() - .expect("Token should be found"); - - assert!(!data.revoked); - assert_eq!(data.session_status, db::sessions::SessionStatus::Completed); - - // Non-existent token returns None - let not_found = db::tokens::get_token_with_session(&pool, "nonexistent-token") - .await - .unwrap(); - assert!(not_found.is_none()); - - teardown_test_db(&pool).await; -}