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
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
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:
Fields to encrypt (ECDH pairwise):
How it works
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_accesson-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.
How it works
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.
How it works
window.__hesskey.chain.resolveDid) to resolve the user's DID and fetch their on-chain authentication key — verified locally against the finalized state root
chainHead_v1_follow, offscreen document (MV3), state-proof verification, bundled chain spec
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/delegatedfortrySilentSignIn - 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 website a complete Hesskey
sign-in / sign-out UX in one HTML tag — three transports
(Bluetooth, QR, eID card), silent re-login, and auto-sign-out when the phone revokes,
with no integrator polling boilerplate. Style-isolated in a shadow DOM; one CSS variable
re-themes it. Full integration guide (security, SRI, CORS): docs/INTEGRATING.md.
How it works
<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 touch it. By default it refuses to run inside an iframe (clickjacking defence) — add allow-framed to opt in.
authenticated with the DID and the full verified result (detail.raw). On sign out or phone-revoke it dispatches logout / force-disconnect. Your page listens and reacts.
@hesskey/web-sdk on npm.
How to implement
Load the module once (pin the version) and drop the tag anywhere you want a sign-in button:
<!-- Load the component once (pin the ?v= version) -->
<script type="module" src="https://hesskey.org/shared/hesskey-login.js?v=3"></script>
<!-- Drop the tag where you want the button to live -->
<hesskey-login
verifier-api="https://hesskey.org"
site-name="My App"
session-key="myapp_session"></hesskey-login>
<script>
const login = document.querySelector('hesskey-login');
login.addEventListener('authenticated', (ev) => {
const did = ev.detail.did; // 32-char hex, no prefix
const raw = ev.detail.raw; // full verified result (null on cached restore)
// → exchange `did` for your own server-side session
});
login.addEventListener('logout', () => { /* clear your session */ });
login.addEventListener('force-disconnect', (ev) => {
// phone revoked this site, or the extension unpaired — treat as logout
});
</script>
Attributes:
verifier-api— base URL of the Hesskey verifier (defaults tolocation.origin)site-name— shown in the popover headersession-key— localStorage key for the cached session (unique per page)allow-framed— opt in to running inside an iframe (off by default; pair with a strictframe-ancestorsCSP)
Events:
authenticated—detail: { did, raw }.rawis the full verified result on fresh sign-in,nullon cached restore.logout— user signed out.force-disconnect—detail: { reason }. Phone-revoke or extension teardown.
Theming — one custom property re-themes the whole widget (it pierces the shadow DOM):
hesskey-login { --hk-login-accent: #7B1FA2; } /* default: Hesskey green */
Requirements: the verifier serves /shared/hesskey-login.js + hesskey-sdk.js + hesskey-card-flow.js (all shipped by default). BT/eID-card need the Hesskey extension; QR works without it. No build step, no framework.
Bundler / React? npm i @hesskey/web-sdk — the same headless core (incl. the eID card flow) with TypeScript types. Full guide + security / SRI / CORS notes: docs/INTEGRATING.md.
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
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
onVerifiedresult
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.
foundation.hesskey.orgRead endpoints
| Endpoint | Returns |
|---|---|
GET /api/v1/orgs?did=<did> | spaces the DID owns/manages (id, name, role) |
GET /api/v1/orgs/{id}/events | events under an org (title, venue, start, capacity) |
GET /api/v1/orgs/{id}/members | on-chain Owners + Managers |
GET /api/v1/orgs/{id}/treasury | keyless org wallet (address, balance) |
GET /api/v1/events/{id}/tickets | tickets 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 } }) });
});