kych

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

commit ab54d2589a575f587c6ac3eea3d3a8bfec93fbc3
parent fb936816d34f7d40a0fd0bd5a356459c02fe8290
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Sun, 23 Nov 2025 19:02:26 +0100

oauth2_gateway: fix verification request misconfiguration

Diffstat:
Moauth2_gateway/scripts/setup_test_db.sh | 23++++-------------------
Moauth2_gateway/scripts/teardown_test_db.sh | 14++------------
Moauth2_gateway/src/db/sessions.rs | 4+++-
Moauth2_gateway/src/handlers.rs | 124+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Moauth2_gateway/src/models.rs | 5++++-
5 files changed, 90 insertions(+), 80 deletions(-)

diff --git a/oauth2_gateway/scripts/setup_test_db.sh b/oauth2_gateway/scripts/setup_test_db.sh @@ -27,29 +27,19 @@ if [ -n "$DB_ADMIN" ]; then PSQL_CMD="$PSQL_CMD -U $DB_ADMIN" fi -# Create user if not exists -echo "Creating database user..." $PSQL_CMD -d postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" | grep -q 1 || \ $PSQL_CMD -d postgres -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';" -# Create database if not exists -echo "Creating database..." $PSQL_CMD -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1 || \ $PSQL_CMD -d postgres -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" -# Grant privileges -echo "Granting privileges..." $PSQL_CMD -d "$DB_NAME" -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;" -# Set connection URL DB_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}" -# Install versioning system (idempotent - safe to run multiple times) -echo "Installing depesz versioning system..." -MIGRATIONS_DIR="$(dirname "$0")/../migrations" +MIGRATIONS_DIR="$(dirname "$0")/../oauth2_gatewaydb" $PSQL_CMD -d "$DB_NAME" -f "$MIGRATIONS_DIR/versioning.sql" -# Apply patches (versioning.sql handles duplicate detection) echo "Applying database patches..." for patch_file in "$MIGRATIONS_DIR"/oauth2gw-*.sql; do if [ -f "$patch_file" ]; then @@ -59,29 +49,24 @@ for patch_file in "$MIGRATIONS_DIR"/oauth2gw-*.sql; do fi done -# Grant schema privileges (minimum required for gateway to operate) -echo "Granting schema privileges..." $PSQL_CMD -d "$DB_NAME" -c "GRANT USAGE ON SCHEMA oauth2gw TO $DB_USER;" $PSQL_CMD -d "$DB_NAME" -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA oauth2gw TO $DB_USER;" $PSQL_CMD -d "$DB_NAME" -c "GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA oauth2gw TO $DB_USER;" $PSQL_CMD -d "$DB_NAME" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA oauth2gw GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $DB_USER;" $PSQL_CMD -d "$DB_NAME" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA oauth2gw GRANT USAGE, SELECT ON SEQUENCES TO $DB_USER;" -# Seed test data -echo "Seeding test data..." +echo "Seeding test data" $PSQL_CMD -d "$DB_NAME" <<EOF -INSERT INTO oauth2gw.clients (client_id, client_secret, notification_url, verifier_base_url, verifier_management_api_path) +INSERT INTO oauth2gw.clients (client_id, secret_hash, webhook_url, verifier_url, verifier_management_api_path) VALUES ( - 'test-exchange-001', + 'test-exchange', 'test-secret-123', 'http://localhost:9090/kyc/webhook', 'http://localhost:8080', '/management/api/verifications' ) -ON CONFLICT (client_id) DO NOTHING; EOF -echo echo "Setup completed." echo echo "Database: $DB_NAME" diff --git a/oauth2_gateway/scripts/teardown_test_db.sh b/oauth2_gateway/scripts/teardown_test_db.sh @@ -7,8 +7,6 @@ DB_ADMIN=${DB_ADMIN:-} echo "Tearing down OAuth2 Gateway test database..." echo -echo "WARNING: This will destroy all data!" -echo if ! command -v psql &> /dev/null then @@ -26,15 +24,7 @@ if [ -n "$DB_ADMIN" ]; then PSQL_CMD="$PSQL_CMD -U $DB_ADMIN" fi -echo "Dropping oauth2gw schema and unregistering all patches..." -MIGRATIONS_DIR="$(dirname "$0")/../migrations" -$PSQL_CMD -d "$DB_NAME" -f "$MIGRATIONS_DIR/drop.sql" +DB_DIR="$(dirname "$0")/../oauth2_gatewaydb" +$PSQL_CMD -d "$DB_NAME" -f "$DB_DIR/drop.sql" -echo echo "Teardown completed." -echo -echo "Schema dropped. Database '$DB_NAME' still exists but is empty." -echo "Run scripts/setup_test_db.sh to rebuild schema." -echo -echo "To completely remove the database, run:" -echo " psql -h localhost -p $DB_PORT -d postgres -c \"DROP DATABASE $DB_NAME;\"" diff --git a/oauth2_gateway/src/db/sessions.rs b/oauth2_gateway/src/db/sessions.rs @@ -262,6 +262,7 @@ pub async fn verify_session_and_queue_notification( client_id: Uuid, webhook_url: &str, webhook_body: &str, + verifiable_credential: Option<&serde_json::Value>, ) -> Result<String> { let timestamp_field = match status { SessionStatus::Verified => "verified_at", @@ -273,7 +274,7 @@ pub async fn verify_session_and_queue_notification( r#" WITH updated_session AS ( UPDATE oauth2gw.verification_sessions - SET status = $1, {} = NOW() + SET status = $1, {} = NOW(), verifiable_credential = $8 WHERE id = $2 RETURNING id ), @@ -298,6 +299,7 @@ pub async fn verify_session_and_queue_notification( .bind(client_id) .bind(webhook_url) .bind(webhook_body) + .bind(verifiable_credential) .fetch_one(pool) .await?; diff --git a/oauth2_gateway/src/handlers.rs b/oauth2_gateway/src/handlers.rs @@ -140,15 +140,19 @@ pub async fn authorize( data.verifier_management_api_path); let verifier_request = SwiyuCreateVerificationRequest { - accepted_issuer_dids: None, + accepted_issuer_dids: default_accepted_issuer_dids(), trust_anchors: None, jwt_secured_authorization_request: Some(true), - response_mode: ResponseMode::DirectPostJwt, + response_mode: ResponseMode::DirectPost, + response_type: "vp_token".to_string(), presentation_definition, configuration_override: ConfigurationOverride::default(), dcql_query: None, }; + // convert verification request to json for debug view + tracing::debug!("Swiyu verifier request: {}", serde_json::to_string_pretty(&verifier_request).unwrap()); + tracing::debug!("Calling Swiyu verifier at: {}", verifier_url); let verifier_response = state.http_client @@ -205,26 +209,39 @@ pub async fn authorize( /// Build a presentation definition from a space-delimited scope string /// -/// Example: "first_name last_name date_of_birth" +/// Example: "age_over_18" or "first_name last_name" fn build_presentation_definition(scope: &str) -> PresentationDefinition { use uuid::Uuid; use std::collections::HashMap; let attributes: Vec<&str> = scope.split_whitespace().collect(); - tracing::debug!("Building presentation definition for attributes: {:?}", + tracing::debug!("Building presentation definition for attributes: {:?}", attributes); - let fields: Vec<Field> = attributes - .iter() - .map(|attr| Field { + // First field: $.vct with filter for credential type + let vct_field = Field { + path: vec!["$.vct".to_string()], + id: None, + name: None, + purpose: None, + filter: Some(Filter { + filter_type: "string".to_string(), + const_value: Some("betaid-sdjwt".to_string()), + }), + }; + + // Attribute fields from scope + let mut fields: Vec<Field> = vec![vct_field]; + for attr in &attributes { + fields.push(Field { path: vec![format!("$.{}", attr)], id: None, - name: Some(attr.to_string()), + name: None, purpose: None, filter: None, - }) - .collect(); + }); + } let mut format = HashMap::new(); format.insert( @@ -237,17 +254,17 @@ fn build_presentation_definition(scope: &str) -> PresentationDefinition { let input_descriptor = InputDescriptor { id: Uuid::new_v4().to_string(), - name: Some("Requested credentials".to_string()), - purpose: Some("KYC verification via OAuth2 Gateway".to_string()), - format: Some(format.clone()), + name: None, + purpose: None, + format: Some(format), constraints: Constraint { fields }, }; PresentationDefinition { id: Uuid::new_v4().to_string(), - name: Some("OAuth2 Gateway KYC Verification".to_string()), - purpose: Some("Verify user credentials for Taler Exchange".to_string()), - format: Some(format), + name: Some("Over 18 Verification".to_string()), + purpose: Some("Verify age is over 18".to_string()), + format: None, // No format at top level input_descriptors: vec![input_descriptor], } } @@ -508,6 +525,7 @@ pub async fn notification_webhook( session_data.client_id, &session_data.webhook_url, &webhook_body, + swiyu_result.wallet_response.as_ref(), ).await { Ok(code) => { tracing::info!( @@ -530,19 +548,28 @@ mod tests { #[test] fn test_build_presentation_definition_single_attribute() { - let scope = "first_name"; + let scope = "age_over_18"; let pd = build_presentation_definition(scope); // Verify structure assert!(!pd.id.is_empty()); - assert_eq!(pd.name, Some("OAuth2 Gateway KYC Verification".to_string())); + assert_eq!(pd.name, Some("Over 18 Verification".to_string())); assert_eq!(pd.input_descriptors.len(), 1); - // Verify fields + // Verify fields: vct filter + requested attribute let fields = &pd.input_descriptors[0].constraints.fields; - assert_eq!(fields.len(), 1); - assert_eq!(fields[0].path, vec!["$.first_name"]); - assert_eq!(fields[0].name, Some("first_name".to_string())); + assert_eq!(fields.len(), 2); + + // First field is vct with filter + assert_eq!(fields[0].path, vec!["$.vct"]); + assert!(fields[0].filter.is_some()); + let filter = fields[0].filter.as_ref().unwrap(); + assert_eq!(filter.filter_type, "string"); + assert_eq!(filter.const_value, Some("betaid-sdjwt".to_string())); + + // Second field is the requested attribute + assert_eq!(fields[1].path, vec!["$.age_over_18"]); + assert!(fields[1].filter.is_none()); } #[test] @@ -551,11 +578,13 @@ mod tests { let pd = build_presentation_definition(scope); let fields = &pd.input_descriptors[0].constraints.fields; - assert_eq!(fields.len(), 3); + // vct + 3 attributes = 4 fields + assert_eq!(fields.len(), 4); - assert_eq!(fields[0].path, vec!["$.first_name"]); - assert_eq!(fields[1].path, vec!["$.last_name"]); - assert_eq!(fields[2].path, vec!["$.date_of_birth"]); + assert_eq!(fields[0].path, vec!["$.vct"]); // vct first + assert_eq!(fields[1].path, vec!["$.first_name"]); + assert_eq!(fields[2].path, vec!["$.last_name"]); + assert_eq!(fields[3].path, vec!["$.date_of_birth"]); } #[test] @@ -565,9 +594,11 @@ mod tests { let fields = &pd.input_descriptors[0].constraints.fields; // split_whitespace handles multiple spaces correctly - assert_eq!(fields.len(), 2); - assert_eq!(fields[0].path, vec!["$.first_name"]); - assert_eq!(fields[1].path, vec!["$.last_name"]); + // vct + 2 attributes = 3 fields + assert_eq!(fields.len(), 3); + assert_eq!(fields[0].path, vec!["$.vct"]); + assert_eq!(fields[1].path, vec!["$.first_name"]); + assert_eq!(fields[2].path, vec!["$.last_name"]); } #[test] @@ -576,28 +607,23 @@ mod tests { let pd = build_presentation_definition(scope); let fields = &pd.input_descriptors[0].constraints.fields; - assert_eq!(fields.len(), 0); + // Only vct field when scope is empty + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].path, vec!["$.vct"]); } #[test] - fn test_build_presentation_definition_format() { - let scope = "first_name"; + fn test_build_presentation_definition_no_top_level_format() { + let scope = "age_over_18"; let pd = build_presentation_definition(scope); - // Verify format is present - assert!(pd.format.is_some()); - let format = pd.format.unwrap(); - - // Verify vc+sd-jwt format - assert!(format.contains_key("vc+sd-jwt")); - let alg = &format["vc+sd-jwt"]; - assert_eq!(alg.sd_jwt_alg_values, vec!["ES256"]); - assert_eq!(alg.kb_jwt_alg_values, vec!["ES256"]); + // No format at top level + assert!(pd.format.is_none()); } #[test] fn test_build_presentation_definition_input_descriptor_structure() { - let scope = "first_name"; + let scope = "age_over_18"; let pd = build_presentation_definition(scope); let descriptor = &pd.input_descriptors[0]; @@ -605,12 +631,16 @@ mod tests { // Verify descriptor has valid UUID assert!(!descriptor.id.is_empty()); - // Verify descriptor has proper metadata - assert_eq!(descriptor.name, Some("Requested credentials".to_string())); - assert_eq!(descriptor.purpose, - Some("KYC verification via OAuth2 Gateway".to_string())); + // Verify no name/purpose at descriptor level + assert!(descriptor.name.is_none()); + assert!(descriptor.purpose.is_none()); - // Verify format is specified at descriptor level too + // Verify format is specified at descriptor level assert!(descriptor.format.is_some()); + let format = descriptor.format.as_ref().unwrap(); + assert!(format.contains_key("vc+sd-jwt")); + let alg = &format["vc+sd-jwt"]; + assert_eq!(alg.sd_jwt_alg_values, vec!["ES256"]); + assert_eq!(alg.kb_jwt_alg_values, vec!["ES256"]); } } diff --git a/oauth2_gateway/src/models.rs b/oauth2_gateway/src/models.rs @@ -84,7 +84,7 @@ impl ErrorResponse { // Swiyu Verifier API models /// Default issuer DID for Swiyu verification -fn default_accepted_issuer_dids() -> Vec<String> { +pub fn default_accepted_issuer_dids() -> Vec<String> { vec!["did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527".to_string()] } @@ -106,6 +106,9 @@ pub struct SwiyuCreateVerificationRequest { /// REQUIRED - will throw NullPointerException if omitted pub response_mode: ResponseMode, + /// Response type - must be "vp_token" for VP requests + pub response_type: String, + pub presentation_definition: PresentationDefinition, pub configuration_override: ConfigurationOverride,