Getting Started with MasterKey2
Add passkey authentication to your website using MasterKey2. Users sign in with their fingerprint, face, or security key — no passwords needed.
Prerequisites
You should have received two values from your MasterKey2 provider:
| Value | Example | Description |
|---|---|---|
MASTERKEY2_URL | https://auth.example.com | MasterKey2 service URL |
MK2_API_KEY | bvsk_... | Your tenant API key |
Store both as server-side environment variables. Never expose MK2_API_KEY to the browser.
Your website must be served over HTTPS (WebAuthn does not work on plain HTTP, except localhost during development).
Overview
MasterKey2 provides a JavaScript SDK with web components that handle the full WebAuthn flow. Your integration has three parts:
- Login — drop in
<masterkey2-authenticate>, handle thesuccessevent to create a session - Registration — drop in
<masterkey2-register>, backed by a server-side token endpoint - Cross-domain setup — serve a
.well-known/webauthnfile (only if MasterKey2 is on a different domain)
The SDK auto-detects the user’s browser and renders the right UI: a passkey button on Chromium/mobile, a QR code on Firefox, or a fallback message on insecure connections. You don’t need to write any browser-detection code.
Both components require api-base-url and a token attribute. If either is missing, the component renders an amber “Authentication Unavailable” error panel and emits a configuration_error event.
Step 1: Load the SDK
Add the SDK script to any page that needs authentication. It auto-registers the web components.
<script type="module" src="https://auth.example.com/sdk/masterkey2.js"></script>
Replace https://auth.example.com with your MASTERKEY2_URL.
Step 2: Add passkey login
2a. Fetch a session token (server-side)
A session token tells MasterKey2 which tenant this login belongs to. Fetch one server-side when rendering your login page:
// Server-side (e.g. in your login page route handler)
const res = await fetch(`${process.env.MASTERKEY2_URL}/api/v1/session-token`, {
method: 'POST',
headers: { 'X-API-KEY': process.env.MK2_API_KEY },
});
const { sessionToken } = await res.json();
// Pass sessionToken to your HTML template
The session token is safe to include in HTML — it only identifies your tenant, not any user. It’s valid for 24 hours.
2b. Render the authenticate component
<masterkey2-authenticate
api-base-url="https://auth.example.com"
token="SESSION_TOKEN_FROM_SERVER">
</masterkey2-authenticate>
<script>
document.querySelector('masterkey2-authenticate')
.addEventListener('success', async (e) => {
const { user } = e.detail;
// user = { id, externalId, displayName? }
//
// Create a session on YOUR server using the externalId,
// then redirect to a protected page.
await fetch('/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ externalId: user.externalId }),
});
window.location.href = '/dashboard';
});
</script>
That’s it for login. The component handles passkey prompts, cross-device QR codes, and browser compatibility automatically.
Required attributes: api-base-url and token are both required. The component shows a configuration error if either is missing.
Optional: Set mode to override auto-detection: "passkey", "qr", or "qr-desktop" (QR on desktop, auto on mobile). If mode="passkey" is set on a browser that doesn’t support WebAuthn, the component shows an “unsupported browser” message instead of failing silently.
Handling errors
<script>
const login = document.querySelector('masterkey2-authenticate');
login.addEventListener('error', (e) => {
const { error, code } = e.detail;
switch (code) {
case 'user_cancelled':
break; // User dismissed the prompt -- do nothing
case 'credential_not_found':
showMessage('No passkey found. Please register one first.');
break;
case 'user_disabled':
showMessage('Your account has been suspended.');
break;
case 'server_unreachable':
showMessage('Cannot reach authentication server.');
break;
default:
showMessage('Authentication failed. Please try again.');
}
});
</script>
Step 3: Add passkey registration
Registration requires identifying the user, so MasterKey2 needs a short-lived user token scoped to a specific person. The recommended approach is just-in-time: the token is fetched the moment the user clicks “Register”, not at page load.
3a. Create a token endpoint (server-side)
Expose a same-origin endpoint that your client can call. It should:
- Validate the user’s session (they must be logged in)
- Fetch a user token from MasterKey2
- Return it to the browser
// Server-side: e.g. GET /api/mk2-user-token
export async function handler(req, res) {
const session = validateSession(req); // Your session validation
const response = await fetch(`${process.env.MASTERKEY2_URL}/api/v1/user-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': process.env.MK2_API_KEY,
},
body: JSON.stringify({
externalId: session.userId, // Your app's user identifier
displayName: session.name, // Optional display name
ttl: 30, // Token lives 30 seconds (fetched just before use)
}),
});
const data = await response.json();
return Response.json({ userToken: data.userToken });
}
3b. Render the register component
Place this on a page where the user is already authenticated (e.g. a settings or profile page):
<masterkey2-register
api-base-url="https://auth.example.com"
assertion-url="/api/mk2-user-token"
name="My Passkey">
</masterkey2-register>
<script>
document.querySelector('masterkey2-register')
.addEventListener('success', () => {
showMessage('Passkey registered successfully!');
});
</script>
When the user clicks the button:
- The component calls your
/api/mk2-user-tokenendpoint (with cookies, so you can validate the session) - Your endpoint returns a 30-second user token
- The component uses the token to run the WebAuthn registration ceremony
The API key never leaves your server.
Required attributes: api-base-url is required. Either token or assertion-url must be provided — the component shows a configuration error if neither is set.
Optional: Set mode to override auto-detection: "passkey", "qr", or "qr-desktop". If mode="passkey" is set on a browser that doesn’t support WebAuthn, the component shows an “unsupported browser” message.
Step 4: Cross-domain setup (if needed)
Skip this step if your website and MasterKey2 share the same domain (e.g.
myapp.comandauth.myapp.comwith a matching RP ID, or the same host during development).
When MasterKey2 runs on a completely different domain (e.g. your site is myapp.com and MasterKey2 is auth.provider.com), cross-device QR flows need your site to declare MasterKey2 as an authorized origin.
Serve this from your website:
GET https://myapp.com/.well-known/webauthn
Content-Type: application/json
{ "origins": ["https://auth.provider.com"] }
Framework examples
Express:
app.get('/.well-known/webauthn', (req, res) => {
res.json({ origins: [process.env.MASTERKEY2_URL] });
});
Next.js (App Router):
// app/.well-known/webauthn/route.ts
export function GET() {
return Response.json({ origins: [process.env.MASTERKEY2_URL] });
}
Static file (nginx, S3, etc.):
{ "origins": ["https://auth.provider.com"] }
Why this is needed
Passkeys are bound to your domain (the RP ID). When a user scans a QR code, their phone opens a page on MasterKey2’s domain. The phone’s browser checks https://your-domain/.well-known/webauthn to verify MasterKey2 is allowed to use your domain as an RP ID. This is the W3C Related Origin Requests spec, supported by Chrome 128+ and Safari 18+.
Step 5: Verify authentication (server-side, recommended)
After the success event fires on the client, the e.detail.challengeId identifies the completed authentication. For additional security, you can verify it server-to-server:
const res = await fetch(`${process.env.MASTERKEY2_URL}/api/v1/verify-auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': process.env.MK2_API_KEY,
},
body: JSON.stringify({ challengeId: challengeIdFromClient }),
});
const { verified, user } = await res.json();
Step 6: PRF extension (optional — client-side encryption)
The PRF extension lets you derive a deterministic encryption key from a passkey. This enables features like client-side encrypted vaults where only the passkey holder can decrypt data.
Authentication with PRF
Add the prf attribute to the authenticate component. In "event" mode, the PRF output is included in the success event:
<masterkey2-authenticate
api-base-url="https://auth.example.com"
token="SESSION_TOKEN"
prf="event">
</masterkey2-authenticate>
<script>
document.querySelector('masterkey2-authenticate')
.addEventListener('success', async (e) => {
const { user, prfOutput, capabilities } = e.detail;
if (prfOutput) {
// Derive an AES key from the PRF output using HKDF
const prfBytes = base64urlDecode(prfOutput);
const keyMaterial = await crypto.subtle.importKey('raw', prfBytes, 'HKDF', false, ['deriveKey']);
const aesKey = await crypto.subtle.deriveKey(
{ name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(0),
info: new TextEncoder().encode('my-app-v1') },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false, ['encrypt', 'decrypt']
);
// Use aesKey to encrypt/decrypt user data
}
// Continue with normal session creation
await fetch('/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ externalId: user.externalId }),
});
window.location.href = '/dashboard';
});
</script>
PRF delivery modes
| Mode | Attribute | Behavior |
|---|---|---|
| Event | prf="event" | PRF output in success event as e.detail.prfOutput (base64url) |
| Callback | prf="callback" prf-callback="/api/store-prf" | SDK POSTs { challengeId, prfOutput, capabilities } to your server endpoint |
| Both | prf="both" prf-callback="/api/store-prf" | Both event and callback |
Registration with PRF detection
You can also enable PRF during registration to detect whether the user’s authenticator supports it:
<masterkey2-register
api-base-url="https://auth.example.com"
assertion-url="/api/mk2-user-token"
name="My Passkey"
prf="event">
</masterkey2-register>
<script>
document.querySelector('masterkey2-register')
.addEventListener('success', (e) => {
if (e.detail.capabilities?.prf) {
showMessage('Passkey created with encryption support!');
} else {
showMessage('Passkey created (encryption not supported by this authenticator).');
}
});
</script>
Important notes
- PRF requires authenticator support. Not all authenticators support PRF. Always check
capabilities.prfor the presence ofprfOutputbefore using it. Chrome 132+ with platform authenticators and Safari 18+ support PRF. Firefox does not yet support PRF. - The PRF output is deterministic for a given authenticator + salt. The same passkey always produces the same output, making it suitable for key derivation.
- MasterKey2 never sees the raw PRF output. During registration, only a hash commitment is sent. During authentication, the PRF output is available via
verify_authonly if your backend explicitly requests it.
See the SDK Reference PRF section for the full API reference.
Complete example
Here’s a minimal login page putting it all together:
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<script type="module" src="https://auth.example.com/sdk/masterkey2.js"></script>
</head>
<body>
<h1>Sign In</h1>
<masterkey2-authenticate
api-base-url="https://auth.example.com"
token="SESSION_TOKEN_FROM_SERVER">
</masterkey2-authenticate>
<p>Don't have a passkey? <a href="/register">Create one</a></p>
<div id="error" style="color: red;"></div>
<script>
const el = document.querySelector('masterkey2-authenticate');
el.addEventListener('success', async (e) => {
await fetch('/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ externalId: e.detail.user.externalId }),
});
window.location.href = '/dashboard';
});
el.addEventListener('error', (e) => {
if (e.detail.code === 'user_cancelled') return;
document.getElementById('error').textContent = e.detail.error.message || 'Login failed';
});
</script>
</body>
</html>
And a minimal registration page:
<!DOCTYPE html>
<html>
<head>
<title>Add Passkey</title>
<script type="module" src="https://auth.example.com/sdk/masterkey2.js"></script>
</head>
<body>
<h1>Add a Passkey</h1>
<p>Register a passkey to sign in with your fingerprint or face.</p>
<masterkey2-register
api-base-url="https://auth.example.com"
assertion-url="/api/mk2-user-token"
name="My Passkey">
</masterkey2-register>
<div id="status"></div>
<script>
const el = document.querySelector('masterkey2-register');
el.addEventListener('success', () => {
document.getElementById('status').textContent = 'Passkey added!';
});
el.addEventListener('error', (e) => {
if (e.detail.code === 'user_cancelled') return;
document.getElementById('status').textContent = 'Registration failed: ' + e.detail.error.message;
});
</script>
</body>
</html>
Optional: User management API
MasterKey2 provides server-to-server endpoints for managing users. All require the X-API-KEY header.
Disable a user
Block authentication without deleting passkeys. Re-enable later to restore access.
POST /api/v1/users/{external_id}/disable
POST /api/v1/users/{external_id}/enable
Headers: X-API-KEY: <your-api-key>
Delete a user
Permanently remove a user and all their passkeys:
DELETE /api/v1/users/{external_id}
Headers: X-API-KEY: <your-api-key>
Rotate your API key
Generate a new API key. The old one stops working immediately.
POST /api/v1/rotate-key
Headers: X-API-KEY: <current-api-key>
Response: { "apiKey": "bvsk_...", "apiKeyPrefix": "..." }
Revoke a session token
Session tokens are not revoked by finish_authentication — they are long-lived tenant identifiers reusable across multiple operations. For explicit cleanup (e.g., on logout or after verify-auth):
DELETE /api/v1/session-token
Headers: Authorization: Bearer <session-token>
Response: { "revoked": true }
Optional: Proof forwarding (advanced security)
For high-security applications, MasterKey2 can forward raw WebAuthn authentication proofs alongside the verify-auth response. This lets your server independently verify that a real passkey ceremony occurred — even if the MasterKey2 server itself were compromised.
How it works
- Enable proof forwarding for your tenant (ask your provider, or use the API:
POST /api/v1/proof-forwardingwith{ "enabled": true }) - After a user registers a passkey, fetch their credential public keys:
GET /api/v1/users/{external_id}/credentials - Store the credential IDs and public keys (JWK format) in your database
- On each
verify-authcall, the response includes aproofobject with the raw authenticator data, client data JSON, signature, and credential ID - Verify the ECDSA P-256 signature:
signed_data = authenticator_data || SHA-256(client_data_json), then check the signature against the stored public key
If verification passes, a real authenticator produced the signature — no server can forge it because the authenticator’s private key never leaves the device.
See INTEGRATION.md for the full API reference and verification algorithm.
Troubleshooting
| Problem | Fix |
|---|---|
| No passkey prompt appears | Ensure the page is served over HTTPS (or localhost). Check browser console for errors. |
| ”Authentication Unavailable” amber panel | The tenant’s RP ID doesn’t match your website’s domain. Contact your MasterKey2 provider. |
| QR code scanned but nothing happens | Ensure the phone can reach the MasterKey2 server over HTTPS. |
| Cross-device QR fails on mobile | Serve /.well-known/webauthn from your domain (see Step 4). |
user_cancelled errors | Normal — the user dismissed the browser prompt. Handle silently. |
credential_not_found | The user hasn’t registered a passkey yet. Direct them to the registration page. |
user_disabled | The user’s account was suspended. Contact your MasterKey2 provider or use the enable API. |
Further reading
- SDK_DOCUMENTATION.md — full API reference (JS API, web components, events, error codes)
- EVENT_DRIVEN_GUIDE.md — event-driven integration with React/Vue/Svelte examples