Credentials and Google Providers

Google Login

  1. Create an OAuth client in Google Cloud Console (APIs and Services → Credentials).
  2. Authorized redirect URI: http://localhost:3000/api/auth/callback/google (and your production domain).
  3. Put the id and secret in .env.local.

.env.local

.env.local

AUTH_SECRET=generated-by-npx-auth-secret
AUTH_GOOGLE_ID=1234567890-abc.apps.googleusercontent.com
AUTH_GOOGLE_SECRET=GOCSPX-xxxxxxxx
// components/GoogleButton.tsx (server component with a form action)
import { signIn } from '@/auth';

export default function GoogleButton() {
  return (
    <form
      action={async () => {
        'use server';
        await signIn('google', { redirectTo: '/account' });
      }}
    >
      <button type="submit">Continue with Google</button>
    </form>
  );
}

Email and Password

The Credentials provider lets you check an email and password against your own database. Store only a password hash (bcrypt or argon2).

// auth.ts
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import Google from 'next-auth/providers/google';
import bcrypt from 'bcryptjs';
import { z } from 'zod';
import { prisma } from '@/lib/prisma';

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

export const { handlers, auth, signIn, signOut } = NextAuth({
  session: { strategy: 'jwt' },
  providers: [
    Google,
    Credentials({
      credentials: { email: {}, password: {} },
      authorize: async (raw) => {
        const parsed = LoginSchema.safeParse(raw);
        if (!parsed.success) return null;
        const user = await prisma.user.findUnique({ where: { email: parsed.data.email } });
        if (!user?.passwordHash) return null;
        const ok = await bcrypt.compare(parsed.data.password, user.passwordHash);
        return ok ? { id: user.id, name: user.name, email: user.email, role: user.role } : null;
      },
    }),
  ],
});

Hashing Passwords

Example

Never store plain passwords. Node has scrypt built in; bcrypt and argon2 work the same way (salt + slow hash + constant-time compare). The salt is fixed here only so the output is stable:

import { scryptSync, timingSafeEqual } from 'node:crypto';

function hashPassword(password: string, salt: string): string {
  return salt + ':' + scryptSync(password, salt, 32).toString('hex');
}

function verifyPassword(password: string, stored: string): boolean {
  const [salt, hash] = stored.split(':');
  const candidate = scryptSync(password, salt, 32);
  return timingSafeEqual(candidate, Buffer.from(hash, 'hex'));
}

const stored = hashPassword('Shop@2026', 'a1b2c3d4e5f60718'); // real code: randomBytes(16).toString('hex')
console.log('stored:', stored.slice(0, 40) + '...');
console.log('correct password:', verifyPassword('Shop@2026', stored));
console.log('wrong password  :', verifyPassword('shop@2026', stored));

Output

stored: a1b2c3d4e5f60718:d29f6ec0b82c26322ad7fe7...
correct password: true
wrong password  : false