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:
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;
-}