Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
11 changes: 5 additions & 6 deletions internal/verifier/apiv1/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ 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()
credentialInfo.Attributes = credentialInfo.VCTM.Attributes()
}
Comment thread
masv3971 marked this conversation as resolved.

c.trustService = &openid4vp.TrustService{}
Expand Down Expand Up @@ -333,12 +333,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 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