Integrate AIDID login into your site

Offline-testable today

Write and self-test the RP-side integration today, entirely offline: read the wire contract, install the SDK, build a request, verify a response, and run the offline end-to-end demo. Production login is blocked until the chain-side resolve endpoint is authorised.

Journey statusOffline-testable today

Steps

  1. 1Five-minute quick start (1/4): install the SDK

    Works today

    The SDK is served as a static file from this site: no npm account, no registry account and no backend of ours in the loop. The package is pure ESM.

    Only the signature verifier imports the optional peer @msg-chain/sdk (the Dilithium-5 primitive). Message building, request and response verification, the callback reader and the drop-in button are dependency-free, so they also run inside an edge worker.

    bash
    npm install https://aidid.im/download/aidid-login-0.1.0.tgz
  2. 2Five-minute quick start (2/4): generate the request on your server

    Works today

    createSignInRequest fills type, version, an https uri default, a single-use nonce (32 CSPRNG bytes) and a 5-minute expirationTime. Keep the TTL at or below 5 minutes.

    Store the request yourself: the response only echoes it back, and that echo proves nothing. The web entry point is https://aidid.im/authorize/?payload=... and that URL must not change.

    js
    // server.js — generate a request and keep YOUR OWN copy of it.
    import { createSignInRequest, encodeSignInDeeplink } from '@aidid/login';
    import { buildAuthorizeUrl } from '@aidid/login/widget';
    
    const WALLET_ORIGIN = 'https://aidid.im'; // must be a bare https origin
    
    export const startSignIn = async (kv, domain) => {
      const request = createSignInRequest({
        domain,                                  // e.g. 'rp.example' (a port is allowed)
        chainId: 'msg-chain-1',                  // the chain the response must be bound to
        uri: `https://${domain}/aidid/callback`, // https callback on your origin
        statement: 'Sign in to my site.',
        resources: ['did:auth'],                 // add 'pay:address' only if you need the payout address
      });
    
      // Store YOUR copy (TTL <= 5 minutes). Never trust the echo inside the response.
      await kv.put(`req:${request.nonce}`, JSON.stringify(request), { ttlSeconds: 300 });
    
      return {
        authorizeUrl: buildAuthorizeUrl(request, WALLET_ORIGIN), // same device: redirect here
        deeplink: encodeSignInDeeplink(request),                 // cross device: turn this into a QR
      };
    };
  3. 3Five-minute quick start (3/4): verify the callback, fail-closed

    Offline-testable today

    The eleven ordered checks run inside verifySignInResponse. It returns { ok: false, error } and never downgrades to allow.

    Today it always fails closed at the resolve step with endpoint_unavailable, because the chain-side resolve endpoint is not authorised. That is the honest result, not a bug in your code. nonces must be a shared store (Redis, D1, a Durable Object), not createMemoryNonceStore, as soon as you run more than one instance.

    js
    // callback.js — verify fail-closed against the request YOU stored.
    import { createAgentApiResolver, createCachedResolver, verifySignInResponse } from '@aidid/login';
    import { createMsgChainVerifier } from '@aidid/login/verifiers/msgChainSdk';
    
    export const onCallback = async (kv, nonces, response) => {
      const stored = await kv.get(`req:${response.nonce}`); // look up by the returned nonce
      if (stored === null) return { ok: false, error: 'nonce_unknown' };
    
      return verifySignInResponse({
        request: JSON.parse(stored), // rebuild the canonical message from YOUR copy, never the echo
        response,
        resolve: createCachedResolver({
          resolver: createAgentApiResolver({ agentApiEndpoint: process.env.AGENT_API }),
        }), // 10 s TTL + single-flight by default; the API has no CORS, so this runs server-side
        nonces,                      // shared store: consume each nonce exactly once
        verifySignature: await createMsgChainVerifier(),
        chainId: 'msg-chain-1',
      });
    };
  4. 4Five-minute quick start (4/4): mount the drop-in button

    Works today

    The widget renders a plain button with an inline SVG and no hosted asset, so there is nothing to style or fetch. On creation it reads #response= once (the wallet returns to request.uri#response=...), clears it from the address bar, then calls onResponse exactly once — or onError with a stable reason (no_fragment is not an error).

    It does not verify anything and never approves silently, and it never puts /authorize in an iframe. Verification stays on your server.

    js
    // app.js — the drop-in button (browser, ES module, no framework).
    import { mountSignInButton } from '@aidid/login/widget';
    
    const button = mountSignInButton('#login', {
      request,                        // the request your server generated and stored
      walletOrigin: 'https://aidid.im',
      mode: 'auto',                   // 'redirect' same device | 'deeplink' cross device
      onResponse: (response) => post('/aidid/callback', { response }), // fires once, fail-closed
      onError: (error) => showError(error.reason),
      labels: { signIn: 'Sign in with AIDID' }, // override any subset, including Chinese
    });
    // Cross device: render your own QR from button.getDeeplink().
  5. 5Two transports: same device (top-level redirect) vs cross device (QR / deeplink)

    Works today

    mode: 'auto' picks redirect when a browser exists and the device looks like the one holding the DID (a mobile user agent, including iPadOS reporting a macOS UA), otherwise deeplink. The auto rule is specified in SPEC-LOGIN-v1 §9.2 (a user-agent / touch-point heuristic), and it stays a heuristic: pass mode explicitly when you know your audience better than the heuristic does.

    Same device: a top-level navigation to the wallet. After the user approves, the wallet returns to your callback URL with #response=..., and the widget reads that fragment.

    Cross device: the widget shows a web+aidid://auth?payload=... string and a copy button; render the QR with your own library, because the SDK ships no QR dependency. The phone that holds the DID approves and lands on your callback page — the browser on the other machine does not see the response by itself. Today the spec defines no server-side handoff, so the callback page must POST the fragment to your server and the originating browser must poll for the resulting session.

    Not verified on hardware: whether an iOS PWA (display: standalone) returns to the RP automatically after approval has not been tested on a real device. Test it in your own target environment and offer a fallback (for example a link the user can open manually).

    js
    // 'redirect' — same device: the SDK does a top-level navigation to
    //   https://aidid.im/authorize/?payload=<encoded JSON>
    const url = button.getAuthorizeUrl();
    
    // 'deeplink' — cross device: put this string in a QR code, or offer copy
    const deeplink = button.getDeeplink(); // web+aidid://auth?payload=...
  6. 6Read the wire contract first

    Works today

    The RP contract is four machine-readable schemas: signin-request (what you generate), signin-message (the canonical text that gets signed), signin-response (what the wallet returns) and login-error (the stable error strings you must handle).

    The prose authority is the login specification, shipped in the specs tarball. Read section 0 (honest limits) and section 7 (the eleven ordered checks) before writing code.

  7. 7Run the offline end-to-end demo (real signatures, no chain)

    Offline-testable today

    The fixture uses a fixed public test seed to derive a Dilithium-5 key pair and signs the canonical message for real. It exercises request, approve, verify and session with no network, no chain and no testnet.

    Three runnable paths: the Node flow (examples/node/offline-demo.mjs from the tarball), the browser flow against the SDK's browser bundle, and a server-side example that shows verifySignInResponse plus an HTTP resolver, which fails closed with endpoint_unavailable when no public endpoint exists.

    The demo does not check chain state, because a fixture resolver replaces canonical resolve, so it cannot prove that a DID exists on chain. Its test key must never be used for a real identity.

  8. 8Verify responses in eleven ordered steps, fail-closed

    Offline-testable today

    Verify in order and reject on the first failure: parse the response; compare nonce and domain with your stored request; rebuild the canonical message and compare byte-for-byte; check expiry; consume the nonce once; check the chain id; resolve the DID on the server; confirm the verification method is in authentication; allow only the Dilithium-5 proof type; verify the signature; then create the session.

    Resolve must be server-side, because the agent API returns no CORS headers and a browser cannot call it. There is no built-in rate limit and no Retry-After, so cache with a 5 to 15 second TTL, use single-flight, back off on failure, and do not batch. Consume the nonce before verifying the signature.

    The bridge proof type AididP384BridgeSignature2025 is demo-only; reject it unless you explicitly opt in, and label it non-quantum-safe if you do.

  9. 9Go-live checklist: do not ship before every line is true

    Offline-testable today

    Before production, all of these must hold:

    1. The chain-side resolve endpoint is authorised. It is not today (didLoginProduction), so every real verification fails closed with endpoint_unavailable. 2. The signing user actually holds a DID. Most users cannot create one today, because there is no end-user path to obtain MSG (userCreatesDid). 3. Your nonce store is shared across instances and consumes a nonce atomically; a per-instance memory store will accept replays. 4. Resolve runs server-side with a 5 to 15 second TTL cache and single-flight; the API has no CORS and no built-in rate limit. 5. You handle every stable error from login-error (did_not_found, did_deactivated, nonce_replayed, endpoint_unavailable, and the rest) and never fall back to an anonymous session. 6. Registering in the directory is optional and is only a declaration that you control your origin. It is not a security endorsement and it is not required to log in.

    Until items 1 and 2 are true, ship nothing that claims to be a working sign-in.

  10. 10Production login is blocked (chain-side resolve not authorised)

    Not available today

    You can finish and self-test the integration, but you cannot go live: GET {agent_api}/agent/v1/aidid/resolve is not authorised on the public network yet, so an RP cannot resolve any DID in production. This is the unavailable capability didLoginProduction in status.json.

    Do not describe AIDID login as production-ready, and do not ship a flow that silently falls back to an anonymous session when resolution fails. The correct behaviour is fail-closed with endpoint_unavailable.

    Why this is blocked today

    • didLoginProduction The chain-side DID resolve endpoint (`GET {agent_api}/agent/v1/aidid/resolve`) is not authorized yet, so an RP cannot resolve any DID in production.

    Reason taken from status.json#capabilities — not written by hand for this page.

  11. 11List your site in the registry (proof of origin, nothing more)

    Works today

    Once your integration is written you can publish /.well-known/aidid-rp.txt on your origin and open a pull request against the public registry repository. A GitHub Action fetches your file and compares it with the registered challenge; a pull request that changes only the registry file and matches is merged automatically — including a pull request opened from your own fork, with no invitation or collaborator access needed. Registration is free and explicitly transitional until the on-chain registry exists.

    There is no rebuild to wait for: the directory page reads a first-party cache that is refreshed on demand (a cache older than ten minutes triggers a refresh, rate-limited to once every ten minutes), and falls back to the snapshot built into the deployment when that read fails — so a merged entry normally shows up on your next visit or two.

    Being listed proves only that you control the origin and declare the integration. It is not a security or reputation endorsement by AIDID.