-
Notifications
You must be signed in to change notification settings - Fork 21
#3980: Support of DeziIDTokenCredential #3981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
Coverage Impact ⬇️ Merging this pull request will decrease total coverage on Modified Files with Diff Coverage (6)
🤖 Increase coverage with AI coding...🚦 See full report on Qlty Cloud » 🛟 Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds support for supplying an id_token (Dezi JWT) in the service access token request and converting it into a DeziIDTokenCredential so it can participate in VC/VP-based authorization.
Changes:
- Extend IAM request model/API/docs to accept
id_token, and append a derived Dezi credential to the requested credentials set. - Introduce
DeziIDTokenCredentialtype, creation helper, and a validator hook (currently stubbed). - Add unit/e2e tests and allow
DeziIDJWTas an OpenIDldp_vc/ldp_vpproof type.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| vcr/verifier/verifier_test.go | Adds verifier test and helper to create a Dezi credential from a signed JWT. |
| vcr/verifier/verifier.go | Skips VC signature verification when credential is DeziIDTokenCredential. |
| vcr/pe/presentation_definition_test.go | Adds presentation-exchange matching tests for DeziIDTokenCredential. |
| vcr/credential/validator.go | Adds deziIDTokenCredentialValidator (currently not performing real JWT validation). |
| vcr/credential/util.go | Allows auto-correction for Dezi credentials with “derived proof” (DeziIDJWT). |
| vcr/credential/types.go | Introduces DeziIDTokenCredentialTypeURI. |
| vcr/credential/resolver.go | Registers deziIDTokenCredentialValidator based on VC type. |
| vcr/credential/dezi_test.go | Adds unit test for CreateDeziIDTokenCredential. |
| vcr/credential/dezi.go | Implements CreateDeziIDTokenCredential to derive a VC from an id_token. |
| e2e-tests/oauth-flow/dezi_idtoken/run-test.sh | Adds e2e flow script to request/introspect tokens using id_token. |
| e2e-tests/oauth-flow/dezi_idtoken/generate-jwt.sh | Adds JWT generation helper for the e2e test. |
| e2e-tests/oauth-flow/dezi_idtoken/docker-compose.yml | Adds docker-compose setup for the new e2e test scenario. |
| e2e-tests/oauth-flow/dezi_idtoken/certs/nodeA.pem | Adds test certificate for the e2e environment. |
| e2e-tests/oauth-flow/dezi_idtoken/certs/nodeA.key | Adds test private key for the e2e environment. |
| e2e-tests/oauth-flow/dezi_idtoken/certs/nodeA-chain.pem | Adds test certificate chain for issuing X509Credential in e2e. |
| e2e-tests/oauth-flow/dezi_idtoken/certs/dezi_signing.pem | Adds test signing certificate for Dezi JWT generation. |
| e2e-tests/oauth-flow/dezi_idtoken/certs/dezi_signing.key | Adds test signing private key for Dezi JWT generation. |
| e2e-tests/oauth-flow/dezi_idtoken/certs/README.md | Documents how e2e cert material was generated. |
| e2e-tests/oauth-flow/dezi_idtoken/accesspolicy.json | Adds policy requiring DeziIDTokenCredential and mapping fields into claims. |
| e2e-tests/browser/client/iam/generated.go | Updates generated client model to include id_token. |
| docs/_static/auth/v2.yaml | Documents the new id_token request field in the API spec. |
| auth/oauth/openid.go | Adds DeziIDJWT as supported proof type for ldp_vc/ldp_vp. |
| auth/api/iam/generated.go | Updates generated server model to include id_token. |
| auth/api/iam/api.go | Appends a derived Dezi credential when id_token is provided. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func CreateDeziIDTokenCredential(idTokenSerialized string) (*vc.VerifiableCredential, error) { | ||
| idToken, err := jwt.Parse([]byte(idTokenSerialized), jwt.WithVerify(false), jwt.WithAcceptableSkew(time.Hour*24*365*10)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("parsing id_token: %w", err) | ||
| } |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CreateDeziIDTokenCredential parses the id_token with signature verification explicitly disabled and with an extremely large acceptable clock skew (10 years). This turns untrusted input into a credential that will later be treated as “verified”. Parse and verify the JWT signature (using the x5c/x5t headers as per spec) and enforce reasonable skew / required time claims (nbf/exp) so expired/not-yet-valid tokens are rejected.
| if len(credential.Proof) > 0 { | ||
| return credential | ||
| var proof []proof.LDProof | ||
| _ = credential.UnmarshalProofValue(&proof) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AutoCorrectSelfAttestedCredential ignores the error from UnmarshalProofValue. If proof unmarshalling fails, this silently skips DeziIDJWT detection and can lead to credentials not being auto-corrected when expected. Handle the error explicitly (e.g., treat it as non-Dezi and return early, or return an error to the caller) instead of discarding it.
| _ = credential.UnmarshalProofValue(&proof) | |
| if err := credential.UnmarshalProofValue(&proof); err != nil { | |
| // If the proof cannot be unmarshalled, treat it as non-Dezi and return without auto-correction. | |
| return credential | |
| } |
vcr/verifier/verifier_test.go
Outdated
| err = token.Set("Dezi_id", "900000009") | ||
| require.NoError(t, err) | ||
| err = token.Set("x5c", []string{base64.StdEncoding.EncodeToString(keyPair.Leaf.Raw)}) | ||
| require.NoError(t, err) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Dezi JWT certificate chain is added as an x5c claim in the payload, but the (currently commented) validator code is written to extract x5c from the JWS protected headers. To avoid tests diverging from the intended validation approach, put x5c in the JWT header (or update the validator to use the claim if that’s what the Dezi spec requires).
| "jwt": idTokenSerialized, | ||
| }, | ||
| } | ||
| data, _ := json.Marshal(credentialMap) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
json.Marshal errors are ignored here. While unlikely, marshaling can fail (e.g., if a claim value ends up being a non-marshalable type), and ignoring it can lead to confusing downstream parse errors. Handle and return the marshal error explicitly.
| data, _ := json.Marshal(credentialMap) | |
| data, err := json.Marshal(credentialMap) | |
| if err != nil { | |
| return nil, fmt.Errorf("marshaling credential map: %w", err) | |
| } |
| // DeziIDTokenCredential: signature is verified by Dezi id_token inside the credential. Signature verification is skipped here. | ||
| if credentialToVerify.IsType(credential.DeziIDTokenCredentialTypeURI) { | ||
| checkSignature = false | ||
| } |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disabling VC signature verification for DeziIDTokenCredential makes the credential effectively unverifiable unless the embedded id_token is cryptographically verified elsewhere. The current Dezi validator does not verify the JWT signature, so this becomes an integrity/authenticity bypass. Instead of skipping signature verification here, verify the embedded id_token signature (and its trust chain) as part of credential validation, or keep VC signature verification enabled when no embedded-token verification is performed.
| // DeziIDTokenCredential: signature is verified by Dezi id_token inside the credential. Signature verification is skipped here. | |
| if credentialToVerify.IsType(credential.DeziIDTokenCredentialTypeURI) { | |
| checkSignature = false | |
| } |
| func (d deziIDTokenCredentialValidator) Validate(credential vc.VerifiableCredential) error { | ||
| type proofType struct { | ||
| Type string `json:"type"` | ||
| JWT string `json:"jwt"` | ||
| } | ||
| proofs := []proofType{} | ||
| if err := credential.UnmarshalProofValue(&proofs); err != nil { | ||
| return fmt.Errorf("%w: invalid proof format: %w", errValidation, err) | ||
| } | ||
| if len(proofs) != 1 { | ||
| return fmt.Errorf("%w: expected exactly one proof, got %d", errValidation, len(proofs)) | ||
| } | ||
| proof := proofs[0] | ||
| if proof.Type != "DeziIDJWT" { | ||
| return fmt.Errorf("%w: invalid proof type: expected 'DeziIDToken', got '%s'", errValidation, proof.Type) | ||
| } | ||
| if err := d.validateDeziToken(credential, proof.JWT); err != nil { | ||
| return fmt.Errorf("%w: invalid Dezi id_token: %w", errValidation, err) | ||
| } | ||
| return (defaultCredentialValidator{}).Validate(credential) | ||
| } |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deziIDTokenCredentialValidator currently accepts any embedded JWT: validateDeziToken always returns nil and all signature/claim checks are commented out. This means an attacker can forge a DeziIDTokenCredential with arbitrary subject/roles and it will pass validation. Implement actual JWT parsing with signature verification (e.g., using x5c from the JWS header) and validate required claims (exp/nbf/iss/aud) before accepting the credential.
vcr/verifier/verifier_test.go
Outdated
| keyPair, err := tls.LoadX509KeyPair("../../test/pki/certificate-and-key.pem", "../../test/pki/certificate-and-key.pem") | ||
| require.NoError(t, err) | ||
|
|
||
| key, err := jwk.FromRaw(keyPair.PrivateKey) | ||
| require.NoError(t, err) | ||
|
|
||
| // Set the key ID and x5t (X.509 thumbprint) | ||
| x5t := sha1.Sum(keyPair.Leaf.Raw) | ||
| err = key.Set(jwk.KeyIDKey, base64.StdEncoding.EncodeToString(x5t[:])) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tls.LoadX509KeyPair does not reliably populate keyPair.Leaf (the codebase sets it explicitly elsewhere, e.g. core/server_config.go:113). Accessing keyPair.Leaf.Raw here can panic with a nil pointer. Parse the leaf certificate from keyPair.Certificate[0] and assign it to keyPair.Leaf before using it.
vcr/verifier/verifier_test.go
Outdated
| println(string(signed)) | ||
|
|
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test helper prints the signed JWT to stdout. That adds noise to go test output and can leak potentially sensitive test material in CI logs. Please remove the println or gate it behind an explicit debug flag.
| println(string(signed)) |
| privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
| token := jwt.New() | ||
| _ = token.Set(jwt.NotBeforeKey, 1701933627) | ||
| _ = token.Set(jwt.ExpirationKey, 1701933697) | ||
| _ = token.Set("initials", "B.B.") | ||
| _ = token.Set("surname", "Jansen") | ||
| _ = token.Set("surname_prefix", "van der") | ||
| _ = token.Set("Dezi_id", "900000009") | ||
| _ = token.Set("relations", []map[string]interface{}{ | ||
| { | ||
| "entity_name": "Zorgaanbieder", | ||
| "roles": []string{"01.041", "30.000"}, | ||
| "ura": "87654321", | ||
| }, | ||
| }) | ||
| signedToken, _ := jwt.Sign(token, jwt.WithKey(jwa.ES256, privateKey)) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test ignores errors from key generation, claim setting, signing, and JSON marshaling (using _ = / _ , _ :=). If any of these steps fails, the test may pass/fail for the wrong reason. Use require.NoError for each operation so failures are surfaced clearly.
| privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | |
| token := jwt.New() | |
| _ = token.Set(jwt.NotBeforeKey, 1701933627) | |
| _ = token.Set(jwt.ExpirationKey, 1701933697) | |
| _ = token.Set("initials", "B.B.") | |
| _ = token.Set("surname", "Jansen") | |
| _ = token.Set("surname_prefix", "van der") | |
| _ = token.Set("Dezi_id", "900000009") | |
| _ = token.Set("relations", []map[string]interface{}{ | |
| { | |
| "entity_name": "Zorgaanbieder", | |
| "roles": []string{"01.041", "30.000"}, | |
| "ura": "87654321", | |
| }, | |
| }) | |
| signedToken, _ := jwt.Sign(token, jwt.WithKey(jwa.ES256, privateKey)) | |
| privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | |
| require.NoError(t, err) | |
| token := jwt.New() | |
| err = token.Set(jwt.NotBeforeKey, 1701933627) | |
| require.NoError(t, err) | |
| err = token.Set(jwt.ExpirationKey, 1701933697) | |
| require.NoError(t, err) | |
| err = token.Set("initials", "B.B.") | |
| require.NoError(t, err) | |
| err = token.Set("surname", "Jansen") | |
| require.NoError(t, err) | |
| err = token.Set("surname_prefix", "van der") | |
| require.NoError(t, err) | |
| err = token.Set("Dezi_id", "900000009") | |
| require.NoError(t, err) | |
| err = token.Set("relations", []map[string]interface{}{ | |
| { | |
| "entity_name": "Zorgaanbieder", | |
| "roles": []string{"01.041", "30.000"}, | |
| "ura": "87654321", | |
| }, | |
| }) | |
| require.NoError(t, err) | |
| signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.ES256, privateKey)) | |
| require.NoError(t, err) |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
❌ 25 blocking issues (31 total)
|

I have chosen to simply add an
id_tokenfield in the token request, and the Nuts node (OAuth client side) creates a DeziIDTokenCredential from it. This is then added as extra credential, so the same could be achieved by the EHR by making the credential itself and passing it in the existingcredentialsfield.TODO:
Closes #3980