Using OAuth for Third-Party Authentication in a Next.js App

Share:

Why OAuth for Next.js?

OAuth 2.0 (often with OpenID Connect) lets users sign in with providers like Google, GitHub, Microsoft, Apple, etc. Benefits:

  • Faster onboarding (no passwords to store)
  • Better security (MFA, risk signals from the provider)
  • Less liability (you don’t handle passwords)
  • Works across web, mobile, and APIs

In browser-based Next.js apps, the Authorization Code + PKCE flow is the secure default.


Architecture at a Glance

  • Next.js App Router (server components + Route Handlers)
  • Auth.js (NextAuth) handles OAuth/OIDC flows, sessions, callbacks
  • Session strategy: JWT (no DB) or DB-backed via Prisma Adapter
  • Protected routes: Middleware + auth() server helper
  • API protection: Verify session in Route Handlers

Step 1: Install Auth.js (NextAuth) and peers

npm i next-auth
# If you’ll persist users/profiles:
npm i @prisma/client prisma

Initialize Prisma only if you want a database:

npx prisma init

Step 2: Create OAuth credentials

Create OAuth apps on the provider(s):

  • Google: Console → Credentials → OAuth 2.0 Client ID
    • Authorized redirect URI: https://your-domain.com/api/auth/callback/google
  • GitHub: Settings → Developer settings → OAuth Apps
    • Callback URL: https://your-domain.com/api/auth/callback/github

For local dev, use http://localhost:3000 instead of your domain.

Keep Client ID and Client Secret handy.


Step 3: Configure environment variables

.env.local:

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=replace_with_a_long_random_string

# Google
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret

# GitHub
GITHUB_ID=your_github_client_id
GITHUB_SECRET=your_github_client_secret

Generate a strong NEXTAUTH_SECRET (e.g., openssl rand -base64 32).


Step 4: Add Auth.js Route Handler (App Router)

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

import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import type { NextAuthConfig } from "next-auth";

export const authConfig: NextAuthConfig = {
  session: { strategy: "jwt", maxAge: 60 * 60 * 24 * 7 }, // 7 days
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      // PKCE is used automatically for OAuth 2.0 providers
    }),
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
  ],
  callbacks: {
    async jwt({ token, account, profile }) {
      // First login → persist provider data on the token
      if (account?.provider) {
        token.provider = account.provider;
      }
      // Optionally map profile fields
      if (profile && "email" in profile) token.email = String(profile.email);
      return token;
    },
    async session({ session, token }) {
      // Expose selected token fields to the client session
      session.user.provider = token.provider as string | undefined;
      return session;
    },
  },
  // optional: custom pages
  pages: { signIn: "/login" },
};

const handler = NextAuth(authConfig);
export { handler as GET, handler as POST };

Step 5: Auth helpers & route protection

Auth.js provides a server helper you can use anywhere on the server.

lib/auth.ts (optional convenience):

import { auth } from "next-auth";
export { auth }; // re-export so you can `import { auth } from "@/lib/auth"`

Protect pages (Server Components)

// app/dashboard/page.tsx
import { redirect } from "next/navigation";
import { auth } from "next-auth";

export default async function DashboardPage() {
  const session = await auth();
  if (!session) redirect("/login");
  return <div>Welcome, {session.user?.email}</div>;
}

Protect API routes (Route Handlers)

// app/api/private/route.ts
import { NextResponse } from "next/server";
import { auth } from "next-auth";

export async function GET() {
  const session = await auth();
  if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  return NextResponse.json({ ok: true, email: session.user?.email });
}

Global guard with middleware (optional)

// middleware.ts
import { withAuth } from "next-auth/middleware";

export default withAuth({
  pages: { signIn: "/login" },
});

export const config = {
  matcher: ["/dashboard/:path*", "/api/private/:path*"], // protected paths
};

Step 6: UI – login/logout buttons

// app/login/page.tsx
"use client";
import { signIn } from "next-auth/react";

export default function Login() {
  return (
    <main className="grid gap-3 p-8">
      <button onClick={() => signIn("google")}>Sign in with Google</button>
      <button onClick={() => signIn("github")}>Sign in with GitHub</button>
    </main>
  );
}
// app/components/UserMenu.tsx
"use client";
import { signOut, useSession } from "next-auth/react";

export function UserMenu() {
  const { data } = useSession();
  return data ? (
    <div>
      <span>{data.user?.email}</span>
      <button onClick={() => signOut()}>Sign out</button>
    </div>
  ) : null;
}

Wrap your app with the provider:

// app/providers.tsx
"use client";
import { SessionProvider } from "next-auth/react";

export default function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}

And include it in app/layout.tsx:

import Providers from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body><Providers>{children}</Providers></body>
    </html>
  );
}

Optional: Persist users with Prisma (Adapter)

npm i @auth/prisma-adapter

authConfig:

import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";

export const authConfig = {
  adapter: PrismaAdapter(prisma),
  session: { strategy: "database" }, // or still JWT if you prefer
  // ...providers, callbacks
} satisfies NextAuthConfig;

Run the Auth.js Prisma schema or merge models into yours, then prisma migrate.


Security Best Practices

  • Use PKCE (Auth.js does this for you with OAuth 2.0 providers).
  • Set NEXTAUTH_URL correctly for each environment; use HTTPS in prod.
  • Store secrets only in server env vars; never expose client secrets to the browser.
  • Short session/JWT lifetimes; rotate NEXTAUTH_SECRET if compromised.
  • Limit scopes to what you need (e.g., openid email profile).
  • CSRF is built-in; don’t disable it unless you know why.
  • Strict cookies: Auth.js sets HttpOnly, Secure, SameSite—keep them.
  • Role-based access: add role to the token in jwt callback and gate routes.
  • Logouts: consider provider revocation for sensitive apps.

Common Troubleshooting

  • Callback URL mismatch: Ensure provider settings exactly match /api/auth/callback/{provider}.
  • Wrong domain in production: Set NEXTAUTH_URL to https://your-domain.com.
  • CORS issues on API routes: Prefer same-origin calls to your own /api/*. If calling external APIs, include the bearer from the session on the server (not client).
  • Server Actions: Use const session = await auth() inside Server Actions for user context.

When to Consider a Hosted IdP

For enterprise SSO, audit logs, multi-tenant orgs, or passwordless/MFA policies, consider Auth0, Okta, Cognito, WorkOS, Stytch, etc. Auth.js integrates with most OIDC-compatible IdPs.


Conclusion

With Auth.js (NextAuth) and the App Router, adding secure OAuth to your Next.js app is straightforward:

  1. Create provider credentials
  2. Configure Route Handler with providers
  3. Protect pages/APIs using auth() or middleware
  4. Follow security best practices

Leave a Reply

Your email address will not be published. Required fields are marked *

Contact Now