Skip to content

[#4253 6/8] feat(auth): validate credential_selection keys (Policy 1, 400 invalid_request)#4288

Draft
stevenvegt wants to merge 1 commit into
4253-2-validatorsfrom
4253-6-api-key-validation
Draft

[#4253 6/8] feat(auth): validate credential_selection keys (Policy 1, 400 invalid_request)#4288
stevenvegt wants to merge 1 commit into
4253-2-validatorsfrom
4253-6-api-key-validation

Conversation

@stevenvegt
Copy link
Copy Markdown
Member

Parent PRD

#4253

Item 6 of 8. Depends on item #2 (#4281, ValidateSelectionKeys) — branched off 4253-2-validators. Independent of the #3/#4/#5 chain.

Summary

Validate credential_selection keys against the union of the PDs the request will evaluate, returning 400 OAuth invalid_request listing every unknown key (Policy 1). Moves typo protection to the request entry, replacing the per-input-descriptor strictness that the deleted NewFieldSelector used to provide.

Placement

credential_selection is accepted by exactly one endpoint: RequestServiceAccessToken (body type at auth/api/iam/generated.go:162). The HTTP handler (auth/api/iam/api.go:727) does not resolve PDs — it delegates to the client (auth/client/iam/openid4vp.go:272), which resolves them just before building. So validation lives in the client, right after PD resolution and before any submission build (the PRD's "API handler" is imprecise; this is where the in-scope PDs are actually known):

  • Single-VP requestVPTokenAccessToken — after pdResolver.Resolve (line 307): ValidateSelectionKeys(credentialSelection, resolved.PresentationDefinition).
  • Two-VP requestJwtBearerAccessToken — after loadAndValidateProfile resolves orgPD + spPD (lines 355-356): ValidateSelectionKeys(credentialSelection, orgPD, spPD).

A small shared helper performs the call and maps the result, so both sites stay one line.

Error mapping

On *pe.UnknownSelectionKeysError, return oauth.OAuth2Error{Code: oauth.InvalidRequest, Description: err.Error()} (the typed error's Error() already reads unknown credential_selection keys: a, b, sorted). This matches how the client already emits OAuth errors (e.g. UnsupportedGrantType at lines 275/286/358); the error propagates through api.go:797-799 and renders as a 400.

Notes / non-issues

  • Cache does not bypass it. accessTokenRequestCacheKey (api.go:1034) hashes the whole request including credential_selection, so a typo'd request gets a distinct key, misses the cache, and reaches the client. 400s aren't cached (only successful tokens, line 808).
  • OpenID4VP holder path untouched. handleAuthorizeRequestFromVerifier passes nil credentialSelection (openid4vp.go:321) — nothing to validate.
  • Single-VP remote PD. When pdResolver fetches a remote PD, keys are validated against that resolved PD — correct: a key with no matching field id there is meaningless.
  • Validation runs against the union, so a two-VP key targeting only the org or only the SP PD passes (Policy 1 / PRD user story 5).

Testing

auth/client/iam tests:

  • happy path: valid keys pass through to the build (single-VP and two-VP);
  • error path: an unknown key yields oauth.OAuth2Error with InvalidRequest and a description naming all unknown keys; assert it surfaces as a 400 at the handler;
  • two-VP: a key present only on the org PD (or only the SP PD) passes;
  • a typo'd request is not served from cache (distinct cache key).

Acceptance Criteria

  • Both client request methods call ValidateSelectionKeys against their resolved in-scope PD(s) before building.
  • Unknown keys produce 400 OAuth invalid_request naming all offending keys.
  • Two-VP validation is against the union (org ∪ SP), not each PD independently.
  • OpenID4VP holder path and the cache behavior unchanged.
  • go build ./... and go test ./auth/... pass.

@qltysh
Copy link
Copy Markdown
Contributor

qltysh Bot commented May 27, 2026

Qlty


Coverage Impact

This PR will not change total coverage.

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

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