0Pricing
React Academy · Lesson

Credentials Provider & Custom Login

Authenticate users against a database with a credentials provider and a custom login form.

Credentials Provider & Custom Login is a free React Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the React Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

When to Use Credentials Provider

Use the Credentials provider when you authenticate against your own database (email + password) rather than an OAuth provider. It requires careful handling of password hashing and security.

Defining the Credentials Provider

Add Credentials to the providers array. The authorize function receives the submitted credentials and returns a user object or null.

import Credentials from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';

export const { handlers, auth } = NextAuth({
  providers: [
    Credentials({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        const user = await db.user.findUnique({ where: { email: credentials.email as string } });
        if (!user) return null;
        const valid = await bcrypt.compare(credentials.password as string, user.passwordHash);
        if (!valid) return null;
        return { id: user.id, name: user.name, email: user.email };
      },
    }),
  ],
});

Custom Login Form

Build a login form as a Client Component that calls signIn('credentials', { email, password }) from next-auth/react.

'use client';
import { signIn } from 'next-auth/react';

export function LoginForm() {
  const [error, setError] = useState('');

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = new FormData(e.currentTarget);
    const result = await signIn('credentials', {
      email: form.get('email'),
      password: form.get('password'),
      redirect: false, // handle redirect manually
    });
    if (result?.error) setError('Invalid email or password');
    else router.push('/dashboard');
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <p className="error">{error}</p>}
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit">Sign in</button>
    </form>
  );
}

redirect: false for Error Handling

Pass redirect: false to signIn() to receive the result object and handle errors in the component rather than being redirected to an error page.

const result = await signIn('credentials', { email, password, redirect: false });
if (result?.error) {
  // result.error is 'CredentialsSignin' for auth failures
  setError('Invalid email or password');
}

Zod Validation in authorize

Validate credentials with Zod inside the authorize function before hitting the database.

import { z } from 'zod';

const schema = z.object({ email: z.string().email(), password: z.string().min(8) });

async authorize(credentials) {
  const parsed = schema.safeParse(credentials);
  if (!parsed.success) return null;
  const { email, password } = parsed.data;
  // ... db lookup
}

Session Strategy for Credentials

Credentials provider requires JWT session strategy (no database adapter needed for sessions). Set session: { strategy: 'jwt' } explicitly.

export const { handlers, auth } = NextAuth({
  session: { strategy: 'jwt' },
  providers: [Credentials({ ... })],
});

Adding Custom Fields to Session

Pass extra user data through the JWT and session callbacks to expose it in useSession().

callbacks: {
  async jwt({ token, user }) {
    if (user) { token.id = user.id; token.role = user.role; }
    return token;
  },
  async session({ session, token }) {
    session.user.id = token.id as string;
    session.user.role = token.role as string;
    return session;
  },
},

Password Hashing

Always hash passwords with bcrypt before storing. Never store plain text passwords.

import bcrypt from 'bcryptjs';

// On registration:
const passwordHash = await bcrypt.hash(password, 12);
await db.user.create({ data: { email, passwordHash } });

// On login (in authorize):
const valid = await bcrypt.compare(submittedPassword, user.passwordHash);

Rate Limiting Sign-In Attempts

Protect the credentials endpoint from brute force by rate-limiting sign-in attempts using middleware or an in-memory store.

// middleware.ts or in a rate-limiting library
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '10 s') });

// Check in authorize before DB query:
const { success } = await ratelimit.limit(credentials.email as string);
if (!success) return null;

Registration Flow

Registration is separate from Auth.js — create the user in your database (with hashed password) via a Server Action or API route, then sign them in with signIn('credentials', ...).

Quick Check

Why should you pass redirect: false when calling signIn() in a custom login form?

Recap

Credentials provider calls your authorize function with submitted form data. Validate with Zod, compare hashed passwords with bcrypt, and return a user object or null. Use redirect: false in the form for custom error handling, and always use JWT strategy with credentials.

Frequently asked questions

Is the “Credentials Provider & Custom Login” lesson free?

Yes — the full text of “Credentials Provider & Custom Login” is free to read here on the web, and the React Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the React Academy course, upgrade to CoddyKit PRO.

What will I learn in “Credentials Provider & Custom Login”?

Authenticate users against a database with a credentials provider and a custom login form. You practise React Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start React Academy?

No prior experience is required. React Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Credentials Provider & Custom Login” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this React Academy lesson?

Yes. Every React Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Setting Up Auth.js in Next.js App Router
  2. Session Management & useSession Hook
  3. Credentials Provider & Custom Login
  4. Middleware-Based Route Protection
← Back to React Academy