Local applications
Everything in the setup recipe assumes a domain: you serve a
dfos-app.json, DFOS fetches it over https, and the fetch is the proof of who
is asking. A CLI, an agent, or a desktop app has no domain to serve anything
from. That is not a gap in the flow — it is a different tier of it.
A redirect to localhost, 127.0.0.1, or ::1 is the loopback tier. It has
no name and no domain on either of its shapes, and it comes in two.
The two shapes
Anonymous loopback names no client. Nothing is claimed on its behalf and
nothing can be, so it gets identity and nothing else — a signed challenge, the
user's DID, and no standing grant. A set pairing identity with a credential
scope is refused outright rather than quietly narrowed, because an app that
believed it held a scope it did not would be worse than a refusal. This is the
shape a local dev server signs in with, and it is the right smoke test for the
flow itself.
Key-proven loopback names a client_did and proves, in the same request,
that it holds that DID's key. That proof is what a credential can be issued
against, so this shape asks for credential scopes like any other tier.
What a key-proven request carries
Three things beyond the ordinary parameters, and the second two are what make the first believable:
client_did— the protocol DID your local client wants the credential issued to. A per-install identity, minted once and reused, so the consent a user gives names the same party every time.client_chain— that DID's identity chain, carried inline in the request. It is verified by exactly the rules a/.well-known/dfos-app.jsoncarriage is verified by, derivation of the namedclient_didincluded. On the loopback tier the request URL is the carriage; on the just-in-time tier the well-known document is. The verification does not change — it is the same chain replay either way.client_proof— the ask proof: a JWS over the same canonical challenge bytes the user will sign, made by a key that is current in that chain's auth set, checked before the consent screen renders at all.
Together those say something a domain cannot: the party asking holds the key it is asking to be addressed as, right now, for this challenge. That closes the hole that kept loopback away from credentials in the first place — "issue a grant to whatever happens to be listening on the port."
The ask proof carries its own registered type, did:dfos:siwd-ask, distinct from
the did:dfos:siwd a user's sign-in signature carries, and the verifier requires
it exactly. Both artifacts cover the same bytes, so without separate types one
could be presented in the other's role.
What key custody does not prove
It proves key control and stops there. A verified key is not a verified author, publisher, or binary. Nothing on the DFOS side knows what the software is, who wrote it, or where the user got it — and a key that leaked to a different program on the same machine is still a current key. Treat the tier as "this is a consistent party across sign-ins," never as "this is the software it says it is."
The consent screen says exactly that much and no more.
What the user sees
There is no domain line on either loopback shape, because there is no domain that would be true.
An anonymous loopback request is headed "An app on your device" and carries a single limit row — Nothing about this app can be verified — plus one line of advice: continue only if you just started a sign-in yourself. DFOS states plainly that it cannot verify anything about the client, because it cannot.
A key-proven request is headed Local application and gets two rows, the
proof and its limit in that order: Holds the key for did:dfos:…, and Where
it runs cannot be verified. The DID is shortened head-and-tail to keep the row
one line; in full it is the target of the View on explorer link under the
heading, where a third party's view of that identity answers "who is this".
That is honest rather than thin. A name a local process asserted about itself would be strictly worse than none: the DID is a fact the platform checked, and a self-asserted name is not — which is why no name is invented for this tier, none is accepted from it, and the domain tier does not render one either.
A key-proven client can be trusted by the user, and an anonymous one never can: trust is pinned to a client DID, and an anonymous request names none. See Trust, and the second sign-in. Rotating a key within your app identity's chain keeps the same DID and changes nothing. Minting a new identity does: every user who trusted the old one sees the loud re-fire card.
The same "Local application" label follows the grant into Settings → Connected apps, where the user can revoke it like any other. A credential issued here is a platform-managed grant, not a self-custodied session — see Sessions, grants, and keys.
The 14-day ceiling
A loopback credential expires in 14 days. A credential on the just-in-time tier gets 90.
The short life is doing real work rather than mopping up abandonment. Everything
a hosted app can fall back on is absent here: there is no domain to re-check, no
/.well-known that can go dark, and no sweep that will ever notice this client
rotated or lost its key — a
carried chain is refreshed only by the next authorize, so a machine that stops
asking stops being observed at all. Expiry is the one bound that keeps running
with nobody watching.
Build for re-consent as a normal event on this tier. It is not the failure mode it would be on a hosted app.
The worked example: dfos login
The DFOS CLI implements this whole tier, and reading what it does is the fastest way to understand the shape of your own client:
# sign in as the active identity, asking only to prove who they are
dfos login
# ask for a scope that returns a credential
dfos login alice --scope read:profile
# no browser available (a container, an SSH session): print the URL and wait
dfos login --no-browser --timeout 10m
What happens, in order:
- It finds the authorize endpoint from the subject's own chain. The chain is
fetched, re-verified locally, and read for a
DfosAuthorizationServerservice entry — the sign-in server that speaks for that DID. One entry or none: anything ambiguous falls back to an explicit--authorize-urlrather than picking. - It presents a per-install client identity. Minted on first login, its key in the keystore, its DID stable across logins. That DID, its one-operation chain, and an ask proof signed by its current auth key are what go out with the request.
- It listens on a local port and opens the consent screen in a browser.
- It relays the fragment back to itself. A credential arrives in the URL fragment, which a browser never sends to a server, so the listener answers with a page whose script posts the whole URL back and scrubs the address bar.
- It verifies before it stores anything — the returned artifact's type, the signer against a freshly re-fetched chain, and the challenge consumed last, so nothing invalid can spend it. A credential issued to any DID other than this installation's is refused as inert.
Then the credential lands on disk with 0600 and every later call presents it
with a fresh request proof, exactly
as a server-side app would.
The full command reference — flags, storage paths, the JSON output — is the CLI documentation.
Graduating to a domain
The two tiers are a gradient of proof, not two different products. A client that
starts as a local application and later grows a hosted half keeps the same
DID: bind a domain to that identity, serve a dfos-app.json carrying the same
chain, and the very next request resolves as jit instead of loopback.
Nothing about the credential model changes across that move. What upgrades is only the proof of who is asking, and therefore what the consent screen is willing to say — a domain leading the card, in place of a platform-authored phrase — plus the expiry ceiling lifting to 90 days once there is a domain to re-check. The scopes, the credential, the request proofs, and the API access are identical on both tiers.
Next
- Sign in with DFOS — the flow itself, and both tiers in one page.
- Scopes and credentials — what to ask for, and how a credential lives and dies.
- Key custody — where your client's keys live, and what losing one costs. The same auth key that signs a request proof signs the ask proof.
- Troubleshooting — including what a refused key-proven request means.
- SIWD § Loopback clients — the normative rules, including the ask proof's registered type.
- Glossary — ask proof, tier, sign-in server, and the rest of the vocabulary.