Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions pkg/openid4vp/claims_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,37 @@ func (ce *ClaimsExtractor) ExtractClaimsFromVPToken(ctx context.Context, vpToken
}

// extractClaimsFromDCQLResponse parses a DCQL vp_token response where the value
// is a JSON object mapping credential query IDs to their individual VP tokens.
// Claims from all credentials are merged into a single map.
// is a JSON object mapping credential query IDs to arrays of VP token strings
// (per OID4VP §6.3). Claims from all credentials are merged into a single map;
// credential query IDs are processed in sorted order for deterministic output.
func (ce *ClaimsExtractor) extractClaimsFromDCQLResponse(ctx context.Context, vpToken string) (map[string]any, error) {
var dcqlResponse map[string]string
var dcqlResponse map[string][]string
if err := json.Unmarshal([]byte(vpToken), &dcqlResponse); err != nil {
return nil, fmt.Errorf("failed to parse DCQL vp_token as JSON object: %w", err)
return nil, fmt.Errorf("failed to parse DCQL vp_token as map[string][]string: %w", err)
}

if len(dcqlResponse) == 0 {
return nil, fmt.Errorf("DCQL vp_token contains no credentials")
}

merged := make(map[string]any)
for credID, token := range dcqlResponse {
claims, err := ce.extractClaimsFromSingleToken(token)
if err != nil {
return nil, fmt.Errorf("failed to extract claims from credential %q: %w", credID, err)
credIDs := make([]string, 0, len(dcqlResponse))
for credID := range dcqlResponse {
credIDs = append(credIDs, credID)
}
slices.Sort(credIDs)
for _, credID := range credIDs {
tokens := dcqlResponse[credID]
if len(tokens) == 0 {
return nil, fmt.Errorf("DCQL vp_token contains empty array for credential %q", credID)
}
Comment thread
leifj marked this conversation as resolved.
for _, token := range tokens {
claims, err := ce.extractClaimsFromSingleToken(token)
if err != nil {
return nil, fmt.Errorf("failed to extract claims from credential %q: %w", credID, err)
}
maps.Copy(merged, claims)
}
maps.Copy(merged, claims)
}

return merged, nil
Expand Down
39 changes: 39 additions & 0 deletions pkg/openid4vp/claims_extractor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,3 +788,42 @@ func TestClaimsExtractor_ExtractClaimsFromVPToken_MDocFormat(t *testing.T) {
// Placeholder: in a full integration test we'd verify with actual mdoc tokens
_ = deviceResponse
}

func TestClaimsExtractor_ExtractClaimsFromVPToken_DCQLArray(t *testing.T) {
ce := NewClaimsExtractor()
ctx := t.Context()

t.Run("rejects_map_string_string_json", func(t *testing.T) {
// Old format: map[string]string — should fail because DCQL values must be arrays
vpToken := `{"cred1": "not-an-array"}`
_, err := ce.ExtractClaimsFromVPToken(ctx, vpToken)
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse DCQL vp_token")
})

t.Run("accepts_map_string_array_json", func(t *testing.T) {
// New format: map[string][]string — this is what spec-compliant wallets send
// The individual tokens will fail to parse (not real SD-JWTs), but the
// JSON unmarshal into map[string][]string must succeed
vpToken := `{"cred1": ["fake-token"]}`
_, err := ce.ExtractClaimsFromVPToken(ctx, vpToken)
require.Error(t, err)
// Error should be about parsing the token, NOT about JSON unmarshalling
assert.Contains(t, err.Error(), "failed to extract claims from credential")
assert.NotContains(t, err.Error(), "failed to parse DCQL vp_token")
})

t.Run("empty_array_rejected", func(t *testing.T) {
vpToken := `{"cred1": []}`
_, err := ce.ExtractClaimsFromVPToken(ctx, vpToken)
require.Error(t, err)
assert.Contains(t, err.Error(), "empty array")
})

t.Run("empty_object_rejected", func(t *testing.T) {
vpToken := `{}`
_, err := ce.ExtractClaimsFromVPToken(ctx, vpToken)
require.Error(t, err)
assert.Contains(t, err.Error(), "no credentials")
})
}