nuts-foundation / nuts-foundation/nuts-node

OpenID4VCI: configurable request profiles (auth.experimental.profile) with built-in AET profile

Open
#4,338 0 comments 0 reactions 0 assignees View on GitHub

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
  1. 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.
  2. As a node operator, I want to define or tweak profiles in config, so I can support an issuer without a code change.
  3. 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:

  1. Node parameters (response_type, state, client_id, client_id_scheme, authorization_details, redirect_uri, code_challenge, code_challenge_method); on project-gf-pilot the configured-client block may replace client_id / drop the scheme.
  2. Profile authrequest -- set/override, multi-valued. May override node parameters (trusted operator/built-in config).
  3. 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:

  • profile names an unknown profile (not built-in, not configured) -> 400, fail loud.
  • authorization_request_params tries 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
  1. auth/config.go: ExperimentalConfig.Profiles + ProfileConfig + built-in aet.
  2. auth/auth.go: AuthorizationRequestProfile accessor (merge built-in + config).
  3. auth/interface.go + regenerated mocks: interface method.
  4. docs/_static/auth/v2.yaml + make gen-api: profile request field.
  5. auth/api/iam/openid4vci.go: resolve + apply the profile in RequestOpenid4VCICredentialIssuance.

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 for profile: aet, unknown-profile error, layering with authorization_request_params.
  • Prior art: the authorization_request_params tests in auth/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 authrequest subfield 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 profile request 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.