Legacy password-field shim
You have an old app — ASP.NET Web Forms, classic Rails, a PHP monolith, a vendor product you can't fork — with a vanilla username/password form. You can't (or don't want to) rewrite the frontend. PasskeyBase ships a small JavaScript shim that hijacks the password field, runs the WebAuthn flow, and submits a passkey-derived token in the password field. The form posts as normal. Only the backend needs one small addition.
The trick
A passkey assertion is just data. We sign it server-side at PasskeyBase and hand back an opaque, prefixed, single-use bearer token. The shim writes that token into the password input. Your existing form-submit code, browser autofill, password managers, "remember me" checkbox, CSRF middleware — all of it works exactly as before, because as far as the form is concerned the user just typed something into the password field.
Frontend: one attribute, one script tag
<!-- Your existing legacy login form. Don't change it. -->
<form action="/login" method="POST">
<input name="username" autocomplete="username">
<input name="password" type="password" autocomplete="current-password"
data-passkey> <!-- only addition -->
<button type="submit">Sign in</button>
</form>
<!-- Drop-in shim. ~3 KB gzipped, no framework. -->
<script src="https://cdn.passkeybase.com/legacy-shim/v1.js"
data-client-id="pk_live_abc123"
data-domain="myapp.auth.ing"></script>What this does:
- Decorates the password input with a "Sign in with passkey" affordance.
- Enables WebAuthn conditional autofill — passkey suggestions show up in the browser's autofill dropdown right above the password field.
- On selection, runs
navigator.credentials.get(), sends the assertion to PasskeyBase, receives an opaque token, writes it into the password input, and submits the form.
The token format
# Token submitted in the password field
pkb_v1_eyJhbGciOiJFUzI1NiIs...
# Always prefixed with pkb_v1_ so your backend can detect them with one
# string check before doing anything else with the value.Tokens are always prefixed with pkb_v1_ so your backend can detect a passkey submission with a single startsWith check before doing anything else. The token is signed (ES256), single-use, expires in 60 seconds, and is bound to the username submitted in the same form.
Backend: one branch in your login handler
The only change to your application is the login endpoint. Detect the prefix, hit the verify endpoint, and on success issue a session the same way you would after a successful password check.
// Your backend's existing password-checking code, with one branch added.
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// ── New: detect a passkey token in the password field ──────────────
if (password.startsWith('pkb_v1_')) {
const result = await fetch(
'https://myapp.auth.ing/api/v1/verify-token',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PASSKEYBASE_MGMT_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: password, expectedUsername: username }),
}
).then(r => r.json());
if (result.valid) {
// Issue your existing session cookie. Done.
return startSession(res, result.userId);
}
return res.status(401).send('Invalid passkey');
}
// ── Existing: normal password check, untouched ─────────────────────
const user = await db.users.findOne({ username });
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).send('Invalid credentials');
}
return startSession(res, user.id);
});The same pattern works in any language — there's a one-page recipe in each backend SDK reference (Python, Go, Rust, Java, C#, PHP, Ruby, Elixir).
What this is good for
- Legacy app you can't rewrite. Vendor product, classic ASP.NET, Rails monolith from 2014, internal tool nobody owns.
- Migration on the side. Ship passkeys to power users without blocking the password login for everyone else. Same form, same flow.
- Admin / staff portals. Get phishing-resistant MFA into an internal tool that has 12 users and a Sinatra backend, without spinning up a whole identity provider migration.
What this is not good for
- Apps with no backend touch available at all. The login endpoint has to call the verify API. There's no way around this — a passkey assertion isn't a password and can't be validated with bcrypt. (See the reverse-proxy variant below if literally zero backend changes are possible.)
- Greenfield apps. If you're building from scratch, use theOAuth flow directly — it's cleaner.
Reverse-proxy variant
For "absolutely zero backend changes", you can put a small reverse proxy in front of the login endpoint that detects pkb_v1_ tokens, validates them against PasskeyBase, and rewrites the request body to swap the token for the user's actual stored password before forwarding to the legacy backend. We don't recommend this — it's a security footgun (the proxy now has to know plaintext passwords) — but it's an option for true black-box legacy systems. Talk to us before going down this path.
Security notes
- Replay protection — tokens are single-use. The verify endpoint atomically marks them consumed.
- Origin binding — the underlying WebAuthn assertion is bound to your domain. The shim won't work on a phishing site.
- Username binding — the verify call requires the username from the form to match the username embedded in the token. An attacker can't lift a token from one user's session and submit it as another.
- TLS only — the shim refuses to run on plain HTTP origins.
Implementation status
The shim is shipping behind the cdn.passkeybase.com/legacy-shim/v1.jsURL, built from packages/sdk-legacy-shim. The verify endpoint (POST /api/v1/verify-token) is part of the Management API. See the backlog for current implementation status.