Sign In With DFOS (SIWD)
Cryptographic identity verification for third-party applications — Ed25519 challenge-response via a universal /authorize endpoint. One flow, two signing paths (managed and sovereign), same JWS output. Verification is pure crypto — no DFOS server in the loop after issuance.
Specification only. This document describes the SIWD protocol design. No reference implementation exists yet in this repository — it is published here for review and to inform implementors.
Overview
SIWD lets any third-party application verify a user's DFOS identity. The third party redirects to a single /authorize URL on the DFOS platform. The user consents, the challenge is signed with their DID key, and the callback delivers a standard JWS. The third party verifies the signature against the user's identity chain — resolved from any relay — without contacting the DFOS platform.
Two signing paths exist behind the same endpoint:
| Path | Signer | Trust model |
|---|---|---|
| Managed | Platform signs via KMS-held key | Platform custody — user trusts the platform to sign on their behalf |
| Sovereign | User's local Go CLI signs via local key | Self-custody — user holds the key, platform never touches it |
The third party never knows which path was used. Both produce the same JWS format, both reference keys in the same identity chain, both verify identically.
Flow
1. Redirect to authorize
The third-party application redirects the user to the platform's /authorize endpoint:
https://dfos.com/authorize?
challenge=<base64url-encoded challenge JSON>
&redirect_uri=https://3p.com/callback
&scope=identity
Query parameters:
| Parameter | Required | Description |
|---|---|---|
challenge |
Yes | Base64url-encoded challenge object (see Challenge Schema) |
redirect_uri |
Yes | URL the platform redirects to after signing |
scope |
Yes | Comma-separated list of requested scopes |
Scopes:
| Scope | Meaning |
|---|---|
identity |
Prove DID ownership only |
read:<chainType>:<contentId> |
Prove DID + return a read credential for the specified content chain |
2. Consent screen
The platform authenticates the user (existing session) and presents a consent screen. The screen describes what the third party is requesting — identity verification alone, or identity plus scoped resource access.
If the user has local signing enabled, both signing options are presented. Otherwise, only managed signing is available.
3. Signing
The user's DID key signs the challenge as a JWS compact token. See Managed Signing Path and Sovereign Signing Path for details.
4. Callback
The platform (or local CLI) redirects to the redirect_uri with the signed challenge:
https://3p.com/callback?
jws=<signed challenge JWS>
&did=did:dfos:xxxxxxxxxxxxxxxxxxxx
If a credential was requested via scope, it is included as an additional parameter:
&credential=<DFOS credential JWS>
Challenge Schema
The challenge is a JSON object, base64url-encoded in the challenge query parameter:
{
"domain": "3p.com",
"nonce": "a8f2e93b...",
"timestamp": "2026-04-13T15:30:00.000Z",
"statement": "Sign in to 3P App",
"did": "did:dfos:xxxxxxxxxxxxxxxxxxxx"
}
| Field | Required | Description |
|---|---|---|
domain |
Yes | Origin domain of the requesting application. MUST match the domain in redirect_uri. |
nonce |
Yes | Unique value generated by the third party, used for replay prevention. |
timestamp |
Yes | ISO 8601 timestamp of challenge creation. |
statement |
No | Human-readable description shown on the consent screen. |
did |
No | If provided, binds the challenge to a specific DID. The platform MUST reject signing if the authenticated user's DID does not match. |
The challenge is signed as a JWS using the user's DID key with alg: "EdDSA". The JWS kid header contains the DID URL of the signing key (did:dfos:<id>#<keyId>), following the same convention as identity and content chain operations.
Managed Signing Path
The platform holds the user's DID key material in a KMS (Key Management Service). When the user consents via the managed path:
- Platform verifies the user's session.
- Platform signs the challenge with the user's KMS-held key.
- Platform redirects to
redirect_uriwith the signed JWS and DID.
The KMS key is one of the keys declared in the user's identity chain (authKeys or controllerKeys). The signature is indistinguishable from any other Ed25519 signature over the challenge — the third party verifies it against the identity chain like any other key.
Sovereign Signing Path
Users who hold their own keys via the DFOS Go CLI can sign challenges locally. The platform does not touch the key material.
Configuration
The user enables local signing in their platform settings:
| Setting | Type | Description |
|---|---|---|
localSigningEnabled |
boolean | Whether the sovereign signing option is presented on consent screens |
localSigningPort |
number | Port the local CLI listens on (default: 8420) |
Flow
- User selects "Sign locally" on the consent screen.
- Platform redirects to
http://localhost:<port>/authorizewith the samechallengeandredirect_uriparameters. - The Go CLI receives the request, presents consent (terminal or local web UI), and signs the challenge with the locally-held key.
- The CLI redirects to
redirect_uriwith the signed JWS and DID.
The local key MUST be declared in the user's identity chain (authKeys). The third party resolves the identity chain and finds the key — same verification as the managed path.
Failure handling
If the user selects sovereign signing but the CLI is not running, the browser fails to connect to localhost. The user navigates back and falls through to managed signing. No state is corrupted — the challenge is stateless and can be signed by either path.
The platform MAY perform a preflight health check (GET http://localhost:<port>/health) to disable the sovereign signing button when the CLI is not reachable.
Third-Party Verification
Verification is identical regardless of signing path:
- Decode the JWS — extract the challenge payload,
kidheader (DID URL of signing key), and signature. - Resolve the DID — fetch the identity chain from any DFOS relay. Extract the public key matching the
kid. - Verify the signature — standard Ed25519 verification of the JWS against the resolved public key.
- Validate the nonce — confirm the
noncein the challenge payload matches the server-side value issued to this session. Discard the nonce after use. - Validate the timestamp — reject challenges older than a reasonable window (implementation-defined, e.g., 5 minutes).
- Validate the domain — confirm the
domainin the challenge matches the verifier's own origin.
If a credential was returned, the third party stores it and presents it to relays for scoped access. See Optional Credential Return.
No DFOS platform server is contacted during verification. The third party only needs access to a relay (any relay) to resolve the DID's identity chain.
Optional Credential Return
When scope includes resource access beyond identity, the callback includes a DFOS credential alongside the signed challenge.
User-owned content
For content owned by the user's DID, the credential is issued by that DID:
{
"type": "DFOSCredential",
"iss": "did:dfos:<user>",
"aud": "did:dfos:<3p_app>",
"att": [{ "resource": "chain:<contentId>", "action": "read" }],
"prf": [],
"exp": 1752700000
}
Space-owned content
For content owned by a space (a separate DID), the credential is issued by the space's DID, not the user's. The platform mediates: the user consents, the platform verifies the user's membership and permissions within the space, then issues the credential from the space's DID.
The third party presents the credential to any relay hosting that content. The relay verifies the credential against the space's identity chain and grants scoped access.
Security Considerations
Replay prevention
The nonce field is the primary replay defense. The third party MUST:
- Generate a cryptographically random nonce per authorization request.
- Store it server-side, bound to the user's session.
- Reject any callback where the nonce does not match or has already been consumed.
- Expire unused nonces after a short window.
The timestamp field provides a secondary bound — challenges with stale timestamps SHOULD be rejected even if the nonce is valid.
Redirect URI validation
The platform MUST validate redirect_uri against a registered allowlist for the requesting application. Open redirectors allow phishing — an attacker could substitute their own callback URL to capture signed challenges.
The domain field in the challenge MUST match the domain of the redirect_uri. The platform MUST reject requests where these diverge.
Challenge binding
If the did field is present in the challenge, the platform MUST refuse to sign with any other DID. This prevents an attacker from substituting a different user's identity into a challenge intended for a specific user.
Localhost security (sovereign path)
The sovereign path redirects to localhost, which is not TLS-protected. This is acceptable because:
- The signing key never leaves the local machine.
- The challenge is not secret — it is a value the user is explicitly consenting to sign.
- The redirect back to
redirect_uriuses HTTPS.
The CLI SHOULD bind exclusively to 127.0.0.1 (not 0.0.0.0) to prevent network-adjacent access.
Token lifetime
Signed challenges are single-use authentication proofs, not bearer tokens. Third parties SHOULD establish their own session after verification and discard the JWS.
Credentials returned via scope have an explicit exp (expiration) field. Third parties MUST respect expiration and re-request credentials when they expire.