Skip to content

fix: release PID claims for the pid, pid_1_5 and pid_1_8 scopes - #22

Merged
leifj merged 1 commit into
mainfrom
fix/pid-scope-claims
Aug 24, 2026
Merged

leifj merged 1 commit into
mainfrom
fix/pid-scope-claims

Conversation

@leifj

@leifj leifj commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

The bug

pid, pid_1_5 and pid_1_8 are advertised in scopes_supported, but claimsForScopes has never had a handler for any of them. A request naming one — which is exactly what vc's apigw sends, one credential type at a time (Scopes: []string{session.CredentialType}) — matches none of profile/email/organisation/ehic, falls through to the backwards-compat block, and releases four claims:

map[email:... family_name:... given_name:... name:...]

No birthdate, no nationality, no place of birth, no issuing authority. Not enough to build a PID from, so the issuer rejects the credential request outright. Advertising a scope the OP has no claims for is the bug — a client has no way to discover that the scope is inert.

Why 1.5 and 1.8 need separate branches

They describe the same person under different claim names, and apigw uses the OIDC claims verbatim as the credential's document data. A claim under the other version's name simply isn't present to disclose, and a verifier's DCQL query for it won't match.

ARF 1.8 ARF 1.5
place_of_birth birth_place
nationalities (list) nationality (single)
age_equal_or_over.NN age_over_NN (flat booleans)

The unversioned pid scope follows ARF 1.8, the current version. A request naming several of these is not meaningful — they are three spellings of one credential — so the switch lets the most specific win rather than having one overwrite the other's differently-named claims. All three are added to the backwards-compat guard for the same reason ehic was: otherwise the block below overwrites what the handler just set.

Age attributes

Derived from birthdate rather than stored per user, so they can't go stale as the fixture data ages. Without them a verifier asking for age_over_18 (1.5) or age_equal_or_over.18 (1.8) finds nothing to match, since DCQL requires every requested claim to be present.

The birthday comparison uses month/day, not day-of-year: in a leap year every date after February shifts by one day-of-year, which would report the wrong age for a full day around each such birthday.

Testing

go build, go vet, gofmt and the full suite pass. New coverage:

  • both vocabularies release their own names and none of the other's
  • the unversioned scope tracks 1.8
  • a user with no birthdate yields no age claims at all, rather than empty ones
  • the boundary case, on the day before / of / after a birthday
  • a fallthrough regression guard

The regression guard was verified against the previous behaviour by reverting only the dispatch (leaving the helper in place so it still compiles) — it fails and reproduces the exact four-claim map above.

Context

Found while debugging an unrelated credential-issuance failure. This one is currently dormant, not the cause of that: vc's apigw sends openid profile from auth_providers.oidc.scopes, so today it hits the profile branch. It becomes live the moment an issuer requests the credential type as the scope, as the ehic path already does.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cm287nVbhUoBhvVqSMbNCb

These three scopes have been advertised in scopes_supported all along with no
handler behind them in claimsForScopes. A request naming one - which is
exactly what vc's apigw sends, one credential type at a time
(Scopes: []string{session.CredentialType}) - matched none of
profile/email/organisation/ehic, fell through to the backwards-compat block
and released four claims:

    map[email:... family_name:... given_name:... name:...]

No birthdate, no nationality, no place of birth, no issuing authority. Not
enough to build a PID from, so the issuer rejects the credential request
outright. Advertising a scope the OP has no claims for is the bug; a client
has no way to discover that the scope is inert.

ARF 1.5 and ARF 1.8 describe the same person under different claim NAMES, and
apigw uses the OIDC claims verbatim as the credential's document data, so a
claim under the other version's name simply isn't present to disclose and a
verifier's DCQL query for it won't match. The two therefore get separate
branches rather than one shared set:

    ARF 1.8            ARF 1.5
    place_of_birth     birth_place
    nationalities []   nationality (single)
    age_equal_or_over  age_over_NN (flat booleans)

The unversioned "pid" scope follows ARF 1.8, the current version. A request
naming several of these is not meaningful - they are three spellings of one
credential - so the switch lets the most specific win rather than having one
overwrite the other's differently-named claims. All three are added to the
backwards-compat guard for the same reason "ehic" was: otherwise the block
below overwrites what the handler just set.

Age attributes are derived from birthdate rather than stored per user, so they
cannot go stale as the fixture data ages. Without them a verifier asking for
age_over_18 (1.5) or age_equal_or_over.18 (1.8) finds nothing to match, since
DCQL requires every requested claim to be present. The birthday comparison
uses month/day, not day-of-year: in a leap year every date after February
shifts by one day-of-year, which would report the wrong age for a full day
around each such birthday.

Tested: both vocabularies release their own names and none of the other's; the
unversioned scope tracks 1.8; a user with no birthdate yields no age claims at
all rather than empty ones; and the boundary case is checked on the day
before, of, and after a birthday. The fallthrough regression test was
confirmed to fail against the previous behaviour, reproducing the exact
four-claim map above.
@sonarqubecloud

Copy link
Copy Markdown

@leifj
leifj merged commit a2a2aae into main Aug 24, 2026
12 checks passed
@leifj
leifj deleted the fix/pid-scope-claims branch August 24, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant