Integration test: jwt-bearer two-VP token request payload (4078-4)#4229
Open
stevenvegt wants to merge 7 commits into
Open
Integration test: jwt-bearer two-VP token request payload (4078-4)#4229stevenvegt wants to merge 7 commits into
stevenvegt wants to merge 7 commits into
Conversation
Contributor
|
Coverage Impact ⬆️ Merging this pull request will increase total coverage on 🛟 Help
|
This was referenced May 1, 2026
stevenvegt
added a commit
that referenced
this pull request
May 8, 2026
The original koanf tag had underscores (jwt_bearer_client) which conflict with koanf's env-var loader: NUTS_AUTH_EXPERIMENTAL_JWT_BEARER_CLIENT parses as auth.experimental.jwt.bearer.client (5 path components) and doesn't bind. The new name jwtbearerclient (single lowercase word) matches the rest of the codebase's koanf-tag convention (auth.contract validators, auth.accesstokenlifespan, network.enablediscovery, ...) and parses correctly via the env var. The Go field name JwtBearerClient is unchanged. Caught while writing the integration test (#4229) — env-var override of the flag silently failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Boots a real Nuts node via test/node/StartServer (with the experimental flag on, did:web, and a custom policy directory), provisions four subjects (CIBG / Twiin issuers + HCP organization + service provider) over the internal HTTP API, issues the three credentials needed (HealthcareProviderCredential, ServiceProviderCredential, ServiceProviderDelegationCredential), stands up a mock authorization server via httptest that advertises jwt-bearer and captures the form POST, drives the request-service-access-token endpoint, and asserts: - form body matches the RFC 7523 wire shape (grant_type, client_assertion_type, scope; assertion+client_assertion non-empty; no presentation_submission, no client_id) - both captured VPs round-trip through the same node's /internal/vcr/v2/verifier/vp with validity=true - the cross-VP binding survives end-to-end: the delegation credential's issuer equals VP1's signer DID, and its delegatedBy URA equals VP1's HCP URA Negative paths (feature flag off, AS doesn't advertise jwt-bearer, missing service_provider PD, SP wallet has no matching credentials) are covered by unit tests in auth/client/iam/openid4vp_test.go and the handler tests in auth/api/iam/api_test.go; this integration test focuses on the happy-path round trip that those mock-based tests cannot cover (real cryptographic signing, real DID resolution, real verifyVP). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the polling lastForm() helper: the AS /token POST completes synchronously inside request-service-access-token, so the captured form is set by the time the API returns 200. capturedForm now fails the test loudly if /token was never hit, instead of returning an empty url.Values that would make wire-format asserts misleading. Propagate ParseForm errors via an atomic so a malformed token-request form fails the test instead of silently 400-ing the handler. Replace the generic deepString walker with typed firstSubject and firstIdentifierValue helpers that encode the actual credential shape. Drop the idempotent GET-then-POST in provisionSubject; the test runs against a fresh tempdir node, so the lookup branch was dead code. Assisted-by: AI
ebd9d3a to
d422393
Compare
reinkrul
approved these changes
May 12, 2026
reinkrul
reviewed
May 12, 2026
| // Cross-VP binding survived end-to-end: the delegation credential's issuer equals the | ||
| // organization's DID (the same DID that signed VP1), and its delegatedBy URA equals the | ||
| // HCP credential's URA. | ||
| delegationCred := pluckCredentialByType(t, delegationVP, "ServiceProviderDelegationCredential") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Parent PRD
#4078
Summary
End-to-end integration test that boots a real Nuts node, drives the
request-service-access-tokenAPI withservice_provider_subject_id, andasserts the resulting token-request form body matches the PSA 10.10.6 /
RFC 7523 jwt-bearer wire format. Both captured VPs are round-tripped through
the same node's
/internal/vcr/v2/verifier/vpendpoint to confirm signaturesverify and the cross-VP delegation binding holds end-to-end with real keys.
This is the last PR in the #4078 stack. It validates that #4226 (policy
config), #4227 (two-VP flow), and #4228 (API binding) compose correctly under
real cryptographic signing and real DID resolution, coverage the mock-based
unit tests cannot provide.
What changed
auth/api/iam/jwtbearer_integration_test.go, a single happy-path testthat:
NUTS_AUTH_EXPERIMENTAL_JWTBEARERCLIENT=trueand amedication-overviewpolicy with shareddelegating_hcpfield-id bindingacross
organizationandservice_providerPDs.did:websubjects (CIBG issuer, Twiin issuer, HCP org,service provider) and issues the three credentials required by the
profile (HealthcareProviderCredential, ServiceProviderCredential,
ServiceProviderDelegationCredential).
httptest.Servermock AS that advertises jwt-bearer supportand synchronously captures the form POST to
/token.client_assertion_type, scope, presence of both
assertionandclient_assertion, absence ofpresentation_submission).verifyVPendpoint andasserts the cross-VP binding (delegation credential issuer == VP1 signer,
delegation
delegatedByURA == HCP credential URA).How to review
TestIntegration_JwtBearer_TwoVPHappyPathto see the overallflow.
verifyVPround-trips and cross-VP binding checks follow.mockAS,provisionSubject,issueAndLoad, credentialbuilders,
pluckCredentialByType,firstSubject,firstIdentifierValue)are scoped to this file and exist purely to keep the test readable.
mockAS.capturedForm(t)is synchronous: the IAMClient's/tokenPOSTcompletes inside the API call, so the captured form is set by the time
the API returns 200. No polling; failures (parse error or never-called)
surface as test failures, not silent empty maps.
uses
jwt_vc, PE policy usesjwt_vc/jwt_vp, AS metadata usesjwt_vc_json/jwt_vp_json). The header comment onissueAndLoadspellsthis out.
Deviations from spec
The implementation spec calls for negative-path assertions in this
integration test (AS doesn't advertise jwt-bearer, missing
service_providerPD, feature flag disabled). Those scenarios are instead covered by the
mock-based tests added in #4227 and #4228:
service_providerPD, AS doesn't advertisejwt-bearer:
auth/client/iam/openid4vp_test.go.service_provider_subject_idvalidation at the API layer:auth/api/iam/api_test.go.This integration test focuses exclusively on the happy-path round trip,
where the value is real crypto + real DID resolution + real
verifyVP,things unit tests cannot exercise. The file header comment in
jwtbearer_integration_test.godocuments this scope decision.Dependencies
This is PR #4 of 4 in the #4078 stack. It targets
4078-3-api-client-idsothe diff shows only the integration test. Review order:
service_providerPD block)service_provider_subject_id)All three predecessors are approved and out of draft as of opening.
Design context
field.idon$.issuer) is described in the PRD's "Cross-VP field binding" section. Thistest exercises it end-to-end: the captured VP2's
ServiceProviderDelegationCredential.issuermust equal the DID that signedVP1.
parameters (jwt-bearer flow)" section.
Original implementation spec (used during AI-assisted development)
Parent PRD
#4078
Implementation Spec
Integration safety net. Asserts the full client-side flow produces a PSA 10.10.6-conformant token request and that both VPs verify cleanly through the existing VCR
verifyVPAPI.What to build
In-process Go integration test (under
auth/api/iam/, extend or add alongside the existingintegration_test.go). No docker-compose; that's reserved for cross-node workflows.Test setup
organizationPD (e.g.HealthcareProviderCredential).ServiceProviderDelegationCredentialwhoseissueris one of the HCP subject's DIDs.medication-overviewprofile that has bothorganizationandclientPDs, sharing adelegating_hcpfield id on$.issuer.httptest.Servermock AS that:oauth-authorization-servermetadata advertisingurn:ietf:params:oauth:grant-type:jwt-beareringrant_types_supported.Positive path assertions
POST /internal/auth/v2/{HCP-subjectID}/request-service-access-tokenwith body containingclient_id,authorization_server(mock AS URL), and a mixedscope.grant_typeequalsurn:ietf:params:oauth:grant-type:jwt-bearer.client_assertion_typeequalsurn:ietf:params:oauth:client-assertion-type:jwt-bearer.scopematches what was requested.assertionandclient_assertionare both present and non-empty.presentation_submissionform parameter is present.assertionJWT-VP to the same node'sPOST /internal/vcr/v2/verifier/vpand assertvalidity: true. Inspect the verified credentials, VP1 must contain the expected HCP credentials and be signed by the HCP DID.client_assertionJWT-VP the same way. VP2 must contain the delegation credential, be signed by the SP DID, and the delegation'sissuermust equal the HCP DID that signed VP1 (cross-VP binding verified end-to-end).Negative path assertions
jwt-bearerfromgrant_types_supported: API call returns 400 with a clear error.clientPD configured for the requested profile: API call returns 400.client_idpresent: API call returns 400 (feature disabled).Modules touched
auth/api/iam/integration_test.go(or a new sibling test file for the jwt-bearer flow).test/or alongside.Why no docker e2e
PSA 10.10.6 server-side support is out of scope for #4078 (separate PRD). There is no second Nuts node that can receive and validate the request. A mock AS via
httptestis the appropriate level, the test verifies wire format and round-trips both VPs through the same node'sverifyVPfor proof validation.Acceptance Criteria
verifyVP; cross-VP binding holds end-to-end.clientPD, feature flag disabled. (Covered in unit tests in Two-VP token request flow (4078-2) #4227/API binding: accept service_provider_subject_id on request-service-access-token (4078-3) #4228 rather than this integration test, see "Deviations from spec".)