Plugins

Passkey

Official plugin — WebAuthn register and authenticate under /auth with UV, signCount, and ceremony session binding.

passkey() adds register and authenticate Flows for WebAuthn credentials (oke_passkeys). Options return a challenge plus a ceremony sessionId; register/authenticate verify client data, authenticator data, and ECDSA P-256.

The one rule

Enable gate.auth, then .plug(passkey()). Registration needs a Bearer session; authenticate is public. Echo the options sessionId with every ceremony — UV=false assertions never mint a session.

Quick start

Plug it

src/app.ts
import { oke } from "okengine";
import { passkey } from "okengine/plugins";

export const app = oke({
  name: "shop",
  env: "dev",
  gate: { auth: {} },
}).plug(passkey({ origins: ["http://localhost", "https://localhost"] }));

Register (session required)

Wire Bearer on createClient (auth.getToken / memorySession) — calls take input only.

const opts = await api.auth.passkeyRegisterOptions({});
// opts.data: { challenge, sessionId, rpId, userId }

await api.auth.passkeyRegister({
  credentialId: "...", // base64url
  publicKey: "...", // base64url SPKI (ECDSA P-256)
  userId: opts.data!.userId,
  challenge: opts.data!.challenge,
  sessionId: opts.data!.sessionId,
  clientDataJSON: "...", // base64url JSON { type: "webauthn.create", challenge, origin }
  authenticatorData: "...", // base64url (UP|UV required)
  signature: "...", // base64url ECDSA over authData || SHA-256(clientDataJSON)
});

Paths: POST /auth/passkey/register/options, POST /auth/passkey/register.

Authenticate

const opts = await api.auth.passkeyAuthenticateOptions({});
const { data } = await api.auth.passkeyAuthenticate({
  credentialId: "...",
  challenge: opts.data!.challenge,
  sessionId: opts.data!.sessionId,
  clientDataJSON: "...", // type must be "webauthn.get"
  authenticatorData: "...",
  signature: "...",
});

Paths: POST /auth/passkey/authenticate/options, POST /auth/passkey/authenticate. Challenges are single-use and bound to sessionId; wrong origin → invalid_origin; UV missing → user_not_verified; cloned counter → reregister_required.

Server checks

CheckBehavior
Ceremony typeclientDataJSON.type must be webauthn.create (register) or webauthn.get (authenticate)
User verificationAuthenticatorData UV bit must be set — UV=false is always rejected
Challenge bindingChallenge hash + sessionId must match; TTL ≤ 5 minutes; single-use
Signature counterStored per credential; if stored ≠ 0 and incoming signCount ≤ stored → delete credential, warn, reregister_required

Options

OptionTypeDefaultMeaning
secretstringactive*HMAC secret (*from gate.auth)
sessionsSessionStoreactive*Session store
now() => numberDate.nowInjectable clock
passkeysPasskeyStorenewCredential → user mapping
challengesVerificationStorenewRegistration / auth challenges
rpIdstring"localhost"Relying party id
originsstring[]["http://localhost","https://localhost"]Allowed clientDataJSON.origin

Surfaces

FlowPathGate
auth.passkeyRegisterOptionsPOST /auth/passkey/register/optionssession + bearer
auth.passkeyRegisterPOST /auth/passkey/registersession + bearer
auth.passkeyAuthenticateOptionsPOST /auth/passkey/authenticate/optionsgate.public + otp rate
auth.passkeyAuthenticatePOST /auth/passkey/authenticategate.public + otp rate

Consequence: a stolen credentialId without the private key (and UV) cannot mint a session. Set origins to your real app origins before production.

Troubleshooting

Learn more

Next

On this page