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.
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",
},
},
});| Option | Type | Description |
|---|---|---|
sender | EmailSender | Delivers the password-reset email. |
resetUrl | string | Where reset links point. Required to use requestReset. |
minLength | number | Minimum password length. Default 8. |
hasher | PasswordHasher | Override the bcrypt hasher (see Custom hasher). |
resetTemplate | LinkTemplate | Custom reset-email subject/HTML/text. |
from | string | Overrides the sender's default From. |
2. Use it over HTTP
| Method & path | Body | Effect |
|---|---|---|
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 |
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.// 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 resetReturn 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
InvalidCredentialsErrorwhether 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 / code | HTTP | Meaning |
|---|---|---|
EmailExistsError / email_exists | 409 | Signup with an email that already has a password. |
InvalidCredentialsError / invalid_credentials | 401 | Unknown email or wrong password. |
WeakPasswordError / weak_password | 400 | Shorter than minLength. |
TokenInvalidError / token_invalid | 401 | Reset token bad, expired, or used. |