nuts-foundation / nuts-foundation/nuts-node
OpenID4VCI: configurable request profiles (auth.experimental.profile) with built-in AET profile
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 28
- Forks
- 23
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 76
Description
Related: #4316, #4328
Problem Statement
Integrating an EHR with an external OpenID4VCI issuer often requires a set of issuer-specific authorization request parameters. The AET ZORG-ID SDK, for example, needs both auth_method=SmartCard and scope=openid profile api on the authorization request, and more parameters are likely as integrations grow.
Today the only way to add these is the raw authorization_request_params field (#4328) on each requestCredential call. That pushes issuer-specific knowledge into every EHR and is a footgun: each integrator must know the exact parameter set, spell it correctly, and keep it in sync. Getting it wrong fails at the issuer, often with unclear errors.
We want a curated, named bundle of parameters -- a profile -- that the node ships and operators can extend, so an EHR only has to say "use the aet profile" instead of repeating the parameter set.
Solution
Add named profiles in config under auth.experimental.profile.<name>, each with an authrequest field (map[string][]string) that sets authorization request parameters. The node ships a built-in aet profile; operators can add profiles or override the built-in by name (merge per parameter key, operator wins).
A new optional profile field on the requestCredential body selects a profile by name. The node applies the profile's authrequest parameters to the OpenID4VCI authorization request.
auth:
experimental:
profile:
aet: # ships built-in; this block (if present) merges over it
authrequest:
auth_method: [SmartCard]
scope: ["openid profile api"]
{
"issuer": "https://issuer.example.com/oauth",
"wallet_did": "did:web:example.com",
"authorization_details": [ { "type": "openid_credential", "credential_configuration_id": "..." } ],
"redirect_uri": "https://example.com/oauth2/org1/callback",
"profile": "aet"
}
-> redirect: .../authorize?...&auth_method=SmartCard&scope=openid+profile+api.
The subfield is named authrequest so the same profile can later carry parameter sets for other requests (OpenID4VP, OpenID4VCI Credential Request) without a config redesign.
User Stories
User Stories
- As an EHR integrator, I want to name a profile (
"profile": "aet") instead of hand-listing issuer parameters, so I can't get the parameter set wrong. - As a node operator, I want to define or tweak profiles in config, so I can support an issuer without a code change.
- As a maintainer, I want common issuers shipped as built-in profiles, so integrators get them out of the box.
Implementation Decisions
Configuration
// auth/config.go -- under ExperimentalConfig, alongside Clients.
type ExperimentalConfig struct {
// ...
Profiles map[string]ProfileConfig `koanf:"profile"`
}
type ProfileConfig struct {
// AuthorizationRequest sets parameters on the OpenID4VCI authorization request. Multi-valued per key.
AuthorizationRequest map[string][]string `koanf:"authrequest"`
}
EXPERIMENTAL: this configuration may change or be removed without further notice (under auth.experimental).
Built-in profiles live in code and are merged under operator config per parameter key:
var builtinProfiles = map[string]ProfileConfig{
"aet": {AuthorizationRequest: map[string][]string{
"auth_method": {"SmartCard"},
"scope": {"openid profile api"},
}},
}
Lookup
// Returns the merged authrequest params for a profile (built-in overlaid by config), false if the name is unknown.
func (auth *Auth) AuthorizationRequestProfile(name string) (map[string][]string, bool)
Consulted from the api/iam Wrapper via the AuthenticationServices interface, mirroring OAuthClientCredentials.
Applying to the authorization request
Order, building the authorization redirect query:
- Node parameters (
response_type, state, client_id, client_id_scheme, authorization_details, redirect_uri, code_challenge, code_challenge_method); onproject-gf-pilotthe configured-client block may replaceclient_id/ drop the scheme. - Profile
authrequest-- set/override, multi-valued. May override node parameters (trusted operator/built-in config). authorization_request_params(raw, #4328) -- applied last; may not override the node parameters from step 1 (rejected with 400), but may add or override profile-set parameters.
Failure modes:
profilenames an unknown profile (not built-in, not configured) -> 400, fail loud.authorization_request_paramstries to override a node parameter -> 400 (unchanged from #4328).
The query is built as url.Values (was map[string]string) to support multi-valued parameters; scope: ["openid profile api"] renders as a single space-joined value, ["a","b"] renders as repeated parameters.
Modules to build/modify
auth/config.go:ExperimentalConfig.Profiles+ProfileConfig+ built-inaet.auth/auth.go:AuthorizationRequestProfileaccessor (merge built-in + config).auth/interface.go+ regenerated mocks: interface method.docs/_static/auth/v2.yaml+make gen-api:profilerequest field.auth/api/iam/openid4vci.go: resolve + apply the profile inRequestOpenid4VCICredentialIssuance.
Testing Decisions
Assert external behavior. For the accessor: built-in aet returns its parameters; operator config merges/overrides per key; unknown name returns false. For the handler: a request with profile: "aet" produces a redirect whose query carries auth_method=SmartCard and scope=openid profile api; an unknown profile returns 400; profile + authorization_request_params layer in the documented order.
Modules to test:
auth:AuthorizationRequestProfile(built-in, merge, unknown).auth/api/iam: redirect query forprofile: aet, unknown-profile error, layering withauthorization_request_params.- Prior art: the
authorization_request_paramstests inauth/api/iam/openid4vci_test.go.
Impact Assessment
Backwards compatibility: additive. No profile -> unchanged behavior. authorization_request_params keeps working.
Versioning: minor. Experimental: may change or be removed without notice.
Configuration/deployment: new auth.experimental.profile.<name>.authrequest; not a CLI flag (map type), so it loads from YAML/env and does not appear in server_options.rst. Built-in aet needs no config.
Security: profiles are operator/built-in config (trusted), so they may override node parameters -- a misconfigured profile could break the flow (e.g. overwrite state/code_challenge); this is the operator's responsibility. The raw per-request field stays locked down (cannot override node parameters).
Out of Scope
- Profile parameter sets for other requests (OpenID4VP, OpenID4VCI Credential Request) -- the
authrequestsubfield name reserves room; not implemented now. - Removing
authorization_request_params-- kept as the low-level escape hatch. - Per-issuer automatic profile binding -- selection is explicit via the
profilerequest field.
Further Notes
Built-in aet uses scope and auth_method; neither is a node-set authorization parameter today, so aet only adds.
Implementation Plan
| # | Description | PR | Status | Depends on |
|---|---|---|---|---|
| 1 | auth.experimental.profile config + ProfileConfig + built-in aet + accessor |
-- | -- | -- |
| 2 | profile request field + apply in authorization request (multi-value query) |
-- | -- | #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 with auth/config.go, auth/auth.go, auth/interface.go, and the existing authorization_request_params tests in auth/api/iam/openid4vci_test.go. Trace RequestOpenid4VCICredentialIssuance and the AuthenticationServices interface before running the auth and auth/api/iam tests. Done means built-in and configured profiles merge correctly, the profile field produces the documented multi-value redirect query, unknown profiles return 400, and raw parameters retain their layering rules.
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