Login & Authentication
How login works
Packages and users table
npm install jose bcryptjs # .env.local – a long random secret (generate with: openssl rand -base64 32) SESSION_SECRET=ZmJ4b3Y5c2VjcmV0LWtleS1jaGFuZ2UtbWUtMTIzNDU2
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
email VARCHAR(120) NOT NULL UNIQUE,
password_hash VARCHAR(100) NOT NULL,
role ENUM('student','admin') DEFAULT 'student'
);
-- create a hash in Node: require('bcryptjs').hashSync('admin123', 10)lib/session.js – signed cookie
The session is a small JWT (JSON Web Token) signed with your secret. Nobody can change role: 'student' to 'admin' without breaking the signature.
// lib/session.js
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { cookies } from 'next/headers'
const key = new TextEncoder().encode(process.env.SESSION_SECRET)
const COOKIE = 'session'
export async function createSession(user) {
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
const token = await new SignJWT({ id: user.id, name: user.name, role: user.role })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(key)
const store = await cookies()
store.set(COOKIE, token, { httpOnly: true, secure: process.env.NODE_ENV === 'production',
sameSite: 'lax', path: '/', expires })
}
export async function readSession(token) {
if (!token) return null
try {
const { payload } = await jwtVerify(token, key, { algorithms: ['HS256'] })
return payload // { id, name, role, iat, exp }
} catch {
return null // expired or tampered
}
}
export async function getSession() {
const store = await cookies()
return readSession(store.get(COOKIE)?.value)
}
export async function deleteSession() {
const store = await cookies()
store.delete(COOKIE)
}Login Server Action
// app/login/actions.js
'use server'
import bcrypt from 'bcryptjs'
import { redirect } from 'next/navigation'
import { query } from '@/lib/db'
import { createSession, deleteSession } from '@/lib/session'
export async function login(prevState, formData) {
const email = String(formData.get('email') || '').trim().toLowerCase()
const password = String(formData.get('password') || '')
if (!email || !password) return { error: 'Enter email and password.' }
const [user] = await query('SELECT id, name, password_hash, role FROM users WHERE email = ?', [email])
const ok = user && (await bcrypt.compare(password, user.password_hash))
if (!ok) return { error: 'Wrong email or password.' } // same message for both cases
await createSession(user)
redirect(user.role === 'admin' ? '/admin' : '/dashboard')
}
export async function logout() {
await deleteSession()
redirect('/login')
}Login form
// app/login/LoginForm.jsx
'use client'
import { useActionState } from 'react'
import { login } from './actions'
export default function LoginForm() {
const [state, action, pending] = useActionState(login, {})
return (
<form action={action} style={{ display: 'grid', gap: 12, maxWidth: 340 }}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
{state.error && <p style={{ color: 'crimson' }}>{state.error}</p>}
<button className="btn" disabled={pending}>{pending ? 'Checking…' : 'Login'}</button>
</form>
)
}
// app/login/page.jsx
import LoginForm from './LoginForm'
export const metadata = { title: 'Login', robots: { index: false } }
export default function LoginPage() {
return <main className="container section"><h1>Student login</h1><LoginForm /></main>
}Protecting pages (DAL)
// lib/dal.js — Data Access Layer: every protected read goes through here
import 'server-only'
import { cache } from 'react'
import { redirect } from 'next/navigation'
import { getSession } from './session'
export const requireUser = cache(async () => {
const session = await getSession()
if (!session) redirect('/login')
return session
})
export async function requireAdmin() {
const user = await requireUser()
if (user.role !== 'admin') redirect('/dashboard')
return user
}
// app/dashboard/page.jsx
import { requireUser } from '@/lib/dal'
import { logout } from '../login/actions'
export default async function Dashboard() {
const user = await requireUser()
return (
<main className="container section">
<h1>Welcome, {user.name} 👋</h1>
<form action={logout}><button className="btn light">Logout</button></form>
</main>
)
}Fast redirects with proxy.js
In Next.js 16, middleware.js is renamed proxy.js. It can redirect logged-out users before the page even starts rendering. Treat it as a first gate, not the only one.
// proxy.js (project root) — runs before matching requests
import { NextResponse } from 'next/server'
import { readSession } from '@/lib/session' // proxy runs on the Node.js runtime in Next.js 16
export async function proxy(request) {
const token = request.cookies.get('session')?.value
const session = await readSession(token)
if (!session) {
const url = new URL('/login', request.url)
url.searchParams.set('next', request.nextUrl.pathname)
return NextResponse.redirect(url)
}
return NextResponse.next()
}
export const config = { matcher: ['/dashboard/:path*', '/admin/:path*'] }Logout
The logout action deletes the cookie and redirects. Use it in a tiny form: <form action={logout}><button>Logout</button></form>.
Auth libraries (Auth.js, Clerk)
For "Login with Google", magic links or 2FA, use a library such as Auth.js, Better Auth or Clerk. The concepts you learned (session cookie, server checks, protected routes) stay exactly the same.
Common mistakes
- Storing session in
localStorage→ readable by any XSS script; use httpOnly cookies. - Different messages for "email not found" and "wrong password" → helps attackers find accounts.
- Checking login only in a Client Component → data is still reachable; check on the server.
- Short or committed
SESSION_SECRET→ anyone can forge sessions.
Summary
- bcrypt for passwords, signed JWT in an httpOnly cookie for sessions.
- Login/logout are Server Actions;
requireUser()protects pages and actions. proxy.js= fast redirect gate; always re-check near the data.
Create an admin user with a bcrypt hash, build /login, /dashboard and /admin. Log in as a student and confirm /admin redirects you away.
