kych

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

commit 0dbf8467b074c683a6ba7e28ac8e10965870ba68
parent 70ba61ef26d99196ab86ff8564d0ed107ff06578
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Sun, 23 Nov 2025 16:53:03 +0100

documentation: cleanup old diagrams, old db tests

Diffstat:
Ddocumentation/onboarding_verifier.pdf | 0
Mdocumentation/sequence_diagrams/swiyu_taler_sequence_diagram.txt | 7+++++--
Ddocumentation/swiyu_taler_sequence_diagram.txt | 49-------------------------------------------------
Doauth2_gateway/tests/api_tests.rs | 316-------------------------------------------------------------------------------
4 files changed, 5 insertions(+), 367 deletions(-)

diff --git a/documentation/onboarding_verifier.pdf b/documentation/onboarding_verifier.pdf Binary files differ. diff --git a/documentation/sequence_diagrams/swiyu_taler_sequence_diagram.txt b/documentation/sequence_diagrams/swiyu_taler_sequence_diagram.txt @@ -25,8 +25,10 @@ sequenceDiagram Browser ->> Oauth2Gateway: Poll Verification Status Browser ->> SwiyuWallet: Open $VERIFICATION_URL - SwiyuWallet ->> SwiyuVerifier: GET /oid4vp/api/request-object/{request_id} - SwiyuVerifier -->> SwiyuWallet: OID4VP Request Object (DCQL query) + SwiyuWallet ->> SwiyuVerifier: GET /oid4vp/api/request-object/{request_id} (DCQL Query) + SwiyuVerifier -->> SwiyuWallet: verification presentation definition + SwiyuWallet ->> SwiyuVerifier: GET verifier_metadata + SwiyuVerifier -->> SwiyuWallet: return metadata SwiyuWallet ->> SwiyuWallet: Grant Permission SwiyuWallet ->> SwiyuVerifier: POST /oid4vp/api/request-object/{request_id}/response-data (VP Token) @@ -47,3 +49,4 @@ sequenceDiagram Exchange -->> TalerWallet: Notify success TalerWallet ->> Exchange: Retry original operation + diff --git a/documentation/swiyu_taler_sequence_diagram.txt b/documentation/swiyu_taler_sequence_diagram.txt @@ -1,49 +0,0 @@ -title [Tentative] Swiyu-Taler Interaction - -entryspacing 1.2 -participantgroup Taler User -participant Browser -participant TalerWallet -participant Exchange -participant Oauth2Gateway -participant SwiyuVerifier -participant SwiyuWallet - -TalerWallet->Exchange: Initiate KYC-required operation -Exchange->TalerWallet: Send verification link -TalerWallet->Browser: Open link -Browser->Exchange: Select verification method (Swiyu) - -note over Exchange,Oauth2Gateway: Exchange initiates KYC verification process -Exchange->Oauth2Gateway: POST /setup/$CLIENT_ID; -Oauth2Gateway->Exchange: $NONCE -Exchange->Browser: Send /authorize endpoint -Browser->Oauth2Gateway: GET /authorize/$NONCE... -Oauth2Gateway->SwiyuVerifier: POST /management/api/verifications -SwiyuVerifier->Oauth2Gateway: $VERIFICATION_URL, $REQUEST_ID -Oauth2Gateway->Browser: Send $VERIFICATION_URL -Browser->Oauth2Gateway: Poll Verification Status -Browser->SwiyuWallet: Open $VERIFICATION_URL -SwiyuWallet->SwiyuVerifier: GET /oid4vp/api/request-object/{request_id} -SwiyuVerifier->SwiyuWallet: OID4VP Request Object (DCQL query) -SwiyuWallet->SwiyuWallet: Grant Permission -SwiyuWallet->SwiyuVerifier: POST /oid4vp/api/request-object/{request_id}/response-data (VP Token) - -note over Oauth2Gateway,Exchange: Oauth2Gateway receives webhook and checks verification status -SwiyuVerifier->Oauth2Gateway: POST /notification {verification_id, timestamp} -Oauth2Gateway->SwiyuVerifier: GET /management/api/verifications/{verification_id} -SwiyuVerifier->Oauth2Gateway: {state: SUCCESS/FAILED/PENDING, wallet_response} -Oauth2Gateway->Browser: Notify verification result -Oauth2Gateway->Exchange: POST /oauth2gw/kyc/notify/$CLIENT_ID {status} - -note over Exchange,Oauth2Gateway: Exchange retrieves the final proof (Verifiable Credential) -Exchange->Oauth2Gateway: POST /token -Oauth2Gateway->Exchange: Access token -Exchange->Oauth2Gateway: GET /info (with access token) -Oauth2Gateway->SwiyuVerifier: GET /management/api/verifications/{verificationId} -SwiyuVerifier->Oauth2Gateway: Send proof (Verifiable Credential) -Oauth2Gateway->Exchange: Send proof (in response body) - -Exchange->TalerWallet: Notify success -TalerWallet->Exchange: Retry original operation - diff --git a/oauth2_gateway/tests/api_tests.rs b/oauth2_gateway/tests/api_tests.rs @@ -1,315 +0,0 @@ -use axum::{routing::*, Router}; -use axum_test::TestServer; -use oauth2_gateway::{config::*, db, handlers, models::*, state::AppState}; -use serde_json::json; -use wiremock::{MockServer, Mock, ResponseTemplate}; -use wiremock::matchers::{method, path}; -use serial_test::serial; - - -// API endpoint tests with Mocked Swiyu API and Database - -/// Helper to get test database URL -fn get_test_database_url() -> String { - std::env::var("TEST_DATABASE_URL") - .unwrap_or_else(|_| "postgresql://oauth2gw:password@localhost:5432/oauth2gw".to_string()) -} - -/// Helper to setup test database and clean data -async fn setup_test_db() -> sqlx::PgPool { - let pool = db::create_pool(&get_test_database_url()) - .await - .expect("Failed to connect to test database"); - clean_test_data(&pool).await; - pool -} - -/// Clean all test data (in correct FK order) -async fn clean_test_data(pool: &sqlx::PgPool) { - sqlx::query("DELETE FROM oauth2gw.notification_logs") - .execute(pool) - .await - .expect("Failed to clean notification_logs"); - sqlx::query("DELETE FROM oauth2gw.webhook_logs") - .execute(pool) - .await - .expect("Failed to clean webhook_logs"); - sqlx::query("DELETE FROM oauth2gw.access_tokens") - .execute(pool) - .await - .expect("Failed to clean access_tokens"); - sqlx::query("DELETE FROM oauth2gw.verification_sessions") - .execute(pool) - .await - .expect("Failed to clean verification_sessions"); - sqlx::query("DELETE FROM oauth2gw.clients") - .execute(pool) - .await - .expect("Failed to clean clients"); -} - -/// Helper to teardown test database -async fn teardown_test_db(pool: &sqlx::PgPool) { - clean_test_data(pool).await; -} - -/// Create test app with custom verifier base URL (for mock server) -async fn create_test_app(pool: sqlx::PgPool) -> Router { - let config = Config { - server: ServerConfig { - host: "127.0.0.1".to_string(), - port: 9090, - }, - database: DatabaseConfig { - url: get_test_database_url(), - }, - }; - - let state = AppState::new(config, pool); - - Router::new() - .route("/health", get(handlers::health_check)) - .route("/setup/{client_id}", post(handlers::setup)) - .route("/authorize/{nonce}", get(handlers::authorize)) - .route("/token", post(handlers::token)) - .route("/info", get(handlers::info)) - .route("/notification/{client_id}", post(handlers::notification_webhook)) - .with_state(state) -} - -#[tokio::test] -#[serial] -async fn test_setup_with_real_database() { - let pool = setup_test_db().await; - - db::clients::register_client( - &pool, - "test-client-1", - "secret123", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .expect("Failed to register client"); - - let app = create_test_app(pool.clone()).await; - let server = TestServer::new(app).unwrap(); - - let response = server - .post("/setup/test-client-1") - .json(&json!({"scope": "first_name last_name date_of_birth"})) - .await; - - response.assert_status_ok(); - let body: SetupResponse = response.json(); - assert!(!body.nonce.is_empty()); - - let session = db::sessions::get_session_by_nonce(&pool, &body.nonce) - .await - .expect("Failed to query session") - .expect("Session not found"); - - assert_eq!(session.scope, "first_name last_name date_of_birth"); - assert_eq!(session.status, db::sessions::SessionStatus::Pending); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_setup_with_nonexistent_client() { - let pool = setup_test_db().await; - let app = create_test_app(pool.clone()).await; - let server = TestServer::new(app).unwrap(); - - let response = server - .post("/setup/nonexistent-client") - .json(&json!({"scope": "test"})) - .await; - - response.assert_status(axum::http::StatusCode::NOT_FOUND); - let body: ErrorResponse = response.json(); - assert_eq!(body.error, "client_not_found"); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_authorize_successful_flow_with_mocked_swiyu() { - let pool = setup_test_db().await; - - let mock_server = MockServer::start().await; - - db::clients::register_client( - &pool, - "test-client-2", - "secret123", - "https://client.example.com/notify", - &mock_server.uri(), // Use mock verifier - Some("/management/api/verifications"), - ) - .await - .expect("Failed to register client"); - - // mock Swiyu response - Mock::given(method("POST")) - .and(path("/management/api/verifications")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "id": "550e8400-e29b-41d4-a716-446655440000", - "verification_url": "https://wallet.example.com/verify?request=abc123", - "verification_deeplink": "swiyu://verify/abc123", - "state": "PENDING", - "presentation_definition": { - "id": "test-pd-id", - "input_descriptors": [] - } - }))) - .mount(&mock_server) - .await; - - let app = create_test_app(pool.clone()).await; - let server = TestServer::new(app).unwrap(); - - let setup_response = server - .post("/setup/test-client-2") - .json(&json!({"scope": "first_name last_name"})) - .await; - - setup_response.assert_status_ok(); - let setup: SetupResponse = setup_response.json(); - - // Call authorize - let response = server - .get(&format!("/authorize/{}", setup.nonce)) - .await; - - response.assert_status_ok(); - let body: AuthorizeResponse = response.json(); - - assert_eq!(body.verification_url, "https://wallet.example.com/verify?request=abc123"); - assert_eq!(body.verification_id.to_string(), "550e8400-e29b-41d4-a716-446655440000"); - - let session = db::sessions::get_session_by_nonce(&pool, &setup.nonce) - .await - .expect("Failed to query session") - .expect("Session not found"); - - assert_eq!(session.status, db::sessions::SessionStatus::Authorized); - assert_eq!(session.verification_url, Some("https://wallet.example.com/verify?request=abc123".to_string())); - assert_eq!(session.request_id, Some("550e8400-e29b-41d4-a716-446655440000".to_string())); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_authorize_with_invalid_nonce() { - let pool = setup_test_db().await; - let app = create_test_app(pool.clone()).await; - let server = TestServer::new(app).unwrap(); - - let response = server - .get("/authorize/invalid-nonce-12345") - .await; - - response.assert_status(axum::http::StatusCode::NOT_FOUND); - let body: ErrorResponse = response.json(); - assert_eq!(body.error, "session_not_found"); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_authorize_with_expired_session() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "test-client-3", - "secret123", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .expect("Failed to register client"); - - // Create expired session (negative expiration) - sqlx::query( - r#" - INSERT INTO oauth2gw.verification_sessions (client_id, nonce, scope, expires_at, status) - VALUES ($1, $2, $3, CURRENT_TIMESTAMP - INTERVAL '1 hour', 'pending') - "# - ) - .bind(client.id) - .bind("expired-nonce") - .bind("test") - .execute(&pool) - .await - .expect("Failed to create expired session"); - - let app = create_test_app(pool.clone()).await; - let server = TestServer::new(app).unwrap(); - - let response = server - .get("/authorize/expired-nonce") - .await; - - response.assert_status(axum::http::StatusCode::BAD_REQUEST); - let body: ErrorResponse = response.json(); - assert_eq!(body.error, "session_expired"); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_authorize_with_swiyu_api_error() { - let pool = setup_test_db().await; - let mock_server = MockServer::start().await; - - - db::clients::register_client( - &pool, - "test-client-4", - "secret123", - "https://client.example.com/notify", - &mock_server.uri(), - Some("/management/api/verifications"), - ) - .await - .expect("Failed to register client"); - - // mock Swiyu response - Mock::given(method("POST")) - .and(path("/management/api/verifications")) - .respond_with(ResponseTemplate::new(500).set_body_json(json!({ - "error": "internal_server_error" - }))) - .mount(&mock_server) - .await; - - let app = create_test_app(pool.clone()).await; - let server = TestServer::new(app).unwrap(); - - // Setup and authorize - let setup_response = server - .post("/setup/test-client-4") - .json(&json!({"scope": "test"})) - .await; - - let setup: SetupResponse = setup_response.json(); - - let response = server - .get(&format!("/authorize/{}", setup.nonce)) - .await; - - response.assert_status(axum::http::StatusCode::BAD_GATEWAY); - let body: ErrorResponse = response.json(); - assert_eq!(body.error, "verifier_error"); - - teardown_test_db(&pool).await; -} -\ No newline at end of file