Authentication Playground

Test every Hesskey authentication method — QR, BLE, and trustless light client

Web3 Challenge-Response (CAIP-122)

The simplest form of decentralized authentication. Proves you control a DID without sharing any personal data.

How it works
1. Website generates a random nonce (challenge) and registers it with the Hesskey chain node
2. A QR code is displayed — you scan it with the Hesskey app on your phone
3. The app builds a CAIP-122 sign-in message and signs it with your ed25519 private key
4. The signed response is sent to the verifier, which checks the signature against your on-chain DID — if it matches, you're in
What's shared: Your DID (public identifier) + a signature proving you control it. No personal data, no credentials, no claims.
Use cases: Community forums, DAO voting, crypto exchanges, developer portals, Web3 dApps, token-gated access, decentralized social media login
How to implement

Drop the SDK into any HTML page. The SDK handles session creation, QR rendering, and polling:

<button id="signin">Sign in with Hesskey</button>
<div id="qr"></div>

<script type="module">
  import { HessKeyAuth }
    from 'https://your-verifier.com/shared/hesskey-sdk.js';

  const auth = new HessKeyAuth({
    verifierApiUrl: 'https://your-verifier.com',
    domain: window.location.hostname,
    authType: 'web3',
  });

  document.getElementById('signin').onclick = async () => {
    const session = await auth.createSession();
    auth.renderQR(document.getElementById('qr'), { size: 256 });

    auth.onVerified((result) => {
      console.log('Signed in as', result.result.did);
    });
  };
</script>

Server-side prerequisites:

  • A running Hesskey verifier (hesskey-web/server.py)
  • A Hesskey chain node for DID resolution and signature verification
  • No database required — sessions are stored in-memory with a 5-minute TTL

SIOP v2 + OID4VP

OpenID-based authentication with Verifiable Credentials. The website requests specific identity data and you choose what to share.

How it works
1. Website creates an OpenID authorization request with a presentation definition describing which credential fields it needs (e.g. name, nationality)
2. You scan the QR code — your app matches stored credentials against the request and shows a consent screen listing exactly what will be shared
3. You approve → the app builds a signed id_token (JWT) + vp_token (Verifiable Presentation wrapping your W3C credentials) and POSTs them to the verifier
4. The verifier checks the VC issuer chain, the ed25519 signature against the on-chain DID, and the session nonce — your identity is confirmed
What's shared: Your DID + only the credential fields you approve (name, nationality, etc.) in a signed Verifiable Presentation. The VP is audience-bound and cannot be replayed to another site.
Standards: SIOP v2 + OID4VP + W3C VC 2.0
Use cases: Banking KYC, government e-services, healthcare portals, insurance onboarding, enterprise SSO, university enrollment, e-commerce identity verification, travel check-in
How to implement

Declare exactly which credential fields you need with a W3C Presentation Definition:

<script type="module">
  import { HessKeyAuth }
    from 'https://your-verifier.com/shared/hesskey-sdk.js';

  const auth = new HessKeyAuth({
    verifierApiUrl: 'https://your-verifier.com',
    domain: window.location.hostname,
    authType: 'siop_vp',
    presentationDefinition: {
      id: 'login',
      inputDescriptors: [{
        id: 'identity',
        name: 'Identity Credential',
        constraints: {
          fields: [
            { path: ['$.credentialSubject.givenName'] },
            { path: ['$.credentialSubject.familyName'] },
            { path: ['$.credentialSubject.nationality'] },
          ],
        },
      }],
    },
  });

  document.getElementById('signin').onclick = async () => {
    await auth.createSession();
    auth.renderQR(document.getElementById('qr'), { size: 256 });

    auth.onVerified((result) => {
      const vp = result.result.vp;
      const cred = vp.verifiableCredential[0];
      console.log('Hello', cred.credentialSubject.givenName);
    });
  };
</script>

What the verifier checks automatically:

  • VC issuer is a trusted CSCA (passive authentication chain)
  • VC signature (eddsa-jcs-2022) is valid against the holder's on-chain DID
  • id_token (SIOP JWT) nonce matches the session
  • VP is bound to your client_id — cannot be replayed elsewhere

BBS+ ZKP + ECDH Encrypted Claims

Maximum privacy authentication. Prove facts about yourself without revealing the underlying data, and encrypt shared claims so only the verifier can read them.

ZKP Predicates requested:

AGE_OVER: 18 IS_EU_CITIZEN

Fields to encrypt (ECDH pairwise):

given_name family_name nationality
How it works
1. Website requests zero-knowledge predicates (e.g. "prove age ≥ 18") and specifies which identity fields to encrypt for the verifier
2. Your app creates BBS+ selective disclosure proofs — a mathematical proof that a predicate is true without revealing the underlying value (e.g. proves "over 18" without sharing your birthdate)
3. Selected claims are ECDH encrypted (X25519 + ChaCha20-Poly1305) with a pairwise key derived between you and the verifier — no encryption key is ever transmitted
4. The verifier receives ZKP proofs (verifiable without seeing the raw data) + encrypted claims (only the verifier's X25519 private key can decrypt)
What's shared: ZKP proofs (no raw data exposed) + encrypted claims (only the verifier can decrypt). Revoking claim access on-chain makes all stored ciphertext permanently unreadable (GDPR Art. 17 cryptographic erasure).
Crypto: BBS+ Signatures (zkryptium FFI) + ECDH-ES (X25519) + HKDF-SHA256 + ChaCha20-Poly1305
Use cases: Age-restricted content, nationality verification for cross-border services, employment eligibility checks, anonymous credential verification, privacy-preserving loyalty programs, GDPR-compliant data sharing with revocable access
How to implement

Request predicates and encrypted fields. The verifier decrypts with its X25519 private key server-side:

<script type="module">
  import { HessKeyAuth }
    from 'https://your-verifier.com/shared/hesskey-sdk.js';

  const auth = new HessKeyAuth({
    verifierApiUrl: 'https://your-verifier.com',
    domain: window.location.hostname,
    authType: 'bbs_ecdh',
    predicates: [
      {
        fieldPath: '$.credentialSubject.dateOfBirth',
        predicate: 'AGE_OVER',
        value: 18,
        verifierDid: 'did:hesskey:your-verifier',
      },
    ],
    encryptedFields: ['given_name', 'family_name'],
    presentationDefinition: {
      id: 'kyc',
      inputDescriptors: [{
        id: 'identity-bbs',
        constraints: {
          fields: [
            { path: ['$.credentialSubject.dateOfBirth'] },
            { path: ['$.credentialSubject.givenName'] },
            { path: ['$.credentialSubject.familyName'] },
          ],
        },
      }],
    },
  });

  document.getElementById('signin').onclick = async () => {
    await auth.createSession();
    auth.renderQR(document.getElementById('qr'), { size: 256 });

    auth.onVerified((result) => {
      // ZKP proofs verified on-chain by the verifier
      console.log('Predicates:', result.result.predicates);
      // Encrypted claims decrypted server-side
      console.log('Claims:', result.result.encryptedClaims);
    });
  };
</script>

Server-side prerequisites:

  • Register your verifier DID on-chain with an X25519 key-agreement key
  • Keep the X25519 private key on the server — never ship it to the browser
  • Cryptographic erasure: call revoke_claim_access on-chain to make stored ciphertext for that user permanently unreadable

BLE Direct Login

Sign in without scanning a QR code. Your browser connects directly to your phone over Bluetooth Low Energy and delivers the auth request — no QR scan, no relay server.

BLE transport: Detecting…
Web Bluetooth: Detecting…
Web Bluetooth requires a secure context (HTTPS). Make sure you are accessing this page over HTTPS and using a Bluetooth-capable browser (Chrome, Edge).
How it works
Two modes — the SDK picks the best one automatically:
Mode A: Direct Web Bluetooth (no extension needed)
Uses the browser's native Web Bluetooth API. An OS Bluetooth picker appears — you select your phone, the auth request is written over GATT, and the phone POSTs the result back to the verifier. Works on any HTTPS page in Chrome/Edge.
Mode B: Extension bridge (paired device)
If the Hesskey Chrome extension is installed and paired with your phone, the auth request is forwarded silently over the extension's BLE channel — no picker, no user interaction beyond approval on the phone.
1. Website creates a SIOP session and generates an openid:// auth URI
2. The auth URI is delivered to your phone over Bluetooth LE GATT (direct or via extension)
3. Your phone shows a consent screen — you approve which credentials to share
4. The phone builds and signs the id_token + vp_token and POSTs them to the verifier — the browser picks up the result via polling
What's shared: Same as SIOP+OID4VP — only the credential fields you approve. The difference is the transport: BLE replaces QR scanning.
Use cases: One-time logins without installing an extension, daily SSO at the office with a paired extension, kiosks, high-frequency logins where scanning a QR every time is impractical
How to implement

For direct Web Bluetooth, call navigator.bluetooth.requestDevice() in the click handler (before any await) to preserve the user gesture, then write the auth URI over GATT:

<script type="module">
  import { HessKeyAuth, isExtensionPaired }
    from 'https://your-verifier.com/shared/hesskey-sdk.js';

  const BLE_SERVICE = 'ae55be01-c0de-e5c1-b1e1-000000000001';
  const BLE_AUTH    = 'ae55be01-c0de-e5c1-b1e1-000000000010';

  document.getElementById('signin').onclick = async () => {
    // 1. Request device BEFORE any await (user gesture)
    let device = null;
    if ('bluetooth' in navigator && !isExtensionPaired()) {
      device = await navigator.bluetooth.requestDevice({
        filters: [{ services: [BLE_SERVICE] }],
      });
    }

    // 2. Create session (SDK starts polling automatically)
    const auth = new HessKeyAuth({
      verifierApiUrl: 'https://your-verifier.com',
      domain: window.location.hostname,
      authType: 'siop_vp',
      presentationDefinition: { /* ... */ },
    });
    const session = await auth.createSession();

    // 3. Write auth URI over BLE GATT
    if (device) {
      const server = await device.gatt.connect();
      const svc  = await server.getPrimaryService(BLE_SERVICE);
      const char = await svc.getCharacteristic(BLE_AUTH);
      const bytes = new TextEncoder().encode(session.qrPayload);
      await char.writeValueWithResponse(bytes);
      server.disconnect();
    }
    // If extension is paired, it delivers automatically.

    // 4. Wait for result (polling picks up the phone's VP POST)
    auth.onVerified((result) => {
      console.log('Signed in as', result.result.did);
    });
  };
</script>

Requirements:

  • Direct BLE: Page must be served over HTTPS (Web Bluetooth requires a secure context). Works in Chrome and Edge.
  • Extension bridge: The Hesskey Chrome extension must be installed and paired with the phone. No HTTPS requirement for the extension path.
  • No server-side changes — the verifier sees a standard SIOP session regardless of transport

Light Client Trustless Verification

The Hesskey browser extension embeds a smoldot WASM light client that syncs directly with the Hesskey P2P network. DID resolution and signature verification happen locally in the browser — no trusted RPC node in between.

Extension status: Detecting…
Light client: Detecting…
Best block:
Install the Hesskey Chrome extension to enable trustless light client verification. Without it, the flow falls back to server-verified results.
How it works
1. The extension starts a smoldot WASM light client in an offscreen document and joins the Hesskey P2P network directly — only the chain spec bundled with the extension is trusted
2. When a user signs in, the website asks the extension (window.__hesskey.chain.resolveDid) to resolve the user's DID and fetch their on-chain authentication key — verified locally against the finalized state root
3. You sign the challenge on your phone as usual (QR or BLE) — the VP/id_token is returned to the verifier
4. The browser locally verifies the ed25519 signature against the key fetched from the light client — the verifier server's answer becomes a cross-check, not the sole source of truth
What's shared: Same payload as Web3 / SIOP. The difference is trust: the browser proves the on-chain state itself rather than relying on a remote RPC node.
Tech: smoldot WASM light client, chainHead_v1_follow, offscreen document (MV3), state-proof verification, bundled chain spec
Use cases: Hostile networks where no RPC node is trustworthy, regulated industries that need reproducible verification, auditing verifier behaviour, offline-first desktops, blocking-friendly environments where only P2P traffic is allowed
How to implement

The SDK auto-detects the extension's light client. If synced, it confirms the user's DID against on-chain state as a trustless cross-check. With a paired extension you can also skip the QR entirely via trySilentSignIn:

<script type="module">
  import { HessKeyAuth, trySilentSignIn, isLightClientAvailable }
    from 'https://your-verifier.com/shared/hesskey-sdk.js';

  document.getElementById('signin').onclick = async () => {
    // 1. Fast path: paired extension + Session Delegation.
    //    Signs a Session Auth Token locally — no QR, no phone.
    const silent = await trySilentSignIn({
      verifierApiUrl: 'https://your-verifier.com',
      sessionInit: {
        domain: window.location.hostname,
        authType: 'web3',
      },
    });
    if (silent?.status === 'verified') {
      console.log('Silent sign-in:', silent.raw.result.did);
      return;
    }

    // 2. Fallback: QR flow + trustless cross-check.
    const auth = new HessKeyAuth({
      verifierApiUrl: 'https://your-verifier.com',
      domain: window.location.hostname,
      authType: 'web3',
    });
    await auth.createSession();
    auth.renderQR(document.getElementById('qr'), { size: 256 });

    auth.onVerified(async (result) => {
      // Cross-check the DID against the light client's chain state
      let trustless = false;
      if (isLightClientAvailable() && window.__hesskey?.chain) {
        const resolved = await window.__hesskey.chain.resolveDid(
          result.result.did
        );
        trustless = !!resolved;
      }
      console.log('Signed in, trustless =', trustless);
    });
  };
</script>

Requirements:

  • The Hesskey Chrome extension (v0.5.2+) must be installed for the light client and the silent sign-in path
  • Verifier must expose POST /api/sessions/delegated for trySilentSignIn
  • The chain spec bundled with the extension determines which chain is the root of trust
  • No server-side changes needed for the trustless cross-check — it runs entirely in the browser

<hesskey-login> Web Component

A drop-in custom element that gives any Hesskey page a complete sign-in / sign-out UX — extension-aware, smart-routed, session-persistent — in one HTML tag. Matches the Hesskey brand: purple border, orange icon & label, translucent green background.

Demo Header
Status: signed out
How it works
1. The page drops in <hesskey-login>. The element renders its own Sign In button, popover, and Sign Out button inside a shadow DOM so your page CSS can't accidentally style it.
2. On boot it tries a cached session (localStorage, 24 h TTL) first, then falls back to a silent extension sign-in. If neither works, the Sign In button stays visible.
3. Click Sign In → pick transport (Bluetooth or QR). If the extension is already paired via the chosen transport, it logs you in silently. If paired via the other transport, it asks to unpair and switch (custom Yes / No modal).
4. If not paired, step 2 offers Use extension to pair (opens the extension's own pair window) or One-time sign-in (direct Web Bluetooth / QR, no extension).
5. On success, the widget dispatches authenticated with the DID. On sign out or extension disable, it dispatches logout / force-disconnect. Your page listens and reacts.
Why a web component: one tag, attribute-driven, style-isolated, no build step, no imports besides a single <script type="module">. Works in any page — admin portal, Foundation portal, plain static HTML.
How to implement

Add the module once and drop the tag anywhere you want a sign-in button:

<!-- Load the component once -->
<script type="module" src="/shared/hesskey-login.js"></script>

<!-- Drop the tag where you want the button to live -->
<hesskey-login
  verifier-api="https://hesskey.org"
  site-name="Foundation Portal"
  session-key="foundation_session"></hesskey-login>

<script>
  const login = document.querySelector('hesskey-login');

  login.addEventListener('authenticated', (ev) => {
    const did = ev.detail.did;  // 32-char hex, no prefix
    // render your signed-in UI
  });

  login.addEventListener('logout', () => {
    // clear your signed-in UI
  });

  login.addEventListener('force-disconnect', (ev) => {
    // user toggled pairing off in the extension — same as logout
    // ev.detail.reason tells you who initiated it
  });
</script>

Attributes:

  • verifier-api — base URL of the Hesskey verifier (defaults to location.origin)
  • site-name — shown in the popover header (e.g. "Foundation Portal")
  • session-key — localStorage key for the cached session (use a unique one per page to avoid collisions)

Events:

  • authenticateddetail: { did }. Fires on fresh sign-in or cached-session restore.
  • logout — fired after the user signs out via the Sign Out button.
  • force-disconnectdetail: { reason }. Fired when the extension broadcasts a teardown (e.g. user toggled pairing off in the extension popup).

Requirements:

  • Verifier serves /shared/hesskey-login.js and /shared/hesskey-sdk.js (both shipped by default)
  • For the silent-sign-in path, the user must have the Hesskey Chrome extension installed and paired
  • No build step; no framework; works in any page that can load ES modules

Styling:

The Sign In / Sign Out buttons use the Hesskey brand palette — purple border (#7B1FA2), orange text & icon (#FF9800), and a translucent green background (rgba(76,175,80,0.18)). All styling lives inside the shadow DOM so page CSS can't override it.

Protected Application Pattern (SIOP v2)

The classic relying-party integration: a login gate, a Verifiable-Presentation sign-in, then a dashboard that greets the verified user. This exact pattern now powers the real Hesskey web app.

How it works
1. The app sends an OpenID authorization request with a presentation definition — here it asks for your given name and family name so the dashboard can greet you
2. Your Hesskey app matches available credentials against the request and shows a consent screen
3. You approve → the app builds a signed id_token (JWT) + vp_token (a Verifiable Presentation containing only the requested fields)
4. The app verifies the VP against the issuer's chain of trust and your DID, then unlocks the dashboard
What's shared: your DID + the two fields the app explicitly asked for. Nothing else from your identity wallet is sent.
Standards: SIOP v2 + OID4VP + W3C VC 2.0
Add this to your website

The exact pattern this demo uses — a login gate plus a dashboard:

<button id="signin">Sign in with Hesskey</button>
<div id="qr"></div>

<script type="module">
  import { HessKeyAuth }
    from 'https://your-verifier.com/shared/hesskey-sdk.js';

  const auth = new HessKeyAuth({
    verifierApiUrl: 'https://your-verifier.com',
    domain: window.location.hostname,
    authType: 'siop_vp',
    presentationDefinition: {
      id: 'login',
      inputDescriptors: [{
        id: 'identity-credential',
        name: 'Hesskey Identity',
        purpose: 'Sign in to your dashboard',
        constraints: {
          fields: [
            { path: ['$.credentialSubject.givenName'] },
            { path: ['$.credentialSubject.familyName'] },
          ],
        },
      }],
    },
  });

  document.getElementById('signin').onclick = async () => {
    await auth.createSession();
    auth.renderQR(document.getElementById('qr'), { size: 256 });

    auth.onVerified((result) => {
      const subject = result.credentials[0].credentialSubject;
      console.log('Hello', subject.givenName, subject.familyName);
      // ...show your dashboard
    });
  };
</script>

Server-side prerequisites:

  • A running Hesskey verifier (Python FastAPI — see hesskey-web)
  • Your verifier DID registered on-chain (free via the devnet faucet)
  • The verifier validates VC issuer trust + signature for you — you only consume the onVerified result

Build on the Hesskey backbone

Hesskey is an identity + object backbone your site builds on. You read public state over a plain JSON API and write by composing an intent the user's phone approves and signs — your app never touches keys. The Hesskey web app at /web-app/ is the reference consumer: anything it does, your site can too.

Read · GET /api/v1/*
node-trusted JSON, or verify yourself against the chain. CORS-enabled. Base: foundation.hesskey.org
Write · POST /api/action
phone-approved, phone-signed intents (tiered; tier-3 needs an eID face match). You never see a key.

Read endpoints

EndpointReturns
GET /api/v1/orgs?did=<did>spaces the DID owns/manages (id, name, role)
GET /api/v1/orgs/{id}/eventsevents under an org (title, venue, start, capacity)
GET /api/v1/orgs/{id}/memberson-chain Owners + Managers
GET /api/v1/orgs/{id}/treasurykeyless org wallet (address, balance)
GET /api/v1/events/{id}/ticketstickets minted under an event
GET /api/v1/holdings?key=<hex>tickets held under an owner key

Quickstart — sign in, read, write

import { HessKeyAuth }
  from 'https://foundation.hesskey.org/shared/hesskey-sdk.js';

const auth = new HessKeyAuth({ verifierApiUrl: 'https://foundation.hesskey.org',
  domain: location.hostname, authType: 'siop_vp', trust: 'local' });
await auth.createSession();                       // silent or QR

auth.onVerified(async ({ result }) => {
  const did = result.did.replace(/^did:hesskey:/, '');

  // READ — the user's spaces, straight from the backbone
  const { orgs } = await (await fetch(
    `https://foundation.hesskey.org/api/v1/orgs?did=${did}`)).json();

  // WRITE — grant Scan to door staff (Tier 3: the phone face-matches + signs)
  const sat = await window.__hesskey.session.signChallenge(
    { origin: location.origin, challenge });
  await fetch('https://foundation.hesskey.org/api/action', { method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ did,
      intent: { call_index: 13, args: { object: orgs[0].id,
                principal: { Person: doorStaffDid }, cap_bits: 0x10 } },
      auth: { sdc: sat.sdc, sat: sat.sat, origin: location.origin } }) });
});
Full reference (auth, tiers, call indices, capability bits): the Hesskey Backbone — Developer API (PDF). The browser/SDK is always a viewport with a session, never a wallet — identity keys live on the user's phone and never leave it.