Modern SaaS users expect a one-click sign-in. The fastest way to deliver it in Next.js is with Auth.js (NextAuth v5), which ships tight App Router integration and first-party providers for Google, GitHub, and Apple. We’ll stand up a Next.js 15 app, wire providers with environment-first config, add secure buttons that work with Server Actions, and get you ready to deploy. You’ll need standard OAuth credentials for each provider and a place to host environment variables; if you deploy on DigitalOcean App Platform, make sure you have a DigitalOcean account so you can set those variables cleanly during the final step.
You will create a Next.js 15 app that exposes /api/auth/[...nextauth]
via Route Handlers, configures Google, GitHub, and Apple providers using environment variables only, protects pages with middleware.ts
, and renders sign-in/out buttons that run on the server (no client bundles). This approach uses Auth.js v5’s handler re-exports, environment variable inference (AUTH_*
), and server helpers (auth
, signIn
, signOut
).
1) Create and prepare the Next.js project
Install Next.js 15 (or later). If you’re upgrading, run the codemod; for new apps, scaffold from scratch.
# New app
npx create-next-app@latest next-auth-providers --use-npm
cd next-auth-providers
# Install Auth.js (NextAuth v5, currently published under the beta tag)
npm install next-auth@beta
Next.js 15 is stable and works with the App Router; Auth.js v5 documents installation with next-auth@beta
.
Generate a cryptographically strong secret for token/cookie encryption:
# Creates a 32+ char secret; copy it into .env.local as AUTH_SECRET
npx auth secret
Auth.js requires a secret in production and recommends the AUTH_
prefix for all variables.
2) Add the Auth.js core files (App Router)
Create auth.ts
at the project root. This is the single source of truth for your auth config and server helpers.
// auth.ts
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import Apple from "next-auth/providers/apple"
// Minimal config: rely on AUTH_* env inference for all providers
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [Google, GitHub, Apple],
// Optional: add callbacks, session strategy, events here
})
Auth.js v5 automatically reads AUTH_GOOGLE_ID/SECRET
, AUTH_GITHUB_ID/SECRET
, and AUTH_APPLE_ID/SECRET
if present—so you don’t need to pass clientId/clientSecret
manually.
Expose the Route Handler used by the OAuth flows:
// app/api/auth/[...nextauth]/route.ts
export const runtime = "edge" // optional; remove if you prefer Node runtime
export { GET, POST } from "../../../../auth"
Auth.js provides handlers
that you re-export as GET
and POST
.
Keep users signed in and gate routes with Middleware:
// middleware.ts
export { auth as middleware } from "./auth"
// Optionally restrict to specific paths with a matcher:
// export const config = { matcher: ["/dashboard/:path*"] }
auth
is designed to run in Middleware to keep sessions fresh and to protect routes.
3) Configure environment variables
Create .env.local
(not committed to VCS):
# Required for encryption (production only strictly required)
AUTH_SECRET=copy-from-npx-auth-secret
# Set this to your full site URL in production
# (AUTH_URL is aliased to NEXTAUTH_URL; either works)
AUTH_URL=http://localhost:3000
# Google OAuth
AUTH_GOOGLE_ID=...
AUTH_GOOGLE_SECRET=...
# GitHub OAuth
AUTH_GITHUB_ID=...
AUTH_GITHUB_SECRET=...
# Apple OAuth
AUTH_APPLE_ID=...
AUTH_APPLE_SECRET=...
Auth.js v5 infers provider credentials from AUTH_{PROVIDER}_{ID|SECRET}
and aliases AUTH_URL
/AUTH_SECRET
to their NextAuth v4 equivalents.
4) Create working UI: server-side sign-in/out
Add sign-in buttons that execute on the server with zero client JS:
// app/page.tsx
import { auth, signIn, signOut } from "../auth"
export default async function Home() {
const session = await auth()
return (
<main style={{ display: "grid", gap: 12, padding: 24 }}>
<h1>Next.js + Auth.js providers</h1>
{!session && (
<>
<form action={async () => { "use server"; await signIn("google") }}>
<button>Sign in with Google</button>
</form>
<form action={async () => { "use server"; await signIn("github") }}>
<button>Sign in with GitHub</button>
</form>
<form action={async () => { "use server"; await signIn("apple") }}>
<button>Sign in with Apple</button>
</form>
</>
)}
{session && (
<>
<p>Signed in as {session.user?.email}</p>
<form action={async () => { "use server"; await signOut() }}>
<button>Sign out</button>
</form>
</>
)}
</main>
)
}
auth()
reads the session in a Server Component; signIn
/signOut
are Server Actions provided by Auth.js v5.
5) Register each OAuth app and set callback URLs
Auth.js shows canonical callback patterns and the exact env variable names. You’ll copy IDs/secrets from each provider’s dashboard into .env.local
.
Create an OAuth 2.0 Client ID in Google Cloud Console. Set the redirect URI to:
https://<your-domain>/api/auth/callback/google
Then set AUTH_GOOGLE_ID
and AUTH_GOOGLE_SECRET
.
GitHub
Create a GitHub OAuth App (not a GitHub App) and set the redirect to:
https://<your-domain>/api/auth/callback/github
Then set AUTH_GITHUB_ID
and AUTH_GITHUB_SECRET
. If you need access to private emails, grant the “Email addresses” permission when configuring the app.
Apple
Use an Apple Developer account. Create a Services ID (acts as client_id
) and a private key (.p8) associated with your Team. Generate a client secret as a signed JWT (iss=TeamID
, sub=ServicesID
, aud=https://appleid.apple.com
, 6-month max expiry). Set:
AUTH_APPLE_ID=<your Services ID>
AUTH_APPLE_SECRET=<your generated client secret JWT>
Use this redirect URI:
https://<your-domain>/api/auth/callback/apple
Plan to rotate the Apple client secret at least every 6 months; Apple rejects longer expirations.
6) Local run and basic verification
Start the app:
npm run dev
Visit http://localhost:3000
. Click a provider button, complete the OAuth screen, and you should be redirected back with a populated session. If you see missing secret errors, verify AUTH_SECRET
is set; if a provider complains about redirect mismatch, double-check the exact callback URL in that provider’s dashboard.
7) Protect routes or entire sections
To require auth for a subtree (for example /dashboard
), add a matcher:
// middleware.ts
import { auth } from "./auth"
export default auth()
export const config = {
matcher: ["/dashboard/:path*"],
}
Auth decisions are controlled in callbacks.authorized
if you need role-based logic.
8) Deploy and set environment on DigitalOcean App Platform
When you push to a public repo, create an App Platform app from that repo. In Settings → Environment Variables, add the keys from .env.local
to your app and redeploy. The platform injects variables into your runtime; you can confirm via the console if needed. Also set AUTH_URL
to your production domain (for example, https://your-app.ondigitalocean.app
).
9) Production hardening
First, ensure your production URL is correct and served over HTTPS; Auth.js will use secure cookies automatically on HTTPS origins. Second, scope provider permissions narrowly; request only the profile/email scopes you need. Third, for Apple, schedule rotation of the client secret before the 6-month ceiling, or generate the JWT dynamically during startup or on demand. Finally, keep Next.js current; 15.5 includes type-safety and Turbopack improvements that benefit auth workflows and local DX.
Reference snippets you’ll reuse later
Sign-in buttons inside any Server Component:
import { signIn } from "../auth"
export function GoogleButton() {
return (
<form action={async () => { "use server"; await signIn("google") }}>
<button>Continue with Google</button>
</form>
)
}
Reading the user in a protected page:
// app/dashboard/page.tsx
import { auth } from "../../auth"
import { redirect } from "next/navigation"
export default async function Dashboard() {
const session = await auth()
if (!session) redirect("/")
return <pre>{JSON.stringify(session.user, null, 2)}</pre>
}
Extending provider options (e.g., Google offline access):
// auth.ts (partial)
import Google from "next-auth/providers/google"
export const { handlers, auth } = NextAuth({
providers: [
Google({
authorization: { params: { prompt: "consent", access_type: "offline" } },
}),
],
})
The authorization.params
pattern forces Google to issue a refresh token when needed.
Troubleshooting
If a provider rejects the redirect, copy the exact URL from the provider docs and paste it into the provider console. For missing envs in production, re-enter variables and trigger a redeploy so new values take effect. If sessions do not persist locally, remember that secure cookies default to off on http://localhost
and on in HTTPS—moving to a real domain usually resolves cookie scope issues.
Wrap-up
You now have Google, GitHub, and Apple sign-in in a Next.js 15 app using Auth.js v5’s handler API, server helpers, and AUTH_*
environment inference. From here, add an adapter (Prisma, MongoDB, Postgres) to persist user data, or keep the default JWT sessions for pure OAuth. When you deploy, set your variables in your hosting provider, point AUTH_URL
at your production domain, and rotate the Apple client secret on schedule.