Back to roadmaps authjs Course

Project: bcrypt Credentials Login with Rate Limiting

In this project, we will implement a custom username/password login system. We will verify hashes using bcryptjs and execute credential verification inside a Next.js Server Action to shield APIs from script attacks.


1. Creating the Verification Server Action

To process logins securely, write a Next.js Server Action:

// app/actions/loginAction.ts
"use server";

import { signIn } from "../../auth";
import { AuthError } from "next-auth";

export async function authenticateUser(formData: FormData) {
  const email = formData.get("email");
  const password = formData.get("password");

  if (!email || !password) {
    return { error: "Please enter email and password fields" };
  }

  try {
    // Call Auth.js internal credentials routing handler
    await signIn("credentials", {
      email,
      password,
      redirectTo: "/dashboard",
    });
    return { success: true };
  } catch (error) {
    if (error instanceof AuthError) {
      switch (error.type) {
        case "CredentialsSignin":
          return { error: "Invalid username or password credentials" };
        default:
          return { error: "Authentication system error occurred" };
      }
    }
    // Re-throw next.js internal redirect exceptions
    throw error;
  }
}

2. Designing the Custom Credentials Login Page

Create the login component containing the server action binding form:

// app/login-credentials/page.tsx
"use client";

import React, { useState } from "react";
import { authenticateUser } from "../actions/loginAction";

export default function CredentialsLoginPage() {
  const [errorMessage, setErrorMessage] = useState("");
  const [loading, setLoading] = useState(false);

  async function handleFormSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setErrorMessage("");
    setLoading(true);

    const formData = new FormData(e.currentTarget);
    const result = await authenticateUser(formData);

    if (result?.error) {
      setErrorMessage(result.error);
      setLoading(false);
    }
  }

  return (
    <div className="max-w-md mx-auto p-8 border rounded-3xl bg-white shadow-sm mt-20">
      <h2 className="text-xl font-bold text-gray-950">Credentials Login</h2>
      <p className="text-gray-400 text-xs mt-1">Provide your local registered details to enter.</p>

      {errorMessage && (
        <div className="p-3 bg-red-50 text-red-600 text-xs rounded-lg mt-4">
          {errorMessage}
        </div>
      )}

      <form onSubmit={handleFormSubmit} className="mt-8 space-y-6">
        <div>
          <label className="block text-xs font-semibold text-gray-700 uppercase">Email Address</label>
          <input
            type="email"
            name="email"
            required
            className="w-full border p-2.5 rounded-lg mt-2 text-sm"
          />
        </div>

        <div>
          <label className="block text-xs font-semibold text-gray-700 uppercase">Password</label>
          <input
            type="password"
            name="password"
            required
            className="w-full border p-2.5 rounded-lg mt-2 text-sm"
          />
        </div>

        <button
          type="submit"
          disabled={loading}
          className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2.5 rounded-lg text-sm transition"
        >
          {loading ? "Authenticating..." : "Login"}
        </button>
      </form>
    </div>
  );
}
Published on Last updated: