Skip to main content
Email and password authentication provides a traditional credential-based login system with secure password hashing using bcryptjs.

Overview

This authentication method allows users to:
  1. Sign up with email and password
  2. Sign in with their credentials
  3. Optional: Verify their email address
Passwords are hashed using bcryptjs with 12 salt rounds before storage.

Setup

1
Initialize Auth
2
import { createAuth } from '@arraf-auth/core'
import { prismaAdapter } from '@arraf-auth/adapter-prisma'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

const auth = createAuth({
  secret: process.env.AUTH_SECRET!,
  database: prismaAdapter(prisma),
  trustedOrigins: ['http://localhost:3000', 'https://yourdomain.com']
})
3
Add API Routes
4
Create endpoints for sign up and sign in:
5
// app/api/auth/sign-up/route.ts
import { auth } from '@/lib/auth'

export async function POST(req: Request) {
  return auth.handler('/auth/sign-up', req)
}

// app/api/auth/sign-in/route.ts
export async function POST(req: Request) {
  return auth.handler('/auth/sign-in', req)
}

Sign Up

Create a new user account with email and password.

Request

POST /auth/sign-up
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "SecurePass123!",
  "name": "Ahmed Mohammed" // Optional
}
Passwords must be at least 8 characters long. This validation is enforced by the Zod schema in sign-up.ts:8.

Response

{
  "user": {
    "id": "user_abc123",
    "email": "[email protected]",
    "phone": null
  },
  "session": {
    "id": "session_xyz789",
    "token": "eyJhbGc...",
    "expiresAt": "2024-12-31T23:59:59Z"
  }
}
The response includes a Set-Cookie header that sets the session token.

Error Responses

// Email already in use
{
  "error": "Email already in use"
}

// Password too short
{
  "error": {
    "fieldErrors": {
      "password": ["String must contain at least 8 character(s)"]
    }
  }
}

// Invalid email format
{
  "error": {
    "fieldErrors": {
      "email": ["Invalid email"]
    }
  }
}

Sign In

Authenticate an existing user with their email and password.

Request

POST /auth/sign-in
Content-Type: application/json

{
  "method": "email",
  "email": "[email protected]",
  "password": "SecurePass123!"
}

Response

{
  "user": {
    "id": "user_abc123",
    "email": "[email protected]",
    "phone": null
  },
  "session": {
    "id": "session_xyz789",
    "token": "eyJhbGc...",
    "expiresAt": "2024-12-31T23:59:59Z"
  }
}

Error Response

{
  "error": "Invalid credentials"
}
The error message is intentionally generic to prevent user enumeration attacks. Both “user not found” and “wrong password” return the same error.

Password Hashing

Arraf Auth uses bcryptjs for secure password hashing:
// From packages/core/src/password.ts
import bcrypt from "bcryptjs"

const SALT_ROUNDS = 12

export async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS)
}

export async function verifyPassword(
  password: string,
  hash: string
): Promise<boolean> {
  return bcrypt.compare(password, hash)
}
The hashed password is stored in the Account table with:
  • providerId: "credential"
  • accountId: User ID
  • accessToken: Hashed password

Email Verification

While sign-up works without email verification, you can implement an email verification flow using the built-in verification system.

Send Verification Email

import { createAuth } from '@arraf-auth/core'

const auth = createAuth({
  // ... your config
})

// After sign up
const verification = await auth.adapter.createVerification({
  identifier: user.email,
  token: generateVerificationToken(),
  type: 'email-verification',
  expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
  attempts: 0
})

// Send email with verification link
await sendEmail({
  to: user.email,
  subject: 'Verify your email',
  html: `Click here to verify: https://yourapp.com/verify?token=${verification.token}`
})

Verify Email Endpoint

// app/api/auth/verify-email/route.ts
import { auth } from '@/lib/auth'

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url)
  const token = searchParams.get('token')
  
  if (!token) {
    return Response.json({ error: 'Missing token' }, { status: 400 })
  }
  
  // Find verification by token
  const verification = await auth.adapter.findVerification(
    token,
    'email-verification'
  )
  
  if (!verification) {
    return Response.json({ error: 'Invalid token' }, { status: 400 })
  }
  
  if (verification.expiresAt < new Date()) {
    await auth.adapter.deleteVerification(verification.id)
    return Response.json({ error: 'Token expired' }, { status: 400 })
  }
  
  // Update user
  const user = await auth.adapter.findUserByEmail(verification.identifier)
  if (user) {
    await auth.adapter.updateUser(user.id, { emailVerified: true })
    await auth.adapter.deleteVerification(verification.id)
  }
  
  return Response.json({ success: true })
}

Client Implementation

Example React hook for email/password authentication:
import { useState } from 'react'

interface SignUpData {
  email: string
  password: string
  name?: string
}

interface SignInData {
  email: string
  password: string
}

export function useEmailAuth() {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const signUp = async (data: SignUpData) => {
    setLoading(true)
    setError(null)
    
    try {
      const res = await fetch('/api/auth/sign-up', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      })
      
      const result = await res.json()
      
      if (!res.ok) {
        throw new Error(result.error || 'Sign up failed')
      }
      
      return result
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error')
      throw err
    } finally {
      setLoading(false)
    }
  }

  const signIn = async (data: SignInData) => {
    setLoading(true)
    setError(null)
    
    try {
      const res = await fetch('/api/auth/sign-in', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          method: 'email',
          ...data
        })
      })
      
      const result = await res.json()
      
      if (!res.ok) {
        throw new Error(result.error || 'Sign in failed')
      }
      
      return result
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error')
      throw err
    } finally {
      setLoading(false)
    }
  }

  return { signUp, signIn, loading, error }
}

Password Reset Flow

Implement password reset using the verification system:
// 1. Request password reset
export async function requestPasswordReset(email: string) {
  const user = await auth.adapter.findUserByEmail(email)
  
  if (!user) {
    // Don't reveal if email exists
    return { success: true }
  }
  
  const token = generateResetToken()
  
  await auth.adapter.createVerification({
    identifier: email,
    token,
    type: 'password-reset',
    expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
    attempts: 0
  })
  
  await sendPasswordResetEmail(email, token)
  
  return { success: true }
}

// 2. Reset password with token
export async function resetPassword(token: string, newPassword: string) {
  const verification = await auth.adapter.findVerification(
    token,
    'password-reset'
  )
  
  if (!verification || verification.expiresAt < new Date()) {
    throw new Error('Invalid or expired token')
  }
  
  const user = await auth.adapter.findUserByEmail(verification.identifier)
  if (!user) throw new Error('User not found')
  
  // Update password
  const hash = await hashPassword(newPassword)
  const account = await auth.adapter.findAccount('credential', user.id)
  
  if (account) {
    await auth.adapter.updateAccount(account.id, { accessToken: hash })
  }
  
  await auth.adapter.deleteVerification(verification.id)
  
  return { success: true }
}

Combined Email/Phone Sign Up

You can allow users to sign up with both email and phone:
POST /auth/sign-up
Content-Type: application/json

{
  "email": "[email protected]",
  "phone": "0512345678",
  "password": "SecurePass123!",
  "name": "Ahmed"
}
The validation schema requires either email or phone, but allows both (see sign-up.ts:10-16).

Security Best Practices

1
Strong Password Requirements
2
Enforce password complexity on the client side:
3
function validatePassword(password: string): string[] {
  const errors: string[] = []
  
  if (password.length < 8) {
    errors.push('Password must be at least 8 characters')
  }
  if (!/[A-Z]/.test(password)) {
    errors.push('Password must contain an uppercase letter')
  }
  if (!/[a-z]/.test(password)) {
    errors.push('Password must contain a lowercase letter')
  }
  if (!/[0-9]/.test(password)) {
    errors.push('Password must contain a number')
  }
  
  return errors
}
4
Rate Limiting
5
Prevent brute force attacks:
6
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, '15 m'), // 5 attempts per 15 minutes
})

export async function POST(req: Request) {
  const body = await req.json()
  const { success } = await ratelimit.limit(body.email)
  
  if (!success) {
    return Response.json(
      { error: 'Too many login attempts. Please try again later.' },
      { status: 429 }
    )
  }
  
  return auth.handler('/auth/sign-in', req)
}
7
Plugin Hooks
8
Use hooks to add security checks:
9
const securityPlugin = {
  id: 'security',
  hooks: {
    beforeSignIn: async (user) => {
      // Check if account is locked
      const lockStatus = await checkAccountLock(user.id)
      if (lockStatus.locked) {
        throw new Error('Account is temporarily locked')
      }
    },
    afterSignIn: async (user, session) => {
      // Clear failed login attempts
      await clearFailedAttempts(user.id)
      
      // Log successful login
      await logSecurityEvent({
        userId: user.id,
        event: 'sign_in',
        ip: session.ipAddress,
        userAgent: session.userAgent
      })
    }
  }
}
Production Recommendations:
  • Implement rate limiting on sign-in attempts
  • Add CAPTCHA after failed login attempts
  • Send email notifications for new sign-ins
  • Require email verification for sensitive operations
  • Use HTTPS in production (set secure: true in cookie options)

Next Steps

Build docs developers (and LLMs) love