Skip to content

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

Description

@reinkrul

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, OpenID4VCI: add authorization_request_params to requestCredential for issuer-specific authorization parameters #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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions