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.
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 { session_token } = await res.json();
// Pass session_token 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, external_id, display_name? }
//
// Create a session on YOUR server using the external_id,
// then redirect to a protected page.
await fetch('/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ externalId: user.external_id }),
});
window.location.href = '/dashboard';
});
</script>
That’s it for login. The component handles passkey prompts, cross-device QR codes, and browser compatibility automatically.
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({
external_id: session.userId, // Your app's user identifier
display_name: 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.user_token });
}
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.
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.user.external_id tells you who authenticated. For additional security, you can verify the authentication 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({ token: sessionTokenFromClient }),
});
const { verified, user } = await res.json();
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.external_id }),
});
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: { "api_key": "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