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:
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,