Skip to main content
Chapter 5 Authentication and Middleware

Session Management and Protecting Actions

16 min read Lesson 19 / 28

Securing Server Actions

Middleware protects pages, but Server Actions need their own authorization checks. A malicious actor could call a Server Action directly via a POST request, bypassing the UI entirely.

Always Authorize in Server Actions

// app/actions/posts.ts
'use server';

import { auth } from '@/auth';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';

export async function deletePost(postId: string) {
    // Always check the session in every Server Action that mutates data
    const session = await auth();

    if (!session?.user?.id) {
        redirect('/login');
    }

    const post = await db.post.findUnique({
        where: { id: postId },
        select: { authorId: true },
    });

    if (!post) {
        throw new Error('Post not found');
    }

    // Check ownership
    if (post.authorId !== session.user.id && session.user.role !== 'admin') {
        throw new Error('You do not have permission to delete this post');
    }

    await db.post.delete({ where: { id: postId } });
    revalidatePath('/blog');
}

Helper for Getting the Current User

// lib/auth-helpers.ts
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export async function requireUser() {
    const session = await auth();

    if (!session?.user?.id) {
        redirect('/login');
    }

    return session.user;
}

export async function requireAdmin() {
    const user = await requireUser();

    if (user.role !== 'admin') {
        redirect('/unauthorized');
    }

    return user;
}

Use in actions and page components:

// app/actions/admin.ts
'use server';

import { requireAdmin } from '@/lib/auth-helpers';

export async function banUser(userId: string) {
    await requireAdmin(); // Throws/redirects if not admin

    await db.user.update({
        where: { id: userId },
        data: { banned: true },
    });
}

Handling Unauthorized Errors in UI

'use client';

import { deletePost } from '@/actions/posts';

export function DeleteButton({ postId }: { postId: string }) {
    async function handleDelete() {
        try {
            await deletePost(postId);
        } catch (error) {
            alert(error instanceof Error ? error.message : 'Failed to delete');
        }
    }

    return (
        <button
            onClick={handleDelete}
            className="text-red-600 hover:text-red-800"
        >
            Delete
        </button>
    );
}

Never trust the client to enforce authorization. Always verify the session server-side in every action that reads or modifies sensitive data.