AbsoluteJS

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.

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

MethodRouteDescription
POST/auth/passwordless/magic-linkEmails a one-time link.
Body: { email } — mounted when onSendMagicLink is set
POST/auth/passwordless/magic-link/verifyExchanges the link token for a session.
Body: { token }
POST/auth/passwordless/otpSends a 6-digit code over email or SMS.
Body: { email } — mounted when onSendOtp is set
POST/auth/passwordless/otp/verifyExchanges 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.

MethodRouteDescription
POST/auth/webauthn/register/optionsStart registration — add a passkey to the authenticated caller.
POST/auth/webauthn/register/verifyComplete registration and store the new passkey.
POST/auth/webauthn/authenticate/optionsStart passwordless, discoverable-credential authentication.
POST/auth/webauthn/authenticate/verifyComplete 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.