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:

ValueExampleDescription
MASTERKEY2_URLhttps://auth.example.comMasterKey2 service URL
MK2_API_KEYbvsk_...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:

  1. Login — drop in <masterkey2-authenticate>, handle the success event to create a session
  2. Registration — drop in <masterkey2-register>, backed by a server-side token endpoint
  3. Cross-domain setup — serve a .well-known/webauthn file (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:

  1. Validate the user’s session (they must be logged in)
  2. Fetch a user token from MasterKey2
  3. 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:

  1. The component calls your /api/mk2-user-token endpoint (with cookies, so you can validate the session)
  2. Your endpoint returns a 30-second user token
  3. 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.com and auth.myapp.com with 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+.


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

  1. Enable proof forwarding for your tenant (ask your provider, or use the API: POST /api/v1/proof-forwarding with { "enabled": true })
  2. After a user registers a passkey, fetch their credential public keys: GET /api/v1/users/{external_id}/credentials
  3. Store the credential IDs and public keys (JWK format) in your database
  4. On each verify-auth call, the response includes a proof object with the raw authenticator data, client data JSON, signature, and credential ID
  5. 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

ProblemFix
No passkey prompt appearsEnsure the page is served over HTTPS (or localhost). Check browser console for errors.
”Authentication Unavailable” amber panelThe tenant’s RP ID doesn’t match your website’s domain. Contact your MasterKey2 provider.
QR code scanned but nothing happensEnsure the phone can reach the MasterKey2 server over HTTPS.
Cross-device QR fails on mobileServe /.well-known/webauthn from your domain (see Step 4).
user_cancelled errorsNormal — the user dismissed the browser prompt. Handle silently.
credential_not_foundThe user hasn’t registered a passkey yet. Direct them to the registration page.
user_disabledThe user’s account was suspended. Contact your MasterKey2 provider or use the enable API.

Further reading