Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 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
3e58304
\#3978: Return credential/presentation verification errors to client
Copilot Jan 29, 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
3133cb2
Merge branch 'master' into iss3980-validate-idtoken-credential
reinkrul Feb 10, 2026
a71c194
demo-ing revocation: include revoked/expired VCs in wallet.List(), al…
reinkrul Feb 10, 2026
d739fa4
Merge branch 'master' into project-gf
reinkrul Feb 16, 2026
a87f97b
Merge remote-tracking branch 'origin/copilot/improve-client-error-mes…
reinkrul Feb 17, 2026
910dfaa
Merge branch 'master' into project-gf
reinkrul Feb 17, 2026
5fd918b
Disable client-side access token caching
reinkrul Feb 25, 2026
649b839
wipo
reinkrul Feb 26, 2026
a622b57
Update to 2026 version (wip)
reinkrul Feb 27, 2026
1f18ef3
Support both 2024 and v0.7 version
reinkrul Mar 2, 2026
2e67f69
wip
reinkrul Mar 3, 2026
e21acf4
Working Dezi 0.7 implementation
reinkrul Mar 11, 2026
92da21d
Fix
reinkrul Mar 11, 2026
bf8ac19
Merge branch 'master' into iss3980-validate-idtoken-credential
reinkrul Mar 11, 2026
523c6b2
Fix
reinkrul Mar 11, 2026
147219a
Merge iss3957-vp-jwt-type-marshalling into project-gf
reinkrul Mar 20, 2026
daeb4b4
Merge branch 'master' into iss3980-validate-idtoken-credential
reinkrul Mar 22, 2026
c6b5e47
Update to Dezi v0.7
reinkrul Mar 23, 2026
49e28c4
Merge branch 'iss3980-validate-idtoken-credential' into project-gf
reinkrul Mar 23, 2026
1114fcc
Fix passing Dezi attestation as extra credential
reinkrul Mar 23, 2026
8383d51
Merge branch 'iss3980-validate-idtoken-credential' into project-gf
reinkrul Mar 23, 2026
3dc7c61
Fix test
reinkrul Mar 23, 2026
7c74d55
fix
reinkrul Mar 23, 2026
9ceed0d
Merge branch 'iss3980-validate-idtoken-credential' into project-gf
reinkrul Mar 23, 2026
ef4b3df
Add vcr.dezi.allowedjku
reinkrul Mar 23, 2026
b2e73c2
Merge branch 'iss3980-validate-idtoken-credential' into project-gf
reinkrul Mar 23, 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/
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.
7 changes: 5 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,17 @@ 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').
storage.sql.rdsiam.dbuser Database username for IAM authentication. If not specified, the username from the connection string will be used. The database user must be created with IAM authentication enabled.
storage.sql.rdsiam.enabled false Enable AWS RDS IAM authentication for the SQL database connection. When enabled, the node will use temporary IAM tokens instead of passwords. Requires the connection string to be a PostgreSQL or MySQL RDS endpoint without a password.
storage.sql.rdsiam.region AWS region where the RDS instance is located (e.g., 'us-east-1). Required when RDS IAM authentication is enabled.
storage.sql.rdsiam.dbuser Database username for IAM authentication. If not specified, the username from the connection string will be used. The database user must be created with IAM authentication enabled.
storage.sql.rdsiam.tokenrefreshinterval 14m0s Interval at which to refresh the IAM authentication token. RDS tokens are valid for 15 minutes, so the default is 14 minutes to ensure tokens are refreshed before expiry. Specified as Golang duration (e.g. 10m, 1h).
storage.sql.rdsiam.tokenrefreshinterval 14m0s Interval at which to refresh the IAM authentication token. RDS tokens are valid for 15 minutes, so set this to ensure tokens are refreshed before expiry. Specified as Golang duration (e.g. 10m, 1h).
**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.dezi.allowedjku [] List of allowed JKU URLs for fetching Dezi attestation keys. If not set, defaults to production (https://auth.dezi.nl/dezi/jwks.json), and in non-strict mode also acceptance (https://acceptatie.auth.dezi.nl/dezi/jwks.json).
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
76 changes: 50 additions & 26 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 @@ -729,31 +731,48 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS
return nil, err
}

tokenCache := r.accessTokenCache()
cacheKey := accessTokenRequestCacheKey(request)
if request.Params.CacheControl == nil || *request.Params.CacheControl != "no-cache" {
// try to retrieve token from cache
tokenCacheResult := new(TokenResponse)
err = tokenCache.Get(cacheKey, tokenCacheResult)
if err == nil {
// adjust tokenCacheResult.ExpiresIn to the remaining time
expiresAt := time.Unix(int64(*tokenCacheResult.ExpiresAt), 0)
tokenCacheResult.ExpiresIn = to.Ptr(int(time.Until(expiresAt).Seconds()))
return RequestServiceAccessToken200JSONResponse(*tokenCacheResult), nil
} else if !errors.Is(err, storage.ErrNotFound) {
// only log error, don't fail
log.Logger().WithError(err).Warnf("Failed to retrieve access token from cache: %s", err.Error())
}
}
// PROJECT-GF: Disabled for testing credential revocation
//tokenCache := r.accessTokenCache()
//cacheKey := accessTokenRequestCacheKey(request)
//if request.Params.CacheControl == nil || *request.Params.CacheControl != "no-cache" {
// // try to retrieve token from cache
// tokenCacheResult := new(TokenResponse)
// err = tokenCache.Get(cacheKey, tokenCacheResult)
// if err == nil {
// // adjust tokenCacheResult.ExpiresIn to the remaining time
// expiresAt := time.Unix(int64(*tokenCacheResult.ExpiresAt), 0)
// tokenCacheResult.ExpiresIn = to.Ptr(int(time.Until(expiresAt).Seconds()))
// return RequestServiceAccessToken200JSONResponse(*tokenCacheResult), nil
// } else if !errors.Is(err, storage.ErrNotFound) {
// // only log error, don't fail
// log.Logger().WithError(err).Warnf("Failed to retrieve access token from cache: %s", err.Error())
// }
//}

var credentials []VerifiableCredential
if request.Body.Credentials != nil {
credentials = *request.Body.Credentials
}

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

// 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.
for _, cred := range credentials {
// As a side effect it is no longer possible to pass signed credentials to this API.
for i, cred := range credentials {
// But not for id_token credentials, these are externally signed, meaning they have an issuer
if i == idTokenCredentialIdx {
continue
}

var credentialSubject []map[string]interface{}
if err := cred.UnmarshalCredentialSubject(&credentialSubject); err != nil {
// extremely unlikely
Expand All @@ -774,7 +793,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 All @@ -785,12 +808,13 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS
}
tokenResult.ExpiresAt = to.Ptr(int(time.Now().Add(ttl).Unix()))
// we reduce the ttl by accessTokenCacheOffset to make sure the token is expired when the cache expires
ttl -= accessTokenCacheOffset
err = tokenCache.Put(cacheKey, tokenResult, storage.WithTTL(ttl))
if err != nil {
// only log error, don't fail
log.Logger().WithError(err).Warnf("Failed to cache access token: %s", err.Error())
}
// PROJECT-GF: Disabled for testing credential revocation
//ttl -= accessTokenCacheOffset
//err = tokenCache.Put(cacheKey, tokenResult, storage.WithTTL(ttl))
//if err != nil {
// // only log error, don't fail
// log.Logger().WithError(err).Warnf("Failed to cache access token: %s", err.Error())
//}
return RequestServiceAccessToken200JSONResponse(*tokenResult), nil
}

Expand Down
Loading
Loading