nuts-foundation / nuts-foundation/nuts-node
Configurable OAuth client authentication to external authorization servers (OpenID4VCI auth-code flow)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 28
- Forks
- 23
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 76
Description
Related: #4310, #3197
Experimental. This feature may change or be removed without further notice. Must be flagged as experimental in both code (doc comments) and documentation, and lives under
auth.experimental.
Problem
As an OpenID4VCI wallet in the auth-code flow, the node authenticates as a public client using its did:web base URL as client_id + client_id_scheme=entity_id:
- Authorization request:
auth/api/iam/openid4vci.go:131-140 - Token request:
auth/client/iam/openid4vp.go:227(client_idonly, no secret)
entity_id is Nuts-specific. Third-party issuers expect a registered client_id + secret. AET (OpenIddict) requires client auth -- a public client_id is refused (invalid_client) and client_secret_post is accepted. No way to configure this today, so the auth-code flow can't complete against any non-entity_id AS.
Solution
Per-server OAuth client config under auth.experimental. On a match, present the configured client_id + client_secret (client_secret_post) instead of the did:web + entity_id defaults.
auth:
experimental:
clients:
- serverurl: https://issuer.example.com/oauth # OAuth AS identifier
clientid: nuts-wallet-123
clientsecret: s3cr3t # optional
NUTS_AUTH_EXPERIMENTAL_CLIENTS_0_SERVERURL=https://issuer.example.com/oauth
NUTS_AUTH_EXPERIMENTAL_CLIENTS_0_CLIENTID=nuts-wallet-123
NUTS_AUTH_EXPERIMENTAL_CLIENTS_0_CLIENTSECRET=s3cr3t
Keys are single tokens (serverurl, not server_url): env vars map _ to the . delimiter (core/config.go:60), so ..._SERVER_URL would nest as server.url. No match -> current behavior unchanged (additive).
Implementation Decisions
Config
Add to the existing ExperimentalConfig (auth/config.go), which already documents "subject to change without notice and may be removed":
type OAuthClientConfig struct {
ServerURL string `koanf:"serverurl"`
ClientID string `koanf:"clientid"`
ClientSecret string `koanf:"clientsecret"`
}
// ExperimentalConfig: Clients []OAuthClientConfig `koanf:"clients"`
Validate at Configure: serverurl required + valid URL (HTTPS in strict mode); clientid required; reject duplicate serverurl.
Match key
Exact match of serverurl against the AS identifier authzServerMetadata.Issuer (not credential_issuer) -- credentials are an AS concept and the token request targets the AS. AET splits them: credential_issuer = .../aet, AS = .../aet-oauth. Operators configure the AS URL from authorization_servers. One lookup helper, shared by both request sites.
Auth method
client_secret_post only for now.
TODO: derive method from AS metadata
token_endpoint_auth_methods_supported(fallback post when absent) and supportclient_secret_basic. AET omits the field, so deriving buys nothing yet.
Request changes
| Site | On match |
|---|---|
Authorization request (api/iam/openid4vci.go:131) |
configured client_id, omit client_id_scheme |
Token request (client/iam/openid4vp.go:227) |
configured client_id; secret as client_secret form field, or public client if empty |
Modules
auth/config.go: type +ExperimentalConfig.Clientsfield + validation + experimental doc comment.auth/auth.go:135: thread intoiam.ClientConfig+ reach theapi/iamWrapper.auth/api/iam/openid4vci.go: authorization request.auth/client/iam/openid4vp.go: token request.
Testing
Assert wire output via httptest.Server: on match, redirect carries configured client_id and no client_id_scheme, token request carries client_id + client_secret; no match = today's entity_id shape.
auth/config.go: validation +NUTS_AUTH_EXPERIMENTAL_CLIENTS_0_*array binding.auth/api/iam: authorization-requestclient_id/scheme, match vs no-match.auth/client/iam: token-request public client +client_secret_post.
Impact
- Compat: additive; no configured clients -> unchanged.
- Versioning: minor. Experimental: may change or be removed without notice.
- Security:
clientsecretredacted in/configoutput and logs, never logged in the token request body. Strict mode = HTTPS-only token endpoint.
Out of Scope
- Other protocols/flows (plain OAuth2, OpenID4VP, pre-authorized code). Config shape anticipates them; only OpenID4VCI auth-code is wired here.
- Metadata-derived auth method +
client_secret_basic(TODO above). - Asymmetric client auth (
client_secret_jwt,private_key_jwt); dynamic client registration. - Replacing
did:web+entity_idfor Nuts-to-Nuts flows (#3197).
Further Notes
Only target now is the AET OpenID4VCI issuer; shape kept protocol-neutral for later reuse.
AET probe (via nuts-services.nl bridge): public client_id -> invalid_client; client_secret_post accepted (localhost/password reached invalid_grant on a dummy code). token_endpoint_auth_methods_supported absent; grant_types_supported: ["authorization_code"].
"Send no client_id" is not covered: an entry requires clientid.
Implementation Plan
| # | Description | PR | Status | Depends on |
|---|---|---|---|---|
| 1 | Config type (under experimental), validation, env binding, lookup helper | -- | -- | -- |
| 2 | Authorization request: configured client_id, omit entity_id scheme |
-- | -- | #1 |
| 3 | Token request: configured client_id + client_secret_post |
-- | -- | #1 |
| 4 | Config-reference docs regen (with experimental warning) + clientsecret redaction |
-- | -- | #1 |
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in auth/config.go and auth/auth.go:135 to trace ExperimentalConfig into iam.ClientConfig and the API wrapper. Then read auth/api/iam/openid4vci.go and auth/client/iam/openid4vp.go, along with their existing tests. Done means validated experimental client configuration, correct matched and unmatched wire output, environment binding, and client-secret redaction.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, authentication, backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100