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).
Context
Follow-up to the v2 API design.
RequestOpenid4VCICredentialIssuance(POST /internal/auth/v2/{subjectID}/request-credential) currently requires the caller to passwallet_didin 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_didforces 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
request-service-access-tokendetermineClientDID→ first DID fromListDIDs(api.go:1013)request-user-access-tokenrequest-credential(this issue)ListDIDsProposal
Make
wallet_didoptional in the request body.wallet_didis provided: keep current behavior — parse it, validate ownership viasubjectOwns, fail loud withInvalidInputError("wallet DID does not belong to the subject")if it is not owned by the subject. No change for existing callers (fully backwards compatible).wallet_didis omitted: default to the subject'sdid:webDID.did:webDID → use it.did:webDIDs → fail loud withInvalidInputError(ambiguous; caller must specifywallet_did).did:webDIDs → fail loud withInvalidInputError(e.g. a subject that only hasdid:nuts; caller must specifywallet_did).Rationale for defaulting to
did:webonly:did:webtoday;did:nutsis not used with this flow.did:webDID — there's no functional reason to.did:nuts) can still do so explicitly by passingwallet_did.Scope
docs/_static/auth/v2.yaml— removewallet_didfromrequired, document that it defaults to the subject's soledid:webDID when omitted, and that omitting it errors when the subject has zero or multipledid:webDIDsmake gen-api)auth/api/iam/openid4vci.go— handle the optional/omitted branches inRequestOpenid4VCICredentialIssuanceauth/api/iam/api.go— add thedid:webselection helper (filterListDIDstodid:web, enforce exactly one)Potential problems / footguns
wallet_didomitted, 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 resolvedwallet_didin the response so the choice is observable.did:webtoday may gain a second later, silently flipping previously-working omit-the-field calls intoInvalidInputError. 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 atwallet_did.did:webbakes "OpenID4VCI == did:web" into the API surface. Ifdid: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 towarddid:web.Considerations
did:webrather than reusingdetermineClientDID(which pickscandidateDIDs[0]and enforces theentity_idclient_id scheme). A blind "first DID" could pick adid:nutsDID and silently change behavior — the explicitdid:web-only rule avoids that.did:web) and empty (nodid:web) both returnInvalidInputError, never a silent fallback or panic.Related
request-service-access-token,request-user-access-token(subject-only).