OAuth Setup

intermediate*

Production-ready OAuth authentication with Auth.js (NextAuth v5). Includes GitHub and Google providers with database session storage.

authoauthnextauthsession
Tested on201619TS5.9
$ bunx sinew add auth/oauth-setup
Interactive demo coming soon

Prerequisites

  • Database

    PostgreSQL, MySQL, or any Prisma-supported database

  • OAuth App

    GitHub and/or Google OAuth credentials

Tested With

2/2 passing
Next.js15.0
Auth.js5.0-beta

Related Patterns

Works well with

Extends

1The Problem

Implementing OAuth from scratch is complex and error-prone:

  • Token management and refresh
  • CSRF protection
  • Session handling
  • Database schema for users and accounts
  • Proper security headers

2The Solution

Auth.js handles all the complexity while giving you full control over the authentication flow.

3Files

lib/auth.ts

lib/auth.tsTypeScript
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "./db";

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
    }),
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  callbacks: {
    session({ session, user }) {
      session.user.id = user.id;
      return session;
    },
  },
  pages: {
    signIn: "/login",
    error: "/login",
  },
});

app/api/auth/[...nextauth]/route.ts

app/api/auth/[...nextauth]/route.tsTypeScript
import { handlers } from "@/lib/auth";

export const { GET, POST } = handlers;

middleware.ts

middleware.tsTypeScript
import { auth } from "@/lib/auth";

export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isOnDashboard = req.nextUrl.pathname.startsWith("/dashboard");

  if (isOnDashboard && !isLoggedIn) {
    return Response.redirect(new URL("/login", req.url));
  }
});

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

components/auth-button.tsx

components/auth-button.tsxTypeScript
import { signIn, signOut, auth } from "@/lib/auth";

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

  if (session) {
    return (
      <form
        action={async () => {
          "use server";
          await signOut();
        }}
      >
        <button type="submit">Sign out</button>
      </form>
    );
  }

  return (
    <form
      action={async () => {
        "use server";
        await signIn("github");
      }}
    >
      <button type="submit">Sign in with GitHub</button>
    </form>
  );
}

prisma/schema.prisma

prisma/schema.prismaprisma
model User {
  id            String    @id @default(cuid())
  name          String?
  email         String    @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}

model Account {
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@id([provider, providerAccountId])
}

model Session {
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
}

.env.example

.env.exampleBash
AUTH_SECRET="your-secret-here" # Generate with: openssl rand -base64 32

# GitHub OAuth
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"

# Google OAuth
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"

4Dependencies

$ bun add next-auth @auth/prisma-adapter

5Configuration

GitHub OAuth App

  1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
  2. Create a new OAuth App
  3. Set callback URL to http://localhost:3000/api/auth/callback/github (dev) or your production URL

Google OAuth

  1. Go to [Google Cloud Console](https://console.cloud.google.com)
  2. Create OAuth 2.0 credentials
  3. Add authorized redirect URI: http://localhost:3000/api/auth/callback/google

6Usage

Get Session in Server Components

import { auth } from "@/lib/auth";

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

  if (!session) {
    redirect("/login");
  }

  return <div>Welcome, {session.user.name}!</div>;
}
TypeScript

Get Session in Client Components

"use client";

import { useSession } from "next-auth/react";

export function UserGreeting() {
  const { data: session, status } = useSession();

  if (status === "loading") return <div>Loading...</div>;
  if (!session) return <div>Not signed in</div>;

  return <div>Hello, {session.user.name}!</div>;
}
TypeScript

7Troubleshooting

"CSRF token mismatch" error

Make sure your AUTH_SECRET is set and consistent across all environments.

Session not persisting

Check that your database is correctly configured and the Prisma adapter can write to it.

Related patterns