Skip to main content
EID

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.

W3C DID standard

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:

  1. Server generates a random challenge (nonce) and returns it to the client. Store it server-side with a short expiry (e.g. 3 minutes).
  2. Client builds a Verifiable Presentation that includes the challenge, signed with the user's DID private key.
  3. Server receives the presentation JSON, parses it with VerifiablePresentation.parse(), and validates with await vp.isValid(). Internally, the SDK resolves the holder DID via DIDBackend (JSON-RPC to the EID resolver).
  4. If valid, the server issues a session token (JWT) bound to the holder DID.
Challenge expiry

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:

  1. Elastos Essentials -- mobile wallet that manages did:elastos: identities and signs presentations
  2. Elastos Connectivity SDK -- abstracts wallet discovery and message signing for web apps

Typical Client Sequence

  1. POST /auth/challenge -- receive challenge string
  2. Ask the wallet (via Connectivity or Essentials) to create and sign a VerifiablePresentation with the challenge as the proof nonce
  3. POST /auth/verify with { presentation: <JSON> }
  4. Store the returned JWT for subsequent API calls

You never send the private key to the server -- only the presentation JSON.

Connectivity SDK

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

SymptomLikely Cause
DIDNotFoundException / resolve failuresTypo in DID, wrong network adapter (mainnet vs testnet), or RPC unreachable
isValid() returns falseRevoked credential, wrong challenge nonce, clock skew, or tampered presentation
Parse throwsBody is not valid VP JSON
JWT invalid on reloadSECRET changed between deploys or token expired

Security Notes

TopicPractice
Challenge TTL~3 minutes; reject stale challenges
ReplayOne-time nonces; store and consume server-side
Trust anchorResolution trusts the EID chain; run your own resolver if your threat model requires it
SessionSign JWTs with a strong secret; rotate keys; HTTPS only
Holder bindingEnsure the DID in the JWT matches vp.getHolder().toString() after validation
DeactivationCheck 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.