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