Auth libraries are great, but understanding the OAuth 2.0 flow is important. Here is how to build it from scratch.
PKCE Helpers
// lib/oauth/pkce.ts
import { randomBytes, createHash } from "crypto";
export function generateCodeVerifier(): string {
return randomBytes(32)
.toString("base64url")
.replace(/[^a-zA-Z0-9-._~]/g, "")
.slice(0, 128);
}
export function generateCodeChallenge(verifier: string): string {
return createHash("sha256").update(verifier).digest("base64url");
}
export function generateState(): string {
return randomBytes(16).toString("hex");
}
Provider Configuration
// lib/oauth/providers.ts
interface OAuthProvider {
name: string;
authorizationUrl: string;
tokenUrl: string;
userInfoUrl: string;
clientId: string;
clientSecret: string;
scopes: string[];
mapUser: (data: Record<string, unknown>) => OAuthUser;
}
interface OAuthUser {
id: string;
email: string;
name: string;
avatar?: string;
}
const providers: Record<string, OAuthProvider> = {
google: {
name: "Google",
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scopes: ["openid", "email", "profile"],
mapUser: (data) => ({
id: data.id as string,
email: data.email as string,
name: data.name as string,
avatar: data.picture as string,
}),
},
github: {
name: "GitHub",
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scopes: ["read:user", "user:email"],
mapUser: (data) => ({
id: String(data.id),
email: data.email as string,
name: (data.name ?? data.login) as string,
avatar: data.avatar_url as string,
}),
},
};
export function getProvider(name: string): OAuthProvider {
const provider = providers[name];
if (!provider) throw new Error(`Unknown provider: ${name}`);
return provider;
}
Authorization Route
// app/api/auth/[provider]/route.ts
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getProvider } from "@/lib/oauth/providers";
import {
generateCodeVerifier,
generateCodeChallenge,
generateState,
} from "@/lib/oauth/pkce";
export async function GET(
request: Request,
{ params }: { params: Promise<{ provider: string }> },
) {
const { provider: providerName } = await params;
const provider = getProvider(providerName);
const state = generateState();
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
const cookieStore = await cookies();
// Store state and verifier in httpOnly cookies
cookieStore.set("oauth_state", state, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 600, // 10 minutes
path: "/",
});
cookieStore.set("oauth_code_verifier", codeVerifier, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 600,
path: "/",
});
const authUrl = new URL(provider.authorizationUrl);
authUrl.searchParams.set("client_id", provider.clientId);
authUrl.searchParams.set(
"redirect_uri",
`${process.env.NEXT_PUBLIC_APP_URL}/api/auth/${providerName}/callback`,
);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("scope", provider.scopes.join(" "));
authUrl.searchParams.set("state", state);
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");
redirect(authUrl.toString());
}
Callback Route
// app/api/auth/[provider]/callback/route.ts
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getProvider } from "@/lib/oauth/providers";
import { createSession } from "@/lib/session";
export async function GET(
request: Request,
{ params }: { params: Promise<{ provider: string }> },
) {
const { provider: providerName } = await params;
const provider = getProvider(providerName);
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const error = url.searchParams.get("error");
if (error) {
redirect(`/login?error=${encodeURIComponent(error)}`);
}
const cookieStore = await cookies();
const storedState = cookieStore.get("oauth_state")?.value;
const codeVerifier = cookieStore.get("oauth_code_verifier")?.value;
// Clean up cookies
cookieStore.delete("oauth_state");
cookieStore.delete("oauth_code_verifier");
// Validate state to prevent CSRF
if (!state || !storedState || state !== storedState) {
redirect("/login?error=invalid_state");
}
if (!code || !codeVerifier) {
redirect("/login?error=missing_code");
}
// Exchange code for tokens
const tokenResponse = await fetch(provider.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/${providerName}/callback`,
client_id: provider.clientId,
client_secret: provider.clientSecret,
code_verifier: codeVerifier,
}),
});
if (!tokenResponse.ok) {
redirect("/login?error=token_exchange_failed");
}
const tokens = await tokenResponse.json();
// Fetch user info
const userResponse = await fetch(provider.userInfoUrl, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
if (!userResponse.ok) {
redirect("/login?error=user_info_failed");
}
const userData = await userResponse.json();
const user = provider.mapUser(userData);
// Create session
await createSession({
userId: user.id,
email: user.email,
name: user.name,
avatar: user.avatar,
provider: providerName,
});
redirect("/dashboard");
}
Session Management
// lib/session.ts
import { SignJWT, jwtVerify } from "jose";
import { cookies } from "next/headers";
const secret = new TextEncoder().encode(process.env.SESSION_SECRET!);
interface SessionPayload {
userId: string;
email: string;
name: string;
avatar?: string;
provider: string;
}
export async function createSession(payload: SessionPayload) {
const token = await new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("7d")
.sign(secret);
const cookieStore = await cookies();
cookieStore.set("session", token, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7,
path: "/",
});
}
export async function getSession(): Promise<SessionPayload | null> {
const cookieStore = await cookies();
const token = cookieStore.get("session")?.value;
if (!token) return null;
try {
const { payload } = await jwtVerify(token, secret);
return payload as unknown as SessionPayload;
} catch {
return null;
}
}
export async function destroySession() {
const cookieStore = await cookies();
cookieStore.delete("session");
}
Login Buttons
export function LoginButtons() {
return (
<div className="flex flex-col gap-3">
<a
href="/api/auth/google"
className="inline-flex items-center justify-center gap-2 rounded-lg border px-4 py-2.5 text-sm font-medium hover:bg-muted transition-colors"
>
Sign in with Google
</a>
<a
href="/api/auth/github"
className="inline-flex items-center justify-center gap-2 rounded-lg border px-4 py-2.5 text-sm font-medium hover:bg-muted transition-colors"
>
Sign in with GitHub
</a>
</div>
);
}
Need Secure Authentication?
We build secure authentication flows for web applications. Contact us to protect your users.