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.

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:

  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({
      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:

  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.

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.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.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

ModeAttributeBehavior
Eventprf="event"PRF output in success event as e.detail.prfOutput (base64url)
Callbackprf="callback" prf-callback="/api/store-prf"SDK POSTs { challengeId, prfOutput, capabilities } to your server endpoint
Bothprf="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.prf or the presence of prfOutput before 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_auth only 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

  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