Passwordless & Passkeys
Sign in with magic links, email/SMS one-time codes, or WebAuthn passkeys. Every method mints the same session as password and OAuth login.
#Magic Links & OTP
The passwordless block delivers a one-time link or code over your existing email/SMS hooks and exchanges it for a session. Pair it with onCreateUser for signup on first login.
TS
import {
auth,
createNeonPasswordlessTokenStore
} from '@absolutejs/auth';
await auth<User>({
providersConfiguration: {},
passwordless: {
passwordlessTokenStore: createNeonPasswordlessTokenStore(process.env.DATABASE_URL),
getUserByEmail: (email) => findUserByEmail(email),
onCreateUser: ({ email }) => createUser({ email }), // optional: signup on first login
// each flow mounts only when its send hook is present:
onSendMagicLink: ({ email, token }) =>
sendEmail(email, `https://app.example.com/login?token=${token}`),
onSendOtp: ({ email, code }) => sendEmail(email, `Your code: ${code}`)
}
});#Routes
| Method | Route | Description |
|---|---|---|
| POST | /auth/passwordless/magic-link | Emails a one-time link. Body: { email } — mounted when onSendMagicLink is set |
| POST | /auth/passwordless/magic-link/verify | Exchanges the link token for a session. Body: { token } |
| POST | /auth/passwordless/otp | Sends a 6-digit code over email or SMS. Body: { email } — mounted when onSendOtp is set |
| POST | /auth/passwordless/otp/verify | Exchanges the code for a session. Body: { email, code } |
The token is delivered out-of-band and never returned from the (unauthenticated) request route. Both verify routes mint the same
SessionData as every other flow.#WebAuthn Passkeys
Biometric / passkey login behind a dependency-light WebAuthnAdapter you supply (wrapping @simplewebauthn/server). The package never bundles the WebAuthn crypto, so you pin your own version; option and response payloads stay opaque and credentials are base64url strings.
TS
import {
auth,
createNeonWebAuthnCredentialStore
} from '@absolutejs/auth';
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
await auth<User>({
providersConfiguration: {},
webauthn: {
credentialStore: createNeonWebAuthnCredentialStore(process.env.DATABASE_URL),
getUserId: (user) => user.sub,
getWebAuthnUser: (userId) => findUserBySub(userId),
rpId: 'example.com',
rpName: 'Acme',
origin: 'https://app.example.com',
// dependency-light: you wrap a vetted lib (the package never bundles the
// CBOR / COSE / attestation footgun):
webauthnAdapter: {
createRegistrationOptions: (req) => generateRegistrationOptions(/* … */),
verifyRegistration: (req) => verifyRegistrationResponse(/* … */),
createAuthenticationOptions: (req) => generateAuthenticationOptions(/* … */),
verifyAuthentication: (req) => verifyAuthenticationResponse(/* … */)
}
}
});#Passkey Flow
Registration adds a passkey to the authenticated caller; authentication is passwordless discoverable-credential sign-in.
| Method | Route | Description |
|---|---|---|
| POST | /auth/webauthn/register/options | Start registration — add a passkey to the authenticated caller. |
| POST | /auth/webauthn/register/verify | Complete registration and store the new passkey. |
| POST | /auth/webauthn/authenticate/options | Start passwordless, discoverable-credential authentication. |
| POST | /auth/webauthn/authenticate/verify | Complete authentication and mint the session. |
A short-lived, single-use cookie binds an options request to its verify request. The signature counter is persisted and bumped on each assertion to detect cloned authenticators.