Project
bx-passkeys
WebAuthn passkeys for BoxLang: let people register a passkey and sign in with it, no password. It uses only JDK crypto (java.security), with no third-party jars, and has no routes of its own — you call four methods from your own handlers. The ColdBox edition is cbpasskeys.
JDK crypto only
ES256, Ed25519 and RS256 — the three algorithms browsers actually use — verified with java.security. A small CBOR decoder is built in for the COSE key.
Framework-agnostic
No routes. Challenges live in a session struct you pass in — session in a web app, req.session with boxlang-express.
Your credential store
A table-backed store (MSSQL, MySQL and Postgres scripts included) and an in-memory one ship with the module, or implement the five-method CredentialStore contract yourself.
Checked end to end
Single-use, expiring challenges; origin and rpId checks; signature counters that refuse cloned keys, with races between two sign-ins on one counter settled in the store.
Install
bashinstall-bx-module bx-passkeys
Requires BoxLang 1.19+. Attestation is "none": it trusts the key the authenticator hands over and doesn't check where the authenticator came from, which is what nearly every site wants.
Settings
json"modules": {
"bxPasskeys": {
"settings": {
"rpId": "example.com",
"rpName": "Example",
"store": { "datasource": "app", "table": "passkey_credentials" }
}
}
}
Or pass them straight in: passkeyRelyingParty( { rpId: "example.com" } ). Without origins, sign-ins are allowed over https on the rpId itself, and plain http only on localhost.
Registering a passkey
The user is already signed in some other way:
javascript// POST /passkeys/options
rp = passkeyRelyingParty()
return rp.registrationOptions( { id: user.id, name: user.email, displayName: user.name }, session )
// POST /passkeys body = what createPasskey() returned
try {
credential = rp.register( { id: user.id }, session, requestBody )
return { id: credential.credentialId, name: credential.name }
} catch ( "Passkeys.Invalid" e ) {
// 400, e.message is safe to show
}
Signing in
javascript// POST /sign-in/passkey/options
return passkeyRelyingParty().signInOptions( session )
// POST /sign-in/passkey body = what signWithPasskey() returned
try {
result = passkeyRelyingParty().verify( session, requestBody )
// result = { userId, credentialId, userVerified, signCount, credential }
// sign result.userId in; rotate the session id
} catch ( "Passkeys.Invalid" e ) {
// 401
}
To re-confirm a user who is already signed in before a sensitive action, pass their id: signInOptions( session, user.id ). Only that user's passkeys are offered, and verify() refuses anyone else's.
In the browser
client/passkeys.js is a small ES module, with types in client/passkeys.d.ts. Copy it into your front end:
javascriptimport { createPasskey, signWithPasskey } from './passkeys.js'
const post = (path, body) => fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body ?? {}) }).then((r) => r.json())
// add a passkey
const registration = await createPasskey(await post('/passkeys/options'))
if (registration) await post('/passkeys', registration)
// sign in
const assertion = await signWithPasskey(await post('/sign-in/passkey/options'))
if (assertion) await post('/sign-in/passkey', assertion)
Both functions return null when the person closes the browser's sheet.
Not included
- Attestation verification
- Authenticator metadata (MDS)
- WebAuthn extensions
Tests
TestBox specs, run headlessly. examples/flow.bxs registers a passkey, signs in with it and shows a replay being refused, with a test authenticator standing in for the browser:
bashbox install
boxlang setup-tests.bxs # once per checkout
boxlang run-tests.bxs
boxlang examples/flow.bxs