Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d8cef07
Read Presentation Definition from local PDP backend
reinkrul Nov 28, 2025
eeb783a
build Docker image for branch
reinkrul Dec 1, 2025
811fc85
build docker image
reinkrul Dec 1, 2025
262a774
basic testing setup
reinkrul Dec 2, 2025
921821f
Issue mandaatcredential
reinkrul Dec 4, 2025
19f5960
#3953: add support for urn:ietf:params:oauth:grant-type:jwt-bearer fo…
reinkrul Dec 4, 2025
18d8023
Relax did:x509 certificate key usage validation
reinkrul Dec 16, 2025
dccb5ed
Enable RS256 support
reinkrul Dec 16, 2025
206e5e1
Add AortaGtK CA certs to OS trust bundle
reinkrul Dec 16, 2025
219635f
Add EV intermediate CA to trusted certs
reinkrul Dec 16, 2025
12f6e9e
Don't send presentation_submission
reinkrul Dec 16, 2025
14358d9
Introduce policy_id parameter
reinkrul Dec 16, 2025
708ad5a
Try to marshal VPs as JWT, not JSON-LD
reinkrul Dec 16, 2025
89527d3
Updated README
reinkrul Dec 16, 2025
f07d0f6
Updated README
reinkrul Dec 16, 2025
6254059
test for VP type
reinkrul Dec 16, 2025
68a5e21
write vps to temp file
reinkrul Dec 17, 2025
2572c09
revert VC JWT fix
reinkrul Dec 17, 2025
64cc71f
set fixed key ID
reinkrul Dec 17, 2025
724051f
fix vp.type to array
reinkrul Dec 17, 2025
50625ab
Made token response parsing lenient
reinkrul Dec 17, 2025
9bd652e
Reverted jwt ID
reinkrul Dec 17, 2025
1465cee
Merge branch 'master' into lspxnuts
reinkrul Jan 27, 2026
dac8036
#3980: Support validation of DeziIDTokenCredential
reinkrul Feb 2, 2026
2ebea32
implemented e2e test
reinkrul Feb 2, 2026
6552dfa
Update vcr/credential/validator.go
reinkrul Feb 2, 2026
ea7ffac
cleanup
reinkrul Feb 2, 2026
ea905dc
Merge branch 'lspxnuts' into project-gf
reinkrul Feb 2, 2026
ed58bc1
Merge branch 'iss3980-validate-idtoken-credential' into project-gf
reinkrul Feb 2, 2026
ca4005d
Push docker image
reinkrul Feb 3, 2026
61bc2f7
Merge branch 'master' into project-gf
reinkrul Feb 5, 2026
b53043e
VCR: Allow configuration of revocation list max-age
reinkrul Feb 6, 2026
4ce4d99
Merge branch 'vcr-configure-revocation-maxage' into project-gf
reinkrul Feb 6, 2026
847841c
fix
reinkrul Feb 10, 2026
bcdb76b
Merge branch 'master' into project-gf
reinkrul Feb 10, 2026
a71c194
demo-ing revocation: include revoked/expired VCs in wallet.List(), al…
reinkrul Feb 10, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/build-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ on:
push:
branches:
- master
- project-gf
tags:
- 'v*'
pull_request:
branches:
- master
- project-gf

# cancel build action if superseded by new commit on same branch
concurrency:
Expand Down Expand Up @@ -51,7 +53,7 @@ jobs:
images: nutsfoundation/nuts-node
tags: |
# generate 'master' tag for the master branch
type=ref,event=branch,enable={{is_default_branch}},prefix=
type=ref,event=branch,enable=true,prefix=
# generate 5.2.1 tag
type=semver,pattern={{version}}
flavor: |
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
COPY go.sum .
RUN go mod download && go mod verify

COPY . .

Check warning on line 20 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker

Attempting to Copy file that is excluded by .dockerignore

CopyIgnoredFile: Attempting to Copy file "." that is excluded by .dockerignore More info: https://docs.docker.com/go/dockerfile/rule/copy-ignored-file/

Check warning on line 20 in Dockerfile

View workflow job for this annotation

GitHub Actions / e2e-test

Attempting to Copy file that is excluded by .dockerignore

CopyIgnoredFile: Attempting to Copy file "." that is excluded by .dockerignore More info: https://docs.docker.com/go/dockerfile/rule/copy-ignored-file/

Check warning on line 20 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker

Attempting to Copy file that is excluded by .dockerignore

CopyIgnoredFile: Attempting to Copy file "." that is excluded by .dockerignore More info: https://docs.docker.com/go/dockerfile/rule/copy-ignored-file/
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s -X 'github.com/nuts-foundation/nuts-node/core.GitCommit=${GIT_COMMIT}' -X 'github.com/nuts-foundation/nuts-node/core.GitBranch=${GIT_BRANCH}' -X 'github.com/nuts-foundation/nuts-node/core.GitVersion=${GIT_VERSION}'" -o /opt/nuts/nuts

# alpine
Expand All @@ -25,7 +25,10 @@
RUN apk update \
&& apk add --no-cache \
tzdata \
curl
curl \
ca-certificates
COPY pki/cacerts/* /usr/local/share/ca-certificates/
RUN update-ca-certificates
COPY --from=builder /opt/nuts/nuts /usr/bin/nuts

HEALTHCHECK --start-period=30s --timeout=5s --interval=10s \
Expand Down
16 changes: 16 additions & 0 deletions LSPxNuts_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# LSPxNuts Proof of Concept

This is a branch that for the Proof of Concept of the LSPxNuts project.

It adds or alters the following functionality versus the mainstream Nuts node:

- OAuth2 `vp_bearer` token exchange: read presentation definition from local definitions instead of fetching it from the remote authorization server.
LSP doesn't support presentation definitions, meaning that we need to look it up locally.
- Add support for JWT bearer grant type. If the server supports this, it uses this grant type instead of the Nuts-specific vp_token-bearer grant type.
- Add CA certificates of Sectigo (root CA, OV and EV intermediate CA) to Docker image's OS CA bundle, because they're used by AORTA-LSP.
- Fix marshalling of Verifiable Presentations in JWT format; `type` was marshalled as JSON-LD (single-entry-array was replaced by string)
- Add `policy_id` field to access token request to specify the Presentation Definition that should be used.
The `scope` can then be specified as whatever the use case requires (e.g. SMART on FHIR-esque scopes).
- Relax `did:x509` key usage check: the certificate from UZI smart cards that is used to sign credentials, doesn't have `serverAuth` key usage, only `digitalSignature`.
This broke, since we didn't specify the key usage, but `x509.Verify()` expects key usage `serverAuth` to be present by default.
- Add support for `RS256` (RSA 2048) signatures, since that's what UZI smart cards produce.
14 changes: 10 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ See the `documentation <https://nuts-node.readthedocs.io/en/stable/>`_ for how t
:target: https://nuts-node.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status

.. image:: https://api.codeclimate.com/v1/badges/69f77bd34f3ac253cae0/test_coverage
:target: https://codeclimate.com/github/nuts-foundation/nuts-node/test_coverage
.. image:: https://qlty.sh/gh/nuts-foundation/projects/nuts-node/coverage.svg
:target: https://qlty.sh/gh/nuts-foundation/projects/nuts-node
:alt: Code coverage

.. image:: https://api.codeclimate.com/v1/badges/69f77bd34f3ac253cae0/maintainability
:target: https://codeclimate.com/github/nuts-foundation/nuts-node/maintainability
.. image:: https://qlty.sh/gh/nuts-foundation/projects/nuts-node/maintainability.svg
:target: https://qlty.sh/gh/nuts-foundation/projects/nuts-node
:alt: Maintainability

.. image:: https://github.com/nuts-foundation/nuts-node/actions/workflows/build-images.yaml/badge.svg
Expand Down Expand Up @@ -217,6 +217,12 @@ The following options can be configured on the server:
storage.session.redis.sentinel.username Username for authenticating to Redis Sentinels.
storage.session.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis session servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address).
storage.sql.connection Connection string for the SQL database. If not set it, defaults to a SQLite database stored inside the configured data directory. Note: using SQLite is not recommended in production environments. If using SQLite anyways, remember to enable foreign keys ('_foreign_keys=on') and the write-ahead-log ('_journal_mode=WAL').
**Tracing**
tracing.endpoint OTLP collector endpoint for OpenTelemetry tracing (e.g., 'localhost:4318'). When empty, tracing is disabled.
tracing.insecure false Disable TLS for the OTLP connection.
tracing.servicename Service name reported to the tracing backend. Defaults to 'nuts-node'.
**VCR**
vcr.verifier.revocation.maxage 15m0s Max age of revocation information. If the revocation information is older than this, it will be refreshed from the issuer. If set to 0 or negative, revocation information will always be refreshed.
**policy**
policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping.
======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ============================================================================================================================================================================================================================================================================================================================================
Expand Down
21 changes: 18 additions & 3 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/core/to"
"html/template"
"net/http"
"net/url"
"slices"
"strings"
"time"

"github.com/nuts-foundation/nuts-node/core/to"
"github.com/nuts-foundation/nuts-node/vcr/credential"

"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
Expand Down Expand Up @@ -750,9 +752,18 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS
if request.Body.Credentials != nil {
credentials = *request.Body.Credentials
}

if request.Body.IdToken != nil {
idTokenCredential, err := credential.CreateDeziIDTokenCredential(*request.Body.IdToken)
if err != nil {
return nil, core.InvalidInputError("failed to create id_token credential: %w", err)
}
credentials = append(credentials, *idTokenCredential)
}

// assert that self-asserted credentials do not contain an issuer or credentialSubject.id. These values must be set
// by the nuts-node to build the correct wallet for a DID. See https://github.com/nuts-foundation/nuts-node/issues/3696
// As a sideeffect it is no longer possible to pass signed credentials to this API.
// As a side effect it is no longer possible to pass signed credentials to this API.
for _, cred := range credentials {
var credentialSubject []map[string]interface{}
if err := cred.UnmarshalCredentialSubject(&credentialSubject); err != nil {
Expand All @@ -774,7 +785,11 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS
useDPoP = false
}
clientID := r.subjectToBaseURL(request.SubjectID)
tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, useDPoP, credentials)
var policyId string
if request.Body.PolicyId != nil {
policyId = *request.Body.PolicyId
}
tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, policyId, useDPoP, credentials)
if err != nil {
// this can be an internal server error, a 400 oauth error or a 412 precondition failed if the wallet does not contain the required credentials
return nil, err
Expand Down
18 changes: 9 additions & 9 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
request.Params.CacheControl = to.Ptr("no-cache")
// Initial call to populate cache
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(response, nil).Times(2)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(response, nil).Times(2)
token, err := ctx.client.RequestServiceAccessToken(nil, request)

// Test call to check cache is bypassed
Expand All @@ -894,7 +894,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
TokenType: "Bearer",
ExpiresIn: to.Ptr(900),
}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(response, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(response, nil)

token, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand Down Expand Up @@ -933,7 +933,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
t.Run("cache expired", func(t *testing.T) {
cacheKey := accessTokenRequestCacheKey(request)
_ = ctx.client.accessTokenCache().Delete(cacheKey)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{AccessToken: "other"}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(&oauth.TokenResponse{AccessToken: "other"}, nil)

otherToken, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand All @@ -950,7 +950,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
Scope: "first second",
TokenType: &tokenTypeBearer,
}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", false, nil).Return(&oauth.TokenResponse{}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", false, nil).Return(&oauth.TokenResponse{}, nil)

_, err := ctx.client.RequestServiceAccessToken(nil, RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body})

Expand All @@ -959,7 +959,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
t.Run("ok with expired cache by ttl", func(t *testing.T) {
ctx := newTestClient(t)
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{ExpiresIn: to.Ptr(5)}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(&oauth.TokenResponse{ExpiresIn: to.Ptr(5)}, nil)

_, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand All @@ -968,7 +968,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
})
t.Run("error - no matching credentials", func(t *testing.T) {
ctx := newTestClient(t)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(nil, pe.ErrNoCredentials)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(nil, pe.ErrNoCredentials)

_, err := ctx.client.RequestServiceAccessToken(nil, RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body})

Expand All @@ -984,8 +984,8 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
ctx.client.storageEngine = mockStorage

request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{AccessToken: "first"}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{AccessToken: "second"}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(&oauth.TokenResponse{AccessToken: "first"}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, nil).Return(&oauth.TokenResponse{AccessToken: "second"}, nil)

token1, err := ctx.client.RequestServiceAccessToken(nil, request)
require.NoError(t, err)
Expand All @@ -1010,7 +1010,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
{ID: to.Ptr(ssi.MustParseURI("not empty"))},
}
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, *body.Credentials).Return(response, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", "", true, *body.Credentials).Return(response, nil)

_, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand Down
11 changes: 11 additions & 0 deletions auth/api/iam/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading