Next.js Integration
This guide shows how to use the SDK with Next.js (App Router), including a secure OAuth flow with httpOnly cookies and API routes. A working example lives in examples/nextjs/ in the repo.
Goals
- Server-only storage of refresh/access tokens (httpOnly cookies)
- OAuth Authorization Code + PKCE flow
- Typed calls from server routes and client components
Folder Layout (App Router)
app/
api/
auth/
login/route.ts → starts PKCE + redirect
callback/route.ts → handles callback, stores cookies
refresh/route.ts → refreshes access token
profile/route.ts → calls PoE API using access token
page.tsx → example client component
Environment
OAUTH_CLIENT_ID=... # from PoE developer portal
OAUTH_CLIENT_SECRET=... # optional for confidential clients
OAUTH_REDIRECT_URI=http://localhost:3000/api/auth/callback
USER_AGENT="OAuth myapp/1.0.0 (contact: you@example.com)"
Login Route
app/api/auth/login/route.ts
import { NextResponse } from 'next/server';
import { OAuthHelper } from 'poe-js-sdk';
export async function GET() {
const state = crypto.randomUUID();
const pkce = await OAuthHelper.generatePKCE();
const url = OAuthHelper.buildAuthUrl(
{
clientId: process.env.OAUTH_CLIENT_ID!,
redirectUri: process.env.OAUTH_REDIRECT_URI!,
scopes: ['account:profile'], // recommend account:* only in user login
},
state,
pkce,
);
const res = NextResponse.redirect(url);
res.cookies.set('oauth_state', state, { httpOnly: true, sameSite: 'lax' });
res.cookies.set('pkce_verifier', pkce.codeVerifier, { httpOnly: true, sameSite: 'lax' });
return res;
}
Callback Route
app/api/auth/callback/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { OAuthHelper } from 'poe-js-sdk';
export async function GET(req: Request) {
const url = new URL(req.url);
const code = url.searchParams.get('code');
const returnedState = url.searchParams.get('state');
const cookieStore = cookies();
const state = cookieStore.get('oauth_state')?.value;
const verifier = cookieStore.get('pkce_verifier')?.value;
if (!code || !verifier || state !== returnedState) {
return new NextResponse('Invalid OAuth state or missing code', { status: 400 });
}
const tokens = await OAuthHelper.exchangeCodeForToken(
{
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET, // confidential only; token endpoint uses client_secret_post
redirectUri: process.env.OAUTH_REDIRECT_URI!,
scopes: ['account:profile'], // do not request service:* here
userAgent: process.env.USER_AGENT!, // set UA on token requests (server only)
},
code,
verifier,
);
const res = NextResponse.redirect(new URL('/', req.url));
res.cookies.set('access_token', tokens.access_token, { httpOnly: true, sameSite: 'lax' });
if (tokens.refresh_token) {
res.cookies.set('refresh_token', tokens.refresh_token, { httpOnly: true, sameSite: 'lax' });
}
res.cookies.delete('oauth_state');
res.cookies.delete('pkce_verifier');
return res;
}
Refresh Route
app/api/auth/refresh/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { OAuthHelper } from 'poe-js-sdk';
export async function POST() {
const cookieStore = cookies();
const refreshToken = cookieStore.get('refresh_token')?.value;
if (!refreshToken) return new NextResponse('Missing refresh token', { status: 401 });
const tokens = await OAuthHelper.refreshToken({
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET,
userAgent: process.env.USER_AGENT!,
}, refreshToken);
const res = NextResponse.json({ ok: true });
res.cookies.set('access_token', tokens.access_token, { httpOnly: true, sameSite: 'lax' });
if (tokens.refresh_token) {
res.cookies.set('refresh_token', tokens.refresh_token, { httpOnly: true, sameSite: 'lax' });
}
return res;
}
Using the API from a Route
app/api/profile/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { PoEApiClient } from 'poe-js-sdk';
export async function GET() {
const cookieStore = cookies();
const token = cookieStore.get('access_token')?.value;
if (!token) return new NextResponse('Unauthorized', { status: 401 });
const client = new PoEApiClient({
accessToken: token,
userAgent: process.env.USER_AGENT!,
});
const profile = await client.getProfile();
return NextResponse.json(profile);
}
Client Page Linking to Login
app/page.tsx
export default function Page() {
return (
<main>
<a href="/api/auth/login">Login with PoE</a>
</main>
);
}
See the full working version in examples/nextjs/ for additional cookie flags, error handling, and production hardening.