skills/implementing-scalekit-nextjs-auth/SKILL.md
Implements Scalekit authentication in a Next.js App Router project using the patterns from scalekit-inc/scalekit-nextjs-auth-example. Handles login, OAuth callback, session management, token refresh, logout, and permission-based access control using @scalekit-sdk/node. Use when adding auth routes, protecting pages, managing sessions, or checking permissions in a Next.js + Scalekit codebase.
npx skillsauth add scalekit-inc/skills implementing-scalekit-nextjs-authInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference repo: scalekit-inc/scalekit-nextjs-auth-example
app/api/auth/
├── login/route.ts # GET — generates auth URL + sets CSRF state
├── callback/route.ts # GET — exchanges code, sets session cookie
├── logout/route.ts # POST — clears session, returns Scalekit logout URL
├── refresh/route.ts # POST — refreshes access token, updates session
└── validate/route.ts # Token validation endpoint
lib/
├── scalekit.ts # Singleton ScalekitClient + default scopes
├── cookies.ts # Session read/write/clear + OAuth state helpers
└── auth.ts # isAuthenticated(), getCurrentUser(), hasPermission()
SCALEKIT_ENV_URL=https://your-env.scalekit.io
SCALEKIT_CLIENT_ID=your-client-id
SCALEKIT_CLIENT_SECRET=your-client-secret
SCALEKIT_REDIRECT_URI=http://localhost:3000/auth/callback
NEXT_PUBLIC_APP_URL=http://localhost:3000
SCALEKIT_SCOPES=openid profile email offline_access # optional, space-separated
SCALEKIT_REDIRECT_URI must exactly match the allowed callback URL in the Scalekit dashboard.
lib/scalekit.ts)Singleton pattern — always use getScalekitClient(), never instantiate directly. Throws if env vars are missing.
import { getScalekitClient, getDefaultScopes } from '@/lib/scalekit';
const client = getScalekitClient();
lib/cookies.ts)Session stored as JSON in a single scalekit_session HttpOnly cookie:
interface SessionData {
user: { sub, email, name, given_name, family_name, preferred_username };
tokens: { access_token, refresh_token, id_token, expires_at, expires_in };
roles?: string[];
permissions?: string[];
}
Key helpers:
getSession() — returns SessionData | nullsetSession(data) — writes HttpOnly cookie; expires = token expires_atclearSession() — deletes cookie (call on logout)isTokenExpired(session) — returns true if token expires within 5 minutesgetOAuthState() / setOAuthState(state) — CSRF state cookie, 10-min TTLhttpOnly: true, secure in production, sameSite: 'lax', path: '/'app/api/auth/login/route.ts — GET)const state = crypto.randomBytes(32).toString('base64url');
await setOAuthState(state);
const authUrl = client.getAuthorizationUrl(redirectUri, { state, scopes: getDefaultScopes() });
return NextResponse.json({ authUrl });
app/api/auth/callback/route.ts — GET)state param against stored oauth_state cookie → redirect to /error on mismatchclearOAuthState()client.authenticateWithCode(code, redirectUri) → authResponseclient.validateToken(authResponse.accessToken) → extract roles, permissions
permissions → https://scalekit.com/permissions → scalekit:permissionsuser.name → claims.name → givenName + familyName → email → preferred_username → 'User'setSession({ user, tokens, roles, permissions })/dashboardapp/api/auth/logout/route.ts — POST)const logoutUrl = client.getLogoutUrl({
idTokenHint: session.tokens.id_token,
postLogoutRedirectUri: process.env.NEXT_PUBLIC_APP_URL,
});
await clearSession();
return NextResponse.json({ logoutUrl });
// Client receives logoutUrl and redirects
app/api/auth/refresh/route.ts — POST)const refreshResponse = await client.refreshAccessToken(session.tokens.refresh_token);
// Decode exp from JWT using jose.decodeJwt(); fallback to 3600s if missing
await setSession({ ...session, tokens: { ...session.tokens, access_token, refresh_token, expires_at, expires_in } });
lib/auth.ts)isAuthenticated() // → boolean (session exists)
getCurrentUser() // → session.user | null
getAccessToken() // → access_token string | null
hasPermission('read:data') // → validates token, checks permission claim
For Server Components, call auth helpers directly:
import { isAuthenticated, getCurrentUser } from '@/lib/auth';
import { redirect } from 'next/navigation';
const authenticated = await isAuthenticated();
if (!authenticated) redirect('/login');
const user = await getCurrentUser();
For permission-gated pages:
import { hasPermission } from '@/lib/auth';
const allowed = await hasPermission('org:admin');
if (!allowed) redirect('/permission-denied');
| Route | Auth required |
|---|---|
| / | No |
| /login | No |
| /auth/callback | No |
| /dashboard | Yes |
| /sessions | Yes |
| /organization/settings | Yes + permission |
| /permission-denied | No |
| /error | No |
npm install @scalekit-sdk/node jose date-fns js-cookie
Add middleware.ts at the project root to enforce auth before any Server Component renders:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const PROTECTED_PATHS = ['/dashboard', '/sessions', '/organization']
export function middleware(request: NextRequest) {
const session = request.cookies.get('scalekit_session')
const isProtected = PROTECTED_PATHS.some(p => request.nextUrl.pathname.startsWith(p))
if (isProtected && !session) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('next', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!_next|api|favicon).*)'],
}
Server Components should still call isAuthenticated() as a second layer.
/api/auth/login returns { authUrl } — never navigate there with router.push. OAuth requires a full page navigation:
const { authUrl } = await fetch('/api/auth/login').then(r => r.json())
window.location.href = authUrl // full navigation, not client-side route change
Logout returns { logoutUrl } — the client must navigate to it:
const { logoutUrl } = await fetch('/api/auth/logout', { method: 'POST' }).then(r => r.json())
window.location.href = logoutUrl // navigates to Scalekit end-session endpoint
Local session is already cleared; this step revokes the IdP session so the user isn't silently re-authenticated on next login.
In the login page, read ?next from search params and carry it through the state:
// app/login/page.tsx
const next = searchParams.get('next') || '/dashboard'
// Pass next to /api/auth/login as a query param, store in session before redirect
// In /api/auth/callback: redirect to stored next URL after setSession()
Validate next on the server: only allow relative paths (/...) to prevent open redirect.
The scalekit_session and oauth_state cookies must use sameSite: 'lax'. The OAuth callback is a cross-site redirect from Scalekit back to your app — 'strict' drops the cookie on that redirect, causing a CSRF state mismatch error every time.
Without this, the browser back button after logout serves a cached authenticated page:
// In a protected route handler or layout
export const dynamic = 'force-dynamic'
// Or explicitly in a route handler:
return new Response(html, {
headers: { 'Cache-Control': 'no-store' },
})
Multiple browser tabs can simultaneously trigger token refresh with the same refresh token — most IdPs reject the second attempt. Mitigation: set a short-lived refresh_in_progress flag in the session before calling the refresh endpoint, and check it at the start of the refresh route to skip concurrent calls.
tools
Create or review Scalekit custom providers/connectors for proxy-only usage, including MCP providers. Use this skill when the task is to gather API docs, infer whether a connector is OAuth, Basic, Bearer, or API Key, determine if it is an MCP provider, determine required tracked fields like domain or version, generate provider JSON, check for existing custom providers, show update diffs, run approved create or update curls, and print resolved delete curls.
tools
Use when a developer is new to Scalekit and needs guidance on where to start, doesn't know which auth plugin or skill to choose, wants to connect an AI agent or agentic workflow to third-party services (Gmail, Slack, Notion, Google Calendar), needs OAuth or tool-calling auth for agents, wants to add authentication to a project but hasn't chosen an approach yet, or needs to install the Scalekit plugin for their AI coding tool (Claude Code, Codex, Copilot CLI, Cursor, or other agents).
tools
Use when a user asks to generate, review, validate, or fix any code snippet that uses Scalekit APIs or SDKs. This skill is the single source of truth for Scalekit code correctness — it can generate illustration-quality snippets from scratch (for docs, websites, or integration guides) and review existing code to catch wrong method names, missing parameters, security anti-patterns, and broken auth flows. Covers all four SDKs (Node, Python, Go, Java), raw REST API calls, and both Scalekit product suites — SaaSKit (SSO, login, sessions, RBAC, SCIM) and AgentKit (connections, tool calling, MCP auth). Use when the user says review my Scalekit code, generate a Scalekit example, validate this auth flow, check my SDK usage, fix my Scalekit integration, write a code sample for docs, or anything involving Scalekit code quality.
development
Walks through a structured production readiness checklist for Scalekit SSO implementations. Use when the user says they are going live, launching to production, doing a pre-launch review, hardening their SSO setup, or wants to verify their Scalekit implementation is production-ready.