LogoRobo.js

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:

config/plugins/robojs/auth.ts
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 })]
}
config/plugins/robojs/auth.js
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:

MethodPathDescription
POST/api/auth/signupUser registration
POST/api/auth/callback/credentialsSign-in (intercepted for session/email)
POST/api/auth/password/reset/requestRequest password reset token
GET/api/auth/password/reset/confirmRender password reset form
POST/api/auth/password/reset/confirmSubmit new password
GET/api/auth/verify-emailEmail verification status
GET/api/auth/verify-email/confirmConfirm email verification token
POST/api/auth/verify-email/requestRequest 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 callbackUrl or pages.newUser or /
  • 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:

  1. POST /password/reset/request with email — generates token, sends email
  2. User clicks link → GET /password/reset/confirm?token=... — renders built-in HTML form
  3. POST /password/reset/confirm with 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
  • emailVerified status 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:

PropertyTypeDescription
adapterPasswordAdapterActive storage adapter
requestRequestThe 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:

PropertyDescription
payloadShared RequestPayloadHandle — mutations persist through the lifecycle
defaultHandler()Invokes the stock handler (CSRF, hashing, sessions, emails)
adapterStorage adapter
authConfigFull Auth.js config
cookiesCookie configuration
eventsAuth.js events
basePath, baseUrlRoute prefix and canonical URL
secretAuth.js secret
sessionStrategy'jwt' or 'database'
requestRaw 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:

MethodDescription
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.

Next steps

On this page