≡ Menu

🔐 Mastering Authentication: Building a Login Form with Next.js Server Actions

🚨 CRITICAL SECURITY ALERT: Immediate Action Required for Next.js Users

Before diving into how to use Next.js Server Actions, you must first immediately verify and update your Next.js version to protect your application from a critical vulnerability.

The Exploit

A maximum-severity Remote Code Execution (RCE) vulnerability (CVE-2025-55182 / CVE-2025-66478, known as “React2Shell”) has been disclosed in the underlying React Server Components (RSC) “Flight” protocol.

This flaw is particularly relevant to Server Actions because it affects how the server deserializes attacker-controlled data sent to Server Function endpoints.

An unauthenticated attacker can send a specially crafted HTTP request to execute arbitrary code on the server. This is a critical risk, and exploitation has been observed in the wild.

Affected Next.js Versions

Next.js applications using the App Router are affected in the following release lines:

  • Next.js 15.x

  • Next.js 16.x

  • Next.js 14.3.0-canary.77 and later canary releases.

(If you are using the Pages Router or a stable Next.js 14.x version older than the affected canaries, you are not affected by this specific vulnerability.)

What You Must Do NOW

You need to upgrade your dependencies to a patched version immediately.

  1. Upgrade: Run the following command or update your package.json to a fixed version:

    • 15.x users: Upgrade to 15.0.5 or higher.

    • 16.x users: Upgrade to 16.0.7 or higher.

    • Canary users: Downgrade to the latest stable 14.x release or update to a patched canary version like 15.6.0-canary.58.npm install next@<patched-version>
  2. Rotate Secrets: If your application was online and unpatched, it is strongly recommended to rotate all application secrets, starting with critical ones (API keys, database credentials, etc.), as the RCE could have allowed an attacker to exfiltrate them.

  3. Use Official Tool: Vercel has provided an official interactive tool to help update affected apps:npx fix-react2shell-next

Please stop here, check your version, and apply the patch before continuing with this guide.

Your application’s security is paramount!

Now on to my blog post.

Handling user authentication is central to almost every modern web application.

For years, this meant writing separate API routes (like /api/login), managing fetch calls on the client, and juggling loading states.

Thanks to the App Router and React Server Components (RSC), Next.js now offers a dramatically simpler and more powerful solution: Server Actions (or React Server Functions).

In this post, we’ll walk through how to create a production-ready login form that is secure, performs fast, and offers progressive enhancement out of the box.


What are Next.js Server Actions?

Server Actions allow you to run server-side code (like database queries, mutations, or authentication logic) directly from a client-side component.

They are the new standard for data mutations in Next.js, eliminating the need for boilerplate API endpoints.

Key Benefits:

  • Security: Your sensitive server code (database credentials, secrets) never leaves the server and is never included in the client’s JavaScript bundle.

  • Progressive Enhancement: Forms submitted via a Server Action work even when JavaScript is disabled.

  • Simplicity: You replace complex fetch logic with a simple function call.


Step-by-Step Tutorial: Login Form Implementation

We will break this down into three files: the server logic, the interactive client component, and the final page.

1. Define the Server Action (src/actions/auth.js)

This file contains the critical authentication logic. The mandatory 'use server' directive at the top tells Next.js and the React Flight Protocol to keep this code confined to the server.

// src/actions/auth.js

'use server'; // 👈 MANDATORY: Marks all exports as Server Actions

import { redirect } from 'next/navigation';

export async function loginAction(formData) {
  // 1. Extract data from the native FormData object
  const email = formData.get('email');
  const password = formData.get('password');

  // --- Real-world Logic Goes Here ---
  
  // NOTE: Replace this placeholder with secure credential validation (e.g., comparing
  // hashed passwords against a database record).

  if (email === 'user@example.com' && password === 'password123') {
    // 2. Successful Login: Set Session/Cookie
    console.log(`User ${email} logged in successfully!`);
    
    // In a real application, you would create and set an encrypted session cookie here.
    // e.g., await createSession(userId);

    // 3. Use Next.js redirect to send the user to the dashboard
    redirect('/dashboard'); 
    
  } else {
    // 4. Failed Login: Return an error object
    console.error(`Login failed for ${email}`);
    
    // The client component will handle this returned object
    return { 
      error: 'Invalid credentials. Please check your email and password.',
      success: false 
    };
  }
}

2. Create the Client Component (src/components/LoginForm.jsx)

Although the logic runs on the server, we need a Client Component to handle user input, interactive loading states, and display error messages. We use the special useFormStatus hook to manage the form’s submission state seamlessly.

// src/components/LoginForm.jsx

'use client'; // 👈 Mandatory for interactive hooks like useState and useFormStatus

import React from 'react';
import { useFormStatus } from 'react-dom'; 
import { loginAction } from '@/actions/auth'; // Import your Server Action

// --- Helper Component for Loading State ---
function SubmitButton() {
  // useFormStatus monitors the status of the form it's contained within
  const { pending } = useFormStatus(); 

  return (
    <button 
      type="submit" 
      aria-disabled={pending} 
      disabled={pending}
    >
      {pending ? 'Logging in...' : 'Log In'}
    </button>
  );
}

// --- Main Login Form Component ---
export default function LoginForm() {
  const [errorMessage, setErrorMessage] = React.useState(null); 

  // Client-side wrapper to call the Server Action and handle its return value
  async function handleFormSubmit(formData) {
    setErrorMessage(null); 
    
    // 1. Call the imported Server Action directly
    const result = await loginAction(formData); 
    
    // 2. Check the return object for errors
    if (result && result.error) {
      setErrorMessage(result.error);
    }
    // Note: If login is successful, the server performs the redirect, 
    // so no 'else' logic is needed here.
  }

  return (
    // We pass our wrapper function to the 'action' prop
    <form action={handleFormSubmit} className="login-form">
      <h2>Login to Your Account</h2>
      
      {errorMessage && <p style={{ color: 'red', fontWeight: 'bold' }}>{errorMessage}</p>}

      <label htmlFor="email">Email:</label>
      <input type="email" id="email" name="email" required />

      <label htmlFor="password">Password:</label>
      <input type="password" id="password" name="password" required />
      
      <SubmitButton />
    </form>
  );
}

3. Assemble the Page (src/app/login/page.js)

Finally, we simply render the component on our page.

// src/app/login/page.js (Default Server Component)

import LoginForm from '@/components/LoginForm';

export default function LoginPage() {
  return (
    <main style={{ maxWidth: '400px', margin: '50px auto' }}>
      <h1>Secure Login</h1>
      <LoginForm />
    </main>
  );
}

🚀 The Power of useFormStatus

The useFormStatus hook is the true game-changer here. Notice how we didn’t write any try...catch blocks or manually manage a setLoading(true) state for the submit button.

The useFormStatus hook automatically detects when the form is submitting data to the Server Action and sets the pending state to true, providing a smooth, built-in loading experience for the user.


This pattern—defining business logic on the server and calling it directly from an interactive client form—is the recommended way to handle mutations in modern Next.js applications.

Useful links below:

Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service

Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2

Best email marketing automation solution on the market! http://www.aweber.com/?373860

Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29

Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff

Buy me a coffee ☕️https://buymeacoffee.com/tyronneratcliff

{ 0 comments… add one }

Leave a Comment