Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🟣 Data, Server & APIs  ·  Lesson 24

Login & Authentication

How login works

BrowserNext.jsMySQLPOST email + password (Server Action)SELECT user WHERE email = ?password_hashbcrypt.compare ✔Set-Cookie: session=<signed JWT>; HttpOnlyGET /dashboard (cookie sent automatically)verify signature + expirypersonal dashboard HTML
Session cookie flow

Packages and users table

Terminal + .env.local
npm install jose bcryptjs

# .env.local – a long random secret (generate with: openssl rand -base64 32)
SESSION_SECRET=ZmJ4b3Y5c2VjcmV0LWtleS1jaGFuZ2UtbWUtMTIzNDU2
SQL – users table
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.

JS – lib/session.js
// 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

JS – app/login/actions.js
// 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

JSX – login form and page
// 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>
}
🔒 localhost:3000/loginEduSiteLoginCoursesHomeStudent loginEmailPasswordLoginWrong email or password.

Protecting pages (DAL)

JS + JSX – lib/dal.js and dashboard
// 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.

JS – proxy.js
// 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*'] }
Request /admin/studentsproxy.jsno valid cookie → /loginpage: requireAdmin()not admin → /dashboardData queryonly for verified admin
Two layers of checks

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.
✅ Practice

Create an admin user with a bcrypt hash, build /login, /dashboard and /admin. Log in as a student and confirm /admin redirects you away.

Frequently Asked Questions

Is it safe to store a JWT in a cookie?
Yes, if the cookie is httpOnly (JavaScript cannot read it), secure (HTTPS only), sameSite=lax and the token is signed with a strong secret and has an expiry.
Is proxy.js enough to protect pages?
No. Use proxy.js only for quick redirects. Always verify the session again where data is read (in the page, Server Action or Route Handler), because proxy matchers can miss routes.
Why hash passwords with bcrypt?
If the database leaks, attackers get only slow, salted hashes instead of real passwords. Never store plain-text or MD5 passwords.
← Next.js Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicProxy (Middleware) & Redirects Your progress: 24 / 37 (65%) · Lesson 25 / 37 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free