Skip to content

OpenID4VCI: make wallet_did optional in RequestCredential, default to the subject's did:web #4365

Description

@reinkrul

Context

Follow-up to the v2 API design. RequestOpenid4VCICredentialIssuance (POST /internal/auth/v2/{subjectID}/request-credential) currently requires the caller to pass wallet_did in the request body — the DID that should receive the credential.

In practice, v2 API consumers (EHRs) work with the subject of a care organization, not with its DIDs. Requiring wallet_did forces the caller to first make a separate call to enumerate the subject's DIDs just to pick one to pass back in. That extra round-trip is friction for limited functional gain.

Problem

Endpoint Takes wallet/holder DID? How DID is chosen
request-service-access-token No determineClientDID → first DID from ListDIDs (api.go:1013)
request-user-access-token No derived from subject internally
request-credential (this issue) Yes, required caller-supplied, validated against ListDIDs

Proposal

Make wallet_did optional in the request body.

  • When wallet_did is provided: keep current behavior — parse it, validate ownership via subjectOwns, fail loud with InvalidInputError("wallet DID does not belong to the subject") if it is not owned by the subject. No change for existing callers (fully backwards compatible).
  • When wallet_did is omitted: default to the subject's did:web DID.
    • Exactly one did:web DID → use it.
    • Multiple did:web DIDs → fail loud with InvalidInputError (ambiguous; caller must specify wallet_did).
    • Zero did:web DIDs → fail loud with InvalidInputError (e.g. a subject that only has did:nuts; caller must specify wallet_did).

Rationale for defaulting to did:web only:

  • OpenID4VCI is in practice only used with did:web today; did:nuts is not used with this flow.
  • A subject is not expected to have more than one did:web DID — there's no functional reason to.
  • Callers that want a different DID type (e.g. did:nuts) can still do so explicitly by passing wallet_did.

Scope

  • docs/_static/auth/v2.yaml — remove wallet_did from required, document that it defaults to the subject's sole did:web DID when omitted, and that omitting it errors when the subject has zero or multiple did:web DIDs
  • Regenerate the API (make gen-api)
  • auth/api/iam/openid4vci.go — handle the optional/omitted branches in RequestOpenid4VCICredentialIssuance
  • auth/api/iam/api.go — add the did:web selection helper (filter ListDIDs to did:web, enforce exactly one)
  • Tests, docs, release notes

Potential problems / footguns

  • Implicit DID selection hides where the credential lands. With wallet_did omitted, the caller no longer states which DID holds the issued credential. If the response doesn't echo the chosen DID, the caller has no record of where it went. Mitigation: return the resolved wallet_did in the response so the choice is observable.
  • "Sole did:web" can change over time. A subject that has exactly one did:web today may gain a second later, silently flipping previously-working omit-the-field calls into InvalidInputError. The behavior is correct (fail loud), but it's a latent break for callers that relied on the default — worth a clear error message pointing at wallet_did.
  • Convenience may entrench a bad assumption. Defaulting only to did:web bakes "OpenID4VCI == did:web" into the API surface. If did:nuts (or another method) is ever used with this flow, the default becomes misleading rather than just incomplete. The explicit-DID escape hatch keeps it correct, but reviewers should be comfortable with the API nudging callers toward did:web.

Considerations

  • Selection is intentionally restricted to did:web rather than reusing determineClientDID (which picks candidateDIDs[0] and enforces the entity_id client_id scheme). A blind "first DID" could pick a did:nuts DID and silently change behavior — the explicit did:web-only rule avoids that.
  • Failure modes are explicit: ambiguous (multiple did:web) and empty (no did:web) both return InvalidInputError, never a silent fallback or panic.

Related

  • Triggering rationale: v2 API consumers (EHRs) operate on subjects, not DIDs.
  • Precedent: request-service-access-token, request-user-access-token (subject-only).

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