Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,16 @@ 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).
**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
24 changes: 21 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,10 +752,26 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS
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 Down
28 changes: 28 additions & 0 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,34 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
require.NoError(t, err)
assert.False(t, ctx.client.accessTokenCache().Exists(accessTokenRequestCacheKey(request)))
})
t.Run("with Dezi id_token", func(t *testing.T) {
ctx := newTestClient(t)
idToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFlNDY4MjlkLWM4ZTgtNDhhMC1iZDZhLTIxYjhhMDdiOGNiMiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHBzOi8vYWNjZXB0YXRpZS5hdXRoLmRlemkubmwvZGV6aS9qd2tzLmpzb24ifQ.eyJqc29uX3NjaGVtYSI6Imh0dHBzOi8vd3d3LmRlemkubmwvanNvbl9zY2hlbWFzL3YxL3ZlcmtsYXJpbmcuanNvbiIsImxvYV9kZXppIjoiaHR0cDovL2VpZGFzLmV1cm9wYS5ldS9Mb0EvaGlnaCIsImp0aSI6ImY0MTBiMjU1LTZiMDctNDE4Mi1hYzVjLWM0MWYwMmJkMzk5NSIsInZlcmtsYXJpbmdfaWQiOiIwZTk3MGZjYi01MzBjLTQ4MmUtYmEyOC00N2I0NjFkNGRjYjUiLCJkZXppX251bW1lciI6IjkwMDAyMjE1OSIsInZvb3JsZXR0ZXJzIjoiSi4iLCJ2b29ydm9lZ3NlbCI6bnVsbCwiYWNodGVybmFhbSI6IjkwMDE3MzYyIiwiYWJvbm5lZV9udW1tZXIiOiI5MDAwMDM4MCIsImFib25uZWVfbmFhbSI6IlTDqXN0IFpvcmdpbnN0ZWxsaW5nIDAxIiwicm9sX2NvZGUiOiI5Mi4wMDAiLCJyb2xfbmFhbSI6Ik1vbmRoeWdpw6tuaXN0Iiwicm9sX2NvZGVfYnJvbiI6Imh0dHA6Ly93d3cuZGV6aS5ubC9yb2xfYnJvbi9iaWciLCJzdGF0dXNfdXJpIjoiaHR0cHM6Ly9hY2NlcHRhdGllLmF1dGguZGV6aS5ubC9zdGF0dXMvdjEvdmVya2xhcmluZy8wZTk3MGZjYi01MzBjLTQ4MmUtYmEyOC00N2I0NjFkNGRjYjUiLCJuYmYiOjE3NzI2NjUyMDAsImV4cCI6MTc4MDYxMDQwMCwiaXNzIjoiaHR0cHM6Ly9hYm9ubmVlLmRlemkubmwifQ.ipR4stqmO8MOmmapukeQxIOVpwO_Ipjgy5BHjUsdCvuFObhVrj48AQCndtV48D_Ol1hXO4s9p4b-1epjEiobjEmEO0JQNU0BAOGG0eWl8MujfhzlDnmwo5AEtvdgTjlnBaLReVu1BJ8KYgc1DT7JhCukq9z5wZLqU1aqtETleX2-s-dNdTdwrUjJa1DvIgO-DQ_rCp-1tcfkr2rtyW16ztyI88Q2YdBkNGcG0if5aYZHpcQ4-121WBObUa0FhswS7EHni5Ru8KwZNq0HC8OLWw3YqLrYHTFe2K0GQjMtEO6zNxApbMXWKlgeWdf7Ry2rPpe2l9Z5NuMrFiB8JChZsQ"
request := RequestServiceAccessTokenRequestObject{
SubjectID: holderSubjectID,
Body: &RequestServiceAccessTokenJSONRequestBody{
AuthorizationServer: verifierURL.String(),
Scope: "first second",
IdToken: to.Ptr(idToken),
},
}

// Expect that the id_token is converted to a Dezi credential and passed to RequestRFC021AccessToken
ctx.iamClient.EXPECT().RequestRFC021AccessToken(
nil,
holderClientID,
holderSubjectID,
verifierURL.String(),
"first second",
true,
gomock.Any(), // The id_token is converted to a DeziUserCredential
).Return(&oauth.TokenResponse{ExpiresIn: to.Ptr(5)}, nil)

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

require.NoError(t, err)
assert.False(t, ctx.client.accessTokenCache().Exists(accessTokenRequestCacheKey(request)))
})
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)
Expand Down
4 changes: 4 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.

41 changes: 37 additions & 4 deletions auth/client/iam/openid4vp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/http/client"
test2 "github.com/nuts-foundation/nuts-node/test"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vdr/didsubject"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"

"github.com/nuts-foundation/nuts-node/http/client"
test2 "github.com/nuts-foundation/nuts-node/test"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vdr/didsubject"

ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
Expand Down Expand Up @@ -319,6 +320,38 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) {
assert.Equal(t, "token", response.AccessToken)
assert.Equal(t, "bearer", response.TokenType)
})
t.Run("with Dezi credential", func(t *testing.T) {
ctx := createClientServerTestContext(t)
ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{primaryWalletDID, secondaryWalletDID}, nil)

// Create a Dezi credential from an id_token
idToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFlNDY4MjlkLWM4ZTgtNDhhMC1iZDZhLTIxYjhhMDdiOGNiMiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHBzOi8vYWNjZXB0YXRpZS5hdXRoLmRlemkubmwvZGV6aS9qd2tzLmpzb24ifQ.eyJqc29uX3NjaGVtYSI6Imh0dHBzOi8vd3d3LmRlemkubmwvanNvbl9zY2hlbWFzL3YxL3ZlcmtsYXJpbmcuanNvbiIsImxvYV9kZXppIjoiaHR0cDovL2VpZGFzLmV1cm9wYS5ldS9Mb0EvaGlnaCIsImp0aSI6ImY0MTBiMjU1LTZiMDctNDE4Mi1hYzVjLWM0MWYwMmJkMzk5NSIsInZlcmtsYXJpbmdfaWQiOiIwZTk3MGZjYi01MzBjLTQ4MmUtYmEyOC00N2I0NjFkNGRjYjUiLCJkZXppX251bW1lciI6IjkwMDAyMjE1OSIsInZvb3JsZXR0ZXJzIjoiSi4iLCJ2b29ydm9lZ3NlbCI6bnVsbCwiYWNodGVybmFhbSI6IjkwMDE3MzYyIiwiYWJvbm5lZV9udW1tZXIiOiI5MDAwMDM4MCIsImFib25uZWVfbmFhbSI6IlTDqXN0IFpvcmdpbnN0ZWxsaW5nIDAxIiwicm9sX2NvZGUiOiI5Mi4wMDAiLCJyb2xfbmFhbSI6Ik1vbmRoeWdpw6tuaXN0Iiwicm9sX2NvZGVfYnJvbiI6Imh0dHA6Ly93d3cuZGV6aS5ubC9yb2xfYnJvbi9iaWciLCJzdGF0dXNfdXJpIjoiaHR0cHM6Ly9hY2NlcHRhdGllLmF1dGguZGV6aS5ubC9zdGF0dXMvdjEvdmVya2xhcmluZy8wZTk3MGZjYi01MzBjLTQ4MmUtYmEyOC00N2I0NjFkNGRjYjUiLCJuYmYiOjE3NzI2NjUyMDAsImV4cCI6MTc4MDYxMDQwMCwiaXNzIjoiaHR0cHM6Ly9hYm9ubmVlLmRlemkubmwifQ.ipR4stqmO8MOmmapukeQxIOVpwO_Ipjgy5BHjUsdCvuFObhVrj48AQCndtV48D_Ol1hXO4s9p4b-1epjEiobjEmEO0JQNU0BAOGG0eWl8MujfhzlDnmwo5AEtvdgTjlnBaLReVu1BJ8KYgc1DT7JhCukq9z5wZLqU1aqtETleX2-s-dNdTdwrUjJa1DvIgO-DQ_rCp-1tcfkr2rtyW16ztyI88Q2YdBkNGcG0if5aYZHpcQ4-121WBObUa0FhswS7EHni5Ru8KwZNq0HC8OLWw3YqLrYHTFe2K0GQjMtEO6zNxApbMXWKlgeWdf7Ry2rPpe2l9Z5NuMrFiB8JChZsQ"
deziCredential, err := credential.CreateDeziUserCredential(idToken)
require.NoError(t, err)

credentials := []vc.VerifiableCredential{*deziCredential}

ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, _ []did.DID, additionalCredentials map[did.DID][]vc.VerifiableCredential, _ pe.PresentationDefinition, _ holder.BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) {
// Assert Dezi credentials are NOT self-attested (they have an issuer)
require.Len(t, additionalCredentials, 2)
require.Len(t, additionalCredentials[primaryWalletDID], 1)
// Dezi credentials have their own issuer, not the wallet DID
assert.Equal(t, "https://abonnee.dezi.nl", additionalCredentials[primaryWalletDID][0].Issuer.String())
assert.Contains(t, additionalCredentials[primaryWalletDID][0].Type, ssi.MustParseURI("DeziUserCredential"))
require.Len(t, additionalCredentials[secondaryWalletDID], 1)
assert.Equal(t, "https://abonnee.dezi.nl", additionalCredentials[secondaryWalletDID][0].Issuer.String())
assert.Contains(t, additionalCredentials[secondaryWalletDID][0].Type, ssi.MustParseURI("DeziUserCredential"))
return createdVP, &pe.PresentationSubmission{}, nil
})

response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, credentials)

assert.NoError(t, err)
require.NotNil(t, response)
assert.Equal(t, "token", response.AccessToken)
assert.Equal(t, "bearer", response.TokenType)
})
t.Run("ok with DPoPHeader", func(t *testing.T) {
ctx := createClientServerTestContext(t)
ctx.keyResolver.EXPECT().ResolveKey(primaryWalletDID, nil, resolver.NutsSigningKeyType).Return(primaryKID, nil, nil)
Expand Down
2 changes: 1 addition & 1 deletion auth/oauth/openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

// proofTypeValuesSupported contains a list of supported cipher suites for ldp_vc & ldp_vp presentation formats
// Recommended list of options https://w3c-ccg.github.io/ld-cryptosuite-registry/
var proofTypeValuesSupported = []string{"JsonWebSignature2020"}
var proofTypeValuesSupported = []string{"JsonWebSignature2020", "DeziIDJWT"}

// DefaultOpenIDSupportedFormats returns the OpenID formats supported by the Nuts node and is used in the
// - Authorization Server's metadata field `vp_formats_supported`
Expand Down
Loading
Loading