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
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dev.containers.mountWaylandSocket": false
},
"extensions": [
"golang.Go",
"GitHub.vscode-pull-request-github",
"streetsidesoftware.code-spell-checker",
"DavidAnson.vscode-markdownlint",
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"cryptosuite",
"datastoreclient",
"DCQL",
"DCQLJSON",
"deadcode",
"deployers",
"deviceauth",
Expand Down
2 changes: 1 addition & 1 deletion internal/apigw/apiv1/handlers_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (c *Client) VerificationRequestObject(ctx context.Context, req *Verificatio
claimQueries := make([]openid4vp.ClaimQuery, 0, len(vpAuth.AuthClaims))
for _, claim := range vpAuth.AuthClaims {
claimQueries = append(claimQueries, openid4vp.ClaimQuery{
Path: []string{claim},
Path: openid4vp.StringPath(claim),
})
}

Expand Down
23 changes: 15 additions & 8 deletions internal/verifier/apiv1/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,12 @@ func New(ctx context.Context, db *db.Service, notify *notify.Service, cacheServi
// Initialize claims extractor
c.claimsExtractor = openid4vp.NewClaimsExtractor()

// Override Attributes with filtered variant (excludes nested object claims)
// since verifier only exposes leaf-level attributes to the UI.
// Use full Attributes (including nested object/array claims) so the UI
// can render them as a tree and let users select individual sub-fields.
for _, credentialInfo := range cfg.Common.CredentialMetadata {
credentialInfo.Attributes = credentialInfo.VCTM.AttributesWithoutObjects()
if vctm := credentialInfo.GetVCTM(); vctm != nil {
credentialInfo.Attributes = vctm.Attributes()
}
}
Comment thread
masv3971 marked this conversation as resolved.

c.trustService = &openid4vp.TrustService{}
Expand Down Expand Up @@ -333,12 +335,11 @@ func (c *Client) buildDCQLQueryFromConfig(scopes []string) (*openid4vp.DCQL, err
// Add claims from VCTM claim paths
if credInfo.VCTM != nil {
for _, claim := range credInfo.VCTM.Claims {
// Skip object claims (nested paths) — only leaf claims
if len(claim.Path) != 1 || claim.Path[0] == nil {
if len(claim.Path) == 0 {
continue
Comment thread
masv3971 marked this conversation as resolved.
}
cred.Claims = append(cred.Claims, openid4vp.ClaimQuery{
Path: []string{*claim.Path[0]},
Path: claim.Path,
})
}
}
Expand All @@ -350,9 +351,15 @@ func (c *Client) buildDCQLQueryFromConfig(scopes []string) (*openid4vp.DCQL, err
return nil, fmt.Errorf("no valid credentials found for requested scopes")
}

return &openid4vp.DCQL{
dcql := &openid4vp.DCQL{
Credentials: credentials,
}, nil
}

// Normalize: remove redundant parent paths that are superseded by more
// specific child or array-element paths (same logic used in UI queries).
c.augmentDCQLFromVCTM(dcql)

return dcql, nil
}

// extractAndMapClaims extracts claims from a VP token and maps them to OIDC claims
Expand Down
30 changes: 29 additions & 1 deletion internal/verifier/apiv1/handler_session_preference.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func (c *Client) GetCredentialDisplayData(ctx context.Context, req *GetCredentia
response := &GetCredentialDisplayDataResponse{
SessionID: authCtx.SessionID,
VPToken: authCtx.VPToken,
Claims: authCtx.VerifiedClaims,
Claims: flattenClaimsForDisplay(authCtx.VerifiedClaims),
ClientID: authCtx.ClientID,
RedirectURI: authCtx.RedirectURI,
State: authCtx.State,
Expand All @@ -201,3 +201,31 @@ func (c *Client) GetCredentialDisplayData(ctx context.Context, req *GetCredentia

return response, nil
}

// flattenClaimsForDisplay flattens nested maps into dot-notation keys for display.
// For example, {"address": {"street": "Main St"}} becomes {"address.street": "Main St"}.
// Non-map values are kept as-is. Arrays are kept as-is (rendered by the template).
func flattenClaimsForDisplay(claims map[string]any) map[string]any {
if claims == nil {
return nil
}
result := make(map[string]any)
flattenRecursive(result, "", claims)
return result
}

func flattenRecursive(result map[string]any, prefix string, m map[string]any) {
for key, value := range m {
fullKey := key
if prefix != "" {
fullKey = prefix + "." + key
}

switch v := value.(type) {
case map[string]any:
flattenRecursive(result, fullKey, v)
default:
result[fullKey] = value
}
}
}
83 changes: 83 additions & 0 deletions internal/verifier/apiv1/handler_session_preference_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package apiv1

import (
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -279,3 +280,85 @@

return authCtx
}

func TestFlattenClaimsForDisplay(t *testing.T) {

Check failure on line 284 in internal/verifier/apiv1/handler_session_preference_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=SUNET_vc&issues=AZ6uN4d35U_DIMxdNK5e&open=AZ6uN4d35U_DIMxdNK5e&pullRequest=469
tests := []struct {
name string
input map[string]any
expect map[string]any
}{
{
name: "nil input",
input: nil,
expect: nil,
},
{
name: "flat claims unchanged",
input: map[string]any{
"given_name": "Helen",
"birthdate": "1996-01-30",
},
expect: map[string]any{
"given_name": "Helen",
"birthdate": "1996-01-30",
},
},
{
name: "nested maps flattened",
input: map[string]any{
"given_name": "Helen",
"place_of_birth": map[string]any{
"locality": "Stockholm",
"country": "SE",
},
"address": map[string]any{
"street_address": "Tulegatan",
"postal_code": "11353",
},
},
expect: map[string]any{
"given_name": "Helen",
"place_of_birth.locality": "Stockholm",
"place_of_birth.country": "SE",
"address.street_address": "Tulegatan",
"address.postal_code": "11353",
},
},
{
name: "arrays preserved as-is",
input: map[string]any{
"nationalities": []any{"SE", "EU"},
},
expect: map[string]any{
"nationalities": []any{"SE", "EU"},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := flattenClaimsForDisplay(tt.input)
if tt.expect == nil {
if result != nil {
t.Errorf("expected nil, got %v", result)
}
return
}
if len(result) != len(tt.expect) {
t.Errorf("expected %d keys, got %d: %v", len(tt.expect), len(result), result)
return
}
for k, v := range tt.expect {
got, ok := result[k]
if !ok {
t.Errorf("missing key %q in result", k)
continue
}
// Compare string representations for simplicity
if fmt.Sprintf("%v", got) != fmt.Sprintf("%v", v) {
t.Errorf("key %q: expected %v, got %v", k, v, got)
}
}
})
}
}
Loading