Radon
Auth Methods

Email + password

Classic email and password authentication with signup, login, and a full password-reset flow — bcrypt-hashed by default.

The familiar signup/login form, done safely: passwords are bcrypt-hashed (never stored in plaintext), errors are uniform to avoid user enumeration, and a complete password-reset flow is built in.

1. Enable the provider

Provide a sender and a resetUrl (where reset links point). resetUrl is required only if you use the reset flow.

lib/auth.ts
import { resendSender } from "@radonsdk/auth/senders/resend";

const sender = resendSender({ apiKey: process.env.RESEND_API_KEY!, from: "Acme <auth@acme.com>" });

export const auth = new Radon({
  adapter: postgresAdapter(pool),
  session: { secret: process.env.RADON_SECRET! },
  appName: "Acme",
  providers: {
    emailPassword: {
      sender,
      resetUrl: "https://acme.com/api/auth/password/reset",
    },
  },
});
OptionTypeDescription
senderEmailSenderDelivers the password-reset email.
resetUrlstringWhere reset links point. Required to use requestReset.
minLengthnumberMinimum password length. Default 8.
hasherPasswordHasherOverride the bcrypt hasher (see Custom hasher).
resetTemplateLinkTemplateCustom reset-email subject/HTML/text.
fromstringOverrides the sender's default From.

2. Use it over HTTP

Method & pathBodyEffect
POST /password/signup{ email, password }Creates account → sets session cookie
POST /password/login{ email, password }Signs in → sets session cookie
POST /password/request-reset{ email }Emails a reset link (always 200)
POST /password/reset{ token, newPassword }Sets a new password
Frontend — signup
const res = await fetch("/api/auth/password/signup", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ email, password }),
});
// res.ok → account created and session cookie set. 409 → email already exists.
Frontend — reset flow
// 1. User asks for a reset. Response is ALWAYS 200 (see the callout below).
await fetch("/api/auth/password/request-reset", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ email }),
});

// 2. User clicks the emailed link (lands on your reset page with ?token=…),
//    enters a new password, and you submit:
await fetch("/api/auth/password/reset", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ token, newPassword }),
});

Why request-reset always returns 200

request-reset never reveals whether an email has an account — it returns 200 either way and only sends mail if the account exists. This prevents attackers from using the reset form to discover which emails are registered.

3. Or call it directly in code

// Signup / login
const { user } = await auth.emailPassword.signup({ email, password });
const { user: same } = await auth.emailPassword.login({ email, password });

// Reset flow
await auth.emailPassword.requestReset({ email });                 // emails a signed link
await auth.emailPassword.setPassword({ token, newPassword });      // completes the reset

Return shapes

signup and login{ user: RadonUser }. requestReset({ email, redirectTo? }){ sent: boolean }. setPassword({ token, newPassword }){ user: RadonUser }. Resetting a password revokes the user's existing engine sessions for safety.

Security details

  • Bcrypt by default (cost 10). The hash lives in a reserved namespace on the user and is always stripped before a user object is returned to you.
  • Uniform errors. Login throws the same InvalidCredentialsError whether the email is unknown or the password is wrong — so the form can't be used to probe which emails exist.
  • Reset tokens are signed with your RADON_SECRET (HMAC-SHA256), single-use, and expire after 30 minutes.

Custom hasher

Prefer argon2, or a specific bcrypt cost? Implement the two-method PasswordHasher interface:

import type { PasswordHasher } from "@radonsdk/auth";
import argon2 from "argon2";

const argonHasher: PasswordHasher = {
  hash: (plain) => argon2.hash(plain),
  verify: (plain, hash) => argon2.verify(hash, plain),
};

providers: {
  emailPassword: { sender, resetUrl, hasher: argonHasher, minLength: 10 },
}

Errors

Error / codeHTTPMeaning
EmailExistsError / email_exists409Signup with an email that already has a password.
InvalidCredentialsError / invalid_credentials401Unknown email or wrong password.
WeakPasswordError / weak_password400Shorter than minLength.
TokenInvalidError / token_invalid401Reset token bad, expired, or used.

Next steps

On this page