Skip to content

DID sign-in and the org host exchange (#plan step 7) - #24

Open
elffjs wants to merge 3 commits into
mainfrom
org-host-step-7
Open

elffjs wants to merge 3 commits into
mainfrom
org-host-step-7

Conversation

@elffjs

@elffjs elffjs commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Step 7 of the did-directory org host plan, dauth's share. No backwards compatibility (spec §19 decision 17).

Sign-in is by DID. /siwe becomes /signin: POST /signin/challenge {did} returns a challenge and a single-use nonce; POST /signin/token {nonce, signature, key?} verifies an ECDSA signature over SHA-256 of the challenge (the directory's r||s form, via did-directory/pkg/didkey) against the verification method the DID document lists at token time (#signing by default; #dimo_org is refused as a login key), and mints an RS256 identity token with sub = DID. The org host accepts these as member identity; dimocli signin drives it.

POST /exchange takes an identity token plus an RFC 9449 DPoP proof, calls the org host's /authorize as DAUTH_DID (with an identity token from its own sign-in issuer), and mints a DPoP-bound access token {sub, aud, cnf.jkt, grants}. One grant per (subject, window set): live abilities carry no windows, suspended abilities are dropped, every grant carries the chain. 15 minutes with any live ability, 2 hours otherwise. An optional client_assertion, an identity token for the app's DID from the app's own sign-in, is verified like the caller's token and its sub goes to the host as clientId for the delegation's clientAllowlist; without one no client is claimed.

Public packages for dq: pkg/tokenclaims (the claim set, Holds, Windows.Clamp/ClampOpen, the ability vocabulary) and pkg/dpop (proofs, verification, replay cache).

Deleted: the on-chain SACD exchange, developer-licence check, gRPC AccessCheck and pkg/grpc (vehicle-triggers-api called this), swagger, all Ethereum deps. internal/siwe is renamed internal/signin; config is SIGNIN_*, plus DIRECTORY_URL, ORG_HOST_URL, DAUTH_DID, EXCHANGE_AUDIENCE. Chart values and the local run script follow.

Pins did-directory at DIMO-Network/did-directory#17's commit. That repo is private, so this public module now needs GOPRIVATE/SSH access to it in CI and for anyone building it. Smoke-run against a live directory, org host and this binary: sign-in for an org and a member, org registration, opening and delegation writes, then an exchange yielding a token whose cnf.jkt matched the proof key, with correct refusals for a missing proof, a wrong caller and an uncovered ability.

🤖 Generated with Claude Code

https://claude.ai/code/session_011mDMpJffFJ4BmUyZPtb6j2

dauth becomes the identity and access token service of the delegation
model (did-directory plan §6 step 7, spec §11.1). No backwards
compatibility (spec §19 decision 17).

Sign-in is by DID. Decision B needs the identity token's sub to be a DID,
and a spec §4 person holds a passkey rather than a wallet, so /siwe is
replaced by /signin: a challenge for a DID, signed with a key its DID
document lists (the #signing key dimocli publishes; #dimo_org is refused
as a login key), verified against the directory at token time, and an
RS256 identity token with sub = DID. The org host takes these as member
identity; dimocli signin drives the flow.

/exchange takes an identity token plus an RFC 9449 DPoP proof, asks the
org host's POST /authorize as DAUTH_DID whether the caller may exercise
the named delegation for the named vehicle and abilities, and mints a
DPoP-bound access token: {sub, aud, cnf.jkt, grants}. A grant is one
(subject, window set): the host's per-ability windows are grouped, live
abilities get a grant with no windows, suspended abilities are left out,
and every grant carries the chain. Lifetime is 15 minutes with any live
ability, 2 hours otherwise. clientId is not sent; dauth cannot attest an
app yet, so a delegation with a clientAllowlist stays refused.

pkg/tokenclaims is the new claim set with Holds and window clamping for
dq; pkg/dpop is proof creation and verification with a replay cache, for
dq and clients. Deleted: the on-chain SACD exchange, the developer
licence check, the gRPC AccessCheck service and pkg/grpc, the swagger
specs, and every Ethereum dependency.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mDMpJffFJ4BmUyZPtb6j2
elffjs and others added 2 commits September 22, 2026 14:02
The plan left open what an app is and how dauth attests it, so /exchange
sent no clientId and a delegation with a clientAllowlist stayed refused.
An app now attests itself the way a caller does: client_assertion is an
identity token for the app's DID from the app's own sign-in, verified
like the caller's token, and its sub goes to the host as clientId. dauth
vouches only that the app holds its key; whether the delegation admits
that app is the host's call. An assertion naming the caller is refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mDMpJffFJ4BmUyZPtb6j2
- /exchange accepts only identity tokens addressed to
  <PUBLIC_BASE_URL>/exchange; a sign-in may always ask for the exchange
  or the org host as its audience. A token a user signed in with for
  another service no longer mints access tokens as them.
- client_assertion is an RFC 7523-style JWT the app signs with its DID
  key: aud = the exchange, exp at most five minutes after iat, single-use
  jti, and cnf.jkt bound to this request's DPoP key. An identity token is
  no longer accepted as one, since whoever held it could sign in as the
  app and replay it for any caller.
- The minted token carries only the abilities requested, and a host
  answer for another vehicle is refused. A host 400 is the caller's
  invalid_request, not a 502.
- tokenclaims.Validate requires exp, cnf.jkt, and windows on grants of
  historical abilities.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant