Login with DID
Traditional apps store password hashes or delegate to an OAuth provider. DID login is different: the server never learns a secret. Instead, it issues a random challenge, and the client returns a Verifiable Presentation signed with the private key controlling a did:elastos:i... identity. The server verifies the signature against the user's DID document on the EID chain, then issues a JWT.
This page covers the end-to-end pattern: challenge issuance, VP verification with @elastosfoundation/did-js-sdk, and session tokens.
Elastos DIDs follow the W3C DID data model: a DID resolves to a DID document containing verification methods (public keys). Presentations and credentials in this flow align with Verifiable Credentials usage patterns.
The Authentication Flow
The following steps match how @elastosfoundation/did-js-sdk validates presentations:
- Server generates a random challenge (nonce) and returns it to the client. Store it server-side with a short expiry (e.g. 3 minutes).
- Client builds a Verifiable Presentation that includes the challenge, signed with the user's DID private key.
- Server receives the presentation JSON, parses it with
VerifiablePresentation.parse(), and validates withawait vp.isValid(). Internally, the SDK resolves the holder DID viaDIDBackend(JSON-RPC to the EID resolver). - If valid, the server issues a session token (JWT) bound to the holder DID.
Treat challenges like one-time, short-lived secrets. Expire them within a few minutes, delete or mark used after a successful login, and reject reused nonces to block replay attacks.
Server-Side Setup (Node.js / Express)
npm install @elastosfoundation/did-js-sdk express jsonwebtoken
Initialize the resolver once at startup. DefaultDIDAdapter accepts "mainnet", "testnet", or a full resolver URL.
import crypto from "crypto";
import express from "express";
import jwt from "jsonwebtoken";
import {
DIDBackend,
DefaultDIDAdapter,
VerifiablePresentation,
} from "@elastosfoundation/did-js-sdk";
const SECRET = process.env.JWT_SECRET;
const app = express();
app.use(express.json());
DIDBackend.initialize(new DefaultDIDAdapter("https://api.elastos.io/eid"));
const pending = new Map();
app.post("/auth/challenge", (req, res) => {
const nonce = crypto.randomBytes(32).toString("hex");
const exp = Date.now() + 3 * 60 * 1000;
pending.set(nonce, exp);
res.json({ challenge: nonce });
});
app.post("/auth/verify", async (req, res) => {
try {
const { presentation } = req.body;
const vp = VerifiablePresentation.parse(presentation);
const valid = await vp.isValid();
if (!valid) return res.status(401).json({ error: "invalid presentation" });
const did = vp.getHolder().toString();
const token = jwt.sign({ did }, SECRET, { expiresIn: "7d" });
res.json({ token, did });
} catch (e) {
res.status(400).json({ error: String(e) });
}
});
Production checklist: verify the challenge in the presentation proof matches a pending nonce from /auth/challenge, confirm not expired, then invalidate that nonce.
Resolving a DID Explicitly
const holder = vp.getHolder();
const doc = await holder.resolve();
DID.resolve() delegates to DIDBackend.getInstance().resolveDid(), which posts to the adapter's EID endpoint.
Client-Side: Essentials and Connectivity SDK
The browser does not hold DID private keys unless you embed a DID store. Typical flows use:
- Elastos Essentials -- mobile wallet that manages
did:elastos:identities and signs presentations - Elastos Connectivity SDK -- abstracts wallet discovery and message signing for web apps
Typical Client Sequence
POST /auth/challenge-- receive challenge string- Ask the wallet (via Connectivity or Essentials) to create and sign a
VerifiablePresentationwith the challenge as the proof nonce POST /auth/verifywith{ presentation: <JSON> }- Store the returned JWT for subsequent API calls
You never send the private key to the server -- only the presentation JSON.
Use the Connectivity SDK to route "sign this presentation" requests to Essentials (or compatible wallets) with less glue code than hand-rolling wallet integration.
Tokens and API Authorization
After isValid() succeeds, embed did in JWT claims and set exp appropriately. Subsequent requests send Authorization: Bearer <token>; middleware verifies the JWT and loads your app user by DID.
The DID itself is a stable identifier across sessions; your user database should key off did:elastos:... strings.
Common Errors
| Symptom | Likely Cause |
|---|---|
DIDNotFoundException / resolve failures | Typo in DID, wrong network adapter (mainnet vs testnet), or RPC unreachable |
isValid() returns false | Revoked credential, wrong challenge nonce, clock skew, or tampered presentation |
| Parse throws | Body is not valid VP JSON |
| JWT invalid on reload | SECRET changed between deploys or token expired |
Security Notes
| Topic | Practice |
|---|---|
| Challenge TTL | ~3 minutes; reject stale challenges |
| Replay | One-time nonces; store and consume server-side |
| Trust anchor | Resolution trusts the EID chain; run your own resolver if your threat model requires it |
| Session | Sign JWTs with a strong secret; rotate keys; HTTPS only |
| Holder binding | Ensure the DID in the JWT matches vp.getHolder().toString() after validation |
| Deactivation | Check did.isDeactivated() before issuing long-lived tokens |
Rate limiting: protect /auth/challenge and /auth/verify with per-IP limits.
Next steps: register a testnet DID in Essentials, point DIDBackend at testnet, and test the flow before switching to mainnet.