Email and password
Built-in email/password authentication with signup, password reset, email verification, and custom authorization.
The EmailPassword provider adds traditional email/password authentication on top of Auth.js Credentials. It handles signup, sign-in, password reset, and email verification with Argon2id hashing, CSRF protection, and auto sign-in after registration.
Setup
Configure the provider with your adapter:
import EmailPassword from '@robojs/auth/providers/email-password'
import { createFlashcoreAdapter } from '@robojs/auth'
import type { AuthPluginOptions } from '@robojs/auth'
const adapter = createFlashcoreAdapter({ secret: process.env.AUTH_SECRET! })
export default <AuthPluginOptions>{
secret: process.env.AUTH_SECRET,
adapter,
providers: [EmailPassword({ adapter })]
}import EmailPassword from '@robojs/auth/providers/email-password'
import { createFlashcoreAdapter } from '@robojs/auth'
const adapter = createFlashcoreAdapter({ secret: process.env.AUTH_SECRET! })
export default {
secret: process.env.AUTH_SECRET,
adapter,
providers: [EmailPassword({ adapter })]
}The adapter must be passed to both the plugin config and the EmailPassword provider.
Routes
When EmailPassword is active, these routes are registered:
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/signup | User registration |
| POST | /api/auth/callback/credentials | Sign-in (intercepted for session/email) |
| POST | /api/auth/password/reset/request | Request password reset token |
| GET | /api/auth/password/reset/confirm | Render password reset form |
| POST | /api/auth/password/reset/confirm | Submit new password |
| GET | /api/auth/verify-email | Email verification status |
| GET | /api/auth/verify-email/confirm | Confirm email verification token |
| POST | /api/auth/verify-email/request | Request new verification email |
Signup flow
On POST /signup, the provider:
- Validates email (regex + required), password (min 8 chars, optional confirmation match), terms acceptance, and CSRF token
- Creates user via adapter
- Hashes password with Argon2id
- Auto signs in via credentials provider
- Attaches database session cookie (if using database strategy)
- Sends welcome email and verification email (if mailer configured)
- Redirects to
callbackUrlorpages.newUseror/ - Rolls back user creation on failure
Client-side signup:
import { signUp } from '@robojs/auth/client'
await signUp({
email: 'user@example.com',
password: 'securepassword',
confirmPassword: 'securepassword',
name: 'John Doe'
})import { signUp } from '@robojs/auth/client'
await signUp({
email: 'user@example.com',
password: 'securepassword',
confirmPassword: 'securepassword',
name: 'John Doe'
})Sign-in flow
Uses standard Auth.js credentials flow:
import { signIn } from '@robojs/auth/client'
await signIn('credentials', { email: 'user@example.com', password: 'securepassword' })import { signIn } from '@robojs/auth/client'
await signIn('credentials', { email: 'user@example.com', password: 'securepassword' })Password reset flow
The 3-step password reset flow:
- POST
/password/reset/requestwith email — generates token, sends email - User clicks link → GET
/password/reset/confirm?token=...— renders built-in HTML form - POST
/password/reset/confirmwith token + new password — validates, updates, sends confirmation email
Email verification flow
Email verification follows this flow:
- Verification email sent on signup (if mailer configured)
- GET
/verify-email— returns status (HTML or JSON based on Accept header) - GET
/verify-email/confirm?token=...— consumes token, marks email verified - POST
/verify-email/request— generates new verification token emailVerifiedstatus is injected into sessions via composed callback
Custom authorize
Add custom logic before the default credential check:
EmailPassword({
adapter,
authorize: async (credentials, ctx) => {
const payload = await getRequestPayload(ctx.request)
const body = payload.get<{ inviteCode?: string }>()
if (!body.inviteCode) return null
await verifyInvite(body.inviteCode)
return ctx.defaultAuthorize()
}
})EmailPassword({
adapter,
authorize: async (credentials, ctx) => {
const payload = await getRequestPayload(ctx.request)
const body = payload.get()
if (!body.inviteCode) return null
await verifyInvite(body.inviteCode)
return ctx.defaultAuthorize()
}
})The EmailPasswordAuthorizeContext provides:
| Property | Type | Description |
|---|---|---|
adapter | PasswordAdapter | Active storage adapter |
request | Request | The incoming request |
defaultAuthorize() | () => Promise<AdapterUser | null> | Built-in credentials logic |
Route overrides
Override signup, passwordResetRequest, or passwordResetConfirm handlers:
EmailPassword({
adapter,
routes: {
signup: async ({ payload, defaultHandler }) => {
const body = payload.get<{ inviteCode?: string }>()
await verifyInvite(body.inviteCode)
payload.assign({ inviteCode: body.inviteCode?.trim().toUpperCase() })
return defaultHandler()
},
passwordResetRequest: async ({ payload, defaultHandler }) => {
console.log('Reset requested for:', payload.get().email)
return defaultHandler()
},
passwordResetConfirm: async ({ request, defaultHandler }) => {
// Add custom password policy
return defaultHandler()
}
}
})EmailPassword({
adapter,
routes: {
signup: async ({ payload, defaultHandler }) => {
const body = payload.get()
await verifyInvite(body.inviteCode)
payload.assign({ inviteCode: body.inviteCode?.trim().toUpperCase() })
return defaultHandler()
},
passwordResetRequest: async ({ payload, defaultHandler }) => {
console.log('Reset requested for:', payload.get().email)
return defaultHandler()
},
passwordResetConfirm: async ({ request, defaultHandler }) => {
// Add custom password policy
return defaultHandler()
}
}
})The EmailPasswordRouteContext provides:
| Property | Description |
|---|---|
payload | Shared RequestPayloadHandle — mutations persist through the lifecycle |
defaultHandler() | Invokes the stock handler (CSRF, hashing, sessions, emails) |
adapter | Storage adapter |
authConfig | Full Auth.js config |
cookies | Cookie configuration |
events | Auth.js events |
basePath, baseUrl | Route prefix and canonical URL |
secret | Auth.js secret |
sessionStrategy | 'jwt' or 'database' |
request | Raw request object |
Custom password hashing
Implement the PasswordHasher interface to use a different algorithm:
import EmailPassword from '@robojs/auth/providers/email-password'
import { compare, hash } from 'bcrypt'
import type { PasswordHasher } from '@robojs/auth'
class BcryptHasher implements PasswordHasher {
async hash(password: string) { return hash(password, 10) }
async verify(password: string, storedHash: string) { return compare(password, storedHash) }
needsRehash() { return false }
}
EmailPassword({ adapter, hasher: new BcryptHasher() })import EmailPassword from '@robojs/auth/providers/email-password'
import { compare, hash } from 'bcrypt'
class BcryptHasher {
async hash(password) { return hash(password, 10) }
async verify(password, storedHash) { return compare(password, storedHash) }
needsRehash() { return false }
}
EmailPassword({ adapter, hasher: new BcryptHasher() })For custom Argon2 parameters, use the exported Argon2Hasher class:
import { Argon2Hasher } from '@robojs/auth'
const hasher = new Argon2Hasher({
parameters: { memorySize: 8192, passes: 4 }
})
EmailPassword({ adapter, hasher })import { Argon2Hasher } from '@robojs/auth'
const hasher = new Argon2Hasher({
parameters: { memorySize: 8192, passes: 4 }
})
EmailPassword({ adapter, hasher })Default Argon2 parameters: memorySize=4096 KiB, passes=3, parallelism=1, tagLength=32.
Request payload
Use getRequestPayload to access and modify request data:
import { getRequestPayload } from '@robojs/auth'
const payload = await getRequestPayload(request)
const body = payload.get<{ email: string }>()
payload.assign({ email: body.email.toLowerCase() })import { getRequestPayload } from '@robojs/auth'
const payload = await getRequestPayload(request)
const body = payload.get()
payload.assign({ email: body.email.toLowerCase() })The payload handle provides:
| Method | Description |
|---|---|
get<T>() | Returns cached payload with type |
assign(partial) | Shallow-merges new fields |
replace(data) | Overwrites entire payload |
source | 'json', 'form', or 'empty' |
Mutations persist across authorize hooks, route overrides, and Auth.js callbacks.
Signup redirects
The default redirect after signup is /. Customize via callbackUrl in the request body or pages.newUser in your config.
