AbsoluteJS

Credential Hardening

Block junk emails at sign-up, and catch passwords that show up in a breach after the account already exists.

#Email validation

validateEmailDeliverability checks format, blocks disposable domains (a starter list ships built-in; extend it), and optionally confirms the domain has MX records. isDisposableEmail is exposed on its own too.

TS
import { validateEmailDeliverability } from '@absolutejs/auth';

// In your register flow, before creating the user. checkMx does a DNS lookup;
// disposableDomains extends the built-in disposable list.
const result = await validateEmailDeliverability(email, {
  checkMx: true,
  disposableDomains: ['corporate-blocklist.example']
});

if (!result.ok) {
  // result.reason is 'invalid_format' | 'disposable' | 'no_mx'
  return status('Bad Request', result.reason);
}

#Compromised credentials

Sign-up already blocks breached passwords (the credentials policy's checkBreaches). The login-time check is the half of Auth0 "Credential Guard" a self-hosted library can do: catch a password that was fine at sign-up but later leaked, and prompt a reset.

Turnkey: set checkBreachesOnLogin: true on the credentials config. A successful login then carries passwordCompromised in its response.

The login-time check never blocks — the user is already authenticated — and it fails open on a HIBP outage.
TS
import { auth } from '@absolutejs/auth';

// Turnkey: let the credentials block run the login-time HIBP check for you.
// On a successful login it adds passwordCompromised to the response — it never
// blocks (the user is already authenticated), so prompt a reset on the next
// screen. Fails open on a HIBP outage.
auth({
  credentials: {
    // ...credentialStore, getUserByEmail, onCreateCredentialUser, etc.
    checkBreachesOnLogin: true
  }
});

// client, after POST /auth/login:
// if (data.passwordCompromised) showResetPasswordPrompt();

Or call isPasswordCompromised yourself for full control over the response:

TS
import { isPasswordCompromised } from '@absolutejs/auth';

// On a SUCCESSFUL credential login (the one moment you hold the plaintext),
// re-check it against HaveIBeenPwned. A password that was fine at sign-up may
// later appear in a breach — the login-time half of Auth0 "Credential Guard".
if (await isPasswordCompromised(password)) {
  // force a reset + notify; fails open on a HIBP outage
  return status('OK', { status: 'password_reset_required' });
}

#Background breach re-scan

0.37.0 adds runEmailBreachScan:

1
Walk the user population
Walks your user population in cursor-paged batches — the email counterpart to the login-time password check.
2
Check against HIBP
Each batch is checked against HIBP's breachedaccount API.
3
Run on a schedule & notify
Wire it up as a cron and notify users whose addresses show up in new breaches.
TS
import { runEmailBreachScan } from '@absolutejs/auth';

// 0.37.0: re-scan existing user EMAILS against HaveIBeenPwned's breached-account
// database on a schedule (the email counterpart to the login-time password check).
// Store-agnostic — supply iterateEmails as a cursor-paged callback so the package
// doesn't need to know your user table shape. HIBP rate-limited by default (1700ms);
// requires the paid breachedaccount API key.
const summary = await runEmailBreachScan({
  hibpApiKey: process.env.HIBP_API_KEY,
  iterateEmails: async (cursor) => {
    const rows = await db.execute(
      `SELECT email, id FROM users WHERE id > $1 ORDER BY id LIMIT 500`,
      [cursor ?? '']
    );
    return {
      emails: rows.map(r => r.email),
      nextCursor: rows.at(-1)?.id
    };
  },
  onBreachFound: async ({ email, breaches }) => {
    await mailer.send({ to: email, template: 'breach_notice', data: { breaches } });
    await auditStore.append('email_breach_detected', { email, count: breaches.length });
  }
});
console.log(`scanned ${summary.scanned}, found breaches for ${summary.breached}`);

#Prune inactive users

The other half of 0.37.0's background ops: pruneInactiveUsers.

1
Walk the paged population
A pure orchestrator that walks a paged user population.
2
Identify inactive users
Identifies anyone past the inactivity threshold.
3
Delegate the delete
You decide what "prune" means via the onDelete hook — soft-delete, hard-delete, or disable + notify.
dryRun: true reports candidates without touching anything.
TS
import { pruneInactiveUsers } from '@absolutejs/auth';

// 0.37.0: walk the user population, identify anyone past olderThanDays + delete
// (or just list in dryRun). Pure orchestrator — onDelete decides what "prune"
// means in your system (soft-delete, hard-delete, disable+notify).
const result = await pruneInactiveUsers({
  olderThanDays: 365,
  dryRun: false,
  iterateUsers: async (cursor) => {
    const rows = await db.execute(
      `SELECT id AS user_id, last_login_at_ms AS "lastLoginAt", created_at_ms AS "createdAt"
       FROM users WHERE id > $1 ORDER BY id LIMIT 500`,
      [cursor ?? '']
    );
    return { users: rows, nextCursor: rows.at(-1)?.user_id };
  },
  onDelete: async (userId) => {
    await db.execute(`UPDATE users SET disabled_at = now() WHERE id = $1`, [userId]);
    await auditStore.append('user_pruned_inactive', { userId });
  }
});
console.log(`scanned ${result.scanned}, pruned ${result.prunedUserIds.length}`);

#Secure-by-default cookies

Session cookies are Secure by default. Only the explicit development/test environments opt out (so http://localhost and test runners round-trip cookies) — every other case, including a production deploy that forgot NODE_ENV, gets Secure cookies. Override with cookieSecure when a proxy terminates TLS but reports the request as http.

TS
import { auth } from '@absolutejs/auth';

// Session cookies are Secure by DEFAULT. Only the explicit development / test
// environments opt out (so http://localhost and test runners still round-trip
// cookies) — every other case, including a production deploy that forgot to set
// NODE_ENV, gets Secure cookies. That closes the "session id sent over plaintext
// HTTP" gap without you having to remember a flag.
auth({
  // Force it either way when you need to — e.g. behind a proxy that terminates
  // TLS but forwards the request as http:  cookieSecure: true
  credentials: { /* ...credentialStore, getUserByEmail, onSendEmail */ }
});

#Account-enumeration resistance

Registration is enumeration-safe by default: a duplicate email returns the same generic response a new pending registration does — it never says “email already registered.” onExistingAccount lets you nudge the real owner out of band; revealRegistrationConflicts: true opts back into the explicit 409. Login is timing-equalized too, so response time can't reveal which emails exist.

TS
import { auth } from '@absolutejs/auth';

auth({
  credentials: {
    // Registration is enumeration-safe by DEFAULT: a duplicate email returns the
    // SAME generic response a new pending registration does — it never says
    // "email already registered". onExistingAccount nudges the real owner out of
    // band. (Full indistinguishability assumes requireEmailVerification: true.)
    requireEmailVerification: true,
    onExistingAccount: ({ email }) =>
      mailer.send({ to: email, template: 'you_already_have_an_account' }),

    // Opt back into the explicit 409 if you'd rather surface it:
    // revealRegistrationConflicts: true,
    // ...credentialStore, getUserByEmail, onCreateCredentialUser, onSendEmail
  }
});

// Login is timing-equalized too: a request for an email that doesn't exist burns
// the same argon2id work a real verify would, so response time can't be used to
// enumerate which addresses are registered.

#Origin (CSRF) allowlist

trustedOrigins rejects login/register requests whose Origin header isn't one of yours. enforceTrustedOrigins defaults to true (block); set it false for a report-only rollout, and onUntrustedOrigin fires in both modes so you can observe the real Origin set before enforcing on a login path.

Report-only first on a login path: log with onUntrustedOrigin, confirm the Origins are exactly yours, then flip enforceTrustedOrigins to true.
TS
import { auth } from '@absolutejs/auth';

auth({
  credentials: {
    // Reject login/register requests whose Origin header isn't one of yours
    // (login/registration CSRF defense).
    trustedOrigins: ['https://app.example.com', 'https://www.example.com'],

    // Roll it out REPORT-ONLY first on a login path — observe the real Origin set
    // before you block anyone. enforce defaults to true (secure by default); set
    // it false to log-don't-block. onUntrustedOrigin fires in BOTH modes, so it
    // drives the report-only rollout and stays useful once you enforce.
    enforceTrustedOrigins: false,
    onUntrustedOrigin: ({ origin, request }) =>
      logger.warn('untrusted origin', {
        origin,
        path: new URL(request.url).pathname
      })
  }
});

#Fail-closed route guard

requireAuthPlugin is the fail-closed counterpart to protectRoutePlugin: mounting it guards every route in scope by default, rejecting an unauthenticated request with 401 before the handler runs. Forgetting a per-route check therefore can't silently leave a route public.

TS
import { Elysia } from 'elysia';
import { requireAuthPlugin } from '@absolutejs/auth';

// Fail-closed counterpart to protectRoutePlugin: MOUNTING it guards every route
// in scope by default. An unauthenticated request is rejected with 401 in
// onBeforeHandle, before the handler runs — so forgetting a per-route check can't
// silently leave a route public (protectRoutePlugin's opt-in failure mode).
const app = new Elysia()
  .use(requireAuthPlugin({ authSessionStore }))
  .get('/me', ({ user }) => user)          // user is guaranteed present here
  .get('/settings', () => renderSettings());

#Safe redirects & cookie parsing

isSafeLocalPath/toSafeLocalPath validate a post-login returnUrl as same-origin — rejecting the backslash open-redirect a naive check misses. readSessionCookie reads the session id from a request with an anchored parse, so a decoy xuser_session_id= cookie can't shadow the real one. Use these instead of hand-rolling either check.

TS
import {
  isSafeLocalPath,
  toSafeLocalPath,
  readSessionCookie
} from '@absolutejs/auth';

// Don't hand-roll the "is this returnUrl same-origin?" check. A naive
// startsWith('/') && !startsWith('//') still accepts '/\evil.com', which
// browsers fold to the protocol-relative '//evil.com' — an open redirect.
const target = isSafeLocalPath(returnUrl) ? returnUrl : '/'; // predicate
const safe = toSafeLocalPath(returnUrl); // or coerce, fallback '/'

// Reading the session id OUTSIDE Elysia's cookie plugin (your own middleware, a
// rate limiter)? Use the anchored reader so a decoy 'xuser_session_id=' can't
// shadow the real cookie:
const sessionId = readSessionCookie(request);