Implement OAuth2 authentication flows (authorization code, PKCE for SPAs), provider integration (Google, GitHub), callback handling, token exchange, session management, and CSRF protection.
72
66%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Optimize this skill with Tessl
npx tessl skill review --optimize ./auth/oauth2-skill/SKILL.mdImplement OAuth2 authentication flows (authorization code, PKCE for SPAs), provider integration (Google, GitHub), callback handling, token exchange, session management, and CSRF protection.
# Node.js (Express)
npm install openid-client cookie-session
npm install -D @types/cookie-session
# Node.js (Passport-based alternative)
npm install passport passport-google-oauth20 passport-github2 express-session
npm install -D @types/passport @types/passport-google-oauth20 @types/express-session
# Python (FastAPI)
pip install httpx authlib itsdangeroussrc/
auth/
oauth.controller.ts # /auth/google, /auth/google/callback, etc.
oauth.service.ts # Token exchange, user lookup/creation
providers/
google.ts # Google OAuth2 config
github.ts # GitHub OAuth2 config
types.ts # OAuth profile types
lib/
session.ts # Session middleware configstate and code_verifier in the server session (or encrypted cookie) before redirecting to the provider. Validate state on callback to prevent CSRF.auth/providers/google.ts)export const googleConfig = {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
userInfoUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
callbackUrl: process.env.GOOGLE_CALLBACK_URL ?? "http://localhost:3000/api/auth/google/callback",
scopes: ["openid", "email", "profile"],
};auth/providers/github.ts)export const githubConfig = {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
userEmailsUrl: "https://api.github.com/user/emails",
callbackUrl: process.env.GITHUB_CALLBACK_URL ?? "http://localhost:3000/api/auth/github/callback",
scopes: ["read:user", "user:email"],
};auth/oauth.service.ts)import crypto from "node:crypto";
interface OAuthProviderConfig {
clientId: string;
clientSecret: string;
authorizationUrl: string;
tokenUrl: string;
userInfoUrl: string;
callbackUrl: string;
scopes: string[];
}
interface OAuthTokenResponse {
access_token: string;
token_type: string;
scope?: string;
id_token?: string;
}
// Generate PKCE code verifier and challenge
function generatePKCE() {
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");
return { codeVerifier, codeChallenge };
}
export function buildAuthorizationUrl(
config: OAuthProviderConfig,
state: string,
codeChallenge: string
): string {
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.callbackUrl,
response_type: "code",
scope: config.scopes.join(" "),
state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
return `${config.authorizationUrl}?${params.toString()}`;
}
export async function exchangeCodeForTokens(
config: OAuthProviderConfig,
code: string,
codeVerifier: string
): Promise<OAuthTokenResponse> {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
client_id: config.clientId,
client_secret: config.clientSecret,
code,
redirect_uri: config.callbackUrl,
grant_type: "authorization_code",
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
return response.json() as Promise<OAuthTokenResponse>;
}
export async function fetchUserInfo(
userInfoUrl: string,
accessToken: string
): Promise<Record<string, unknown>> {
const response = await fetch(userInfoUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`User info fetch failed: ${response.status}`);
}
return response.json() as Promise<Record<string, unknown>>;
}
export function generateState(): string {
return crypto.randomBytes(16).toString("hex");
}
export { generatePKCE };auth/oauth.controller.ts)import { Router, type Request, type Response } from "express";
import { googleConfig } from "./providers/google";
import { githubConfig } from "./providers/github";
import {
buildAuthorizationUrl,
exchangeCodeForTokens,
fetchUserInfo,
generateState,
generatePKCE,
} from "./oauth.service";
// Replace with your user creation/lookup and JWT issuance
import { findOrCreateUserByOAuth } from "../modules/users/users.service";
import { generateTokenPair } from "./auth.service";
export const oauthRouter = Router();
// --- Google ---
oauthRouter.get("/google", (req: Request, res: Response) => {
const state = generateState();
const { codeVerifier, codeChallenge } = generatePKCE();
// Store in session for validation on callback
req.session!.oauthState = state;
req.session!.codeVerifier = codeVerifier;
const url = buildAuthorizationUrl(googleConfig, state, codeChallenge);
return res.redirect(url);
});
oauthRouter.get("/google/callback", async (req: Request, res: Response) => {
const { code, state } = req.query;
// Validate state (CSRF protection)
if (state !== req.session!.oauthState) {
return res.status(403).json({ message: "Invalid state parameter" });
}
try {
const tokens = await exchangeCodeForTokens(
googleConfig,
code as string,
req.session!.codeVerifier
);
const userInfo = await fetchUserInfo(googleConfig.userInfoUrl, tokens.access_token);
const user = await findOrCreateUserByOAuth({
provider: "google",
providerId: userInfo.sub as string,
email: userInfo.email as string,
name: userInfo.name as string,
avatar: userInfo.picture as string,
});
const { accessToken, refreshToken } = await generateTokenPair(user.id, user.role);
// Clean up session
delete req.session!.oauthState;
delete req.session!.codeVerifier;
// Set refresh token cookie and redirect to frontend with access token
res.cookie("refresh_token", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
path: "/api/auth",
});
// Redirect to frontend with access token as URL fragment (not query param)
return res.redirect(`${process.env.FRONTEND_URL}/auth/callback#token=${accessToken}`);
} catch (error) {
console.error("OAuth callback error:", error);
return res.redirect(`${process.env.FRONTEND_URL}/auth/error`);
}
});
// --- GitHub ---
oauthRouter.get("/github", (req: Request, res: Response) => {
const state = generateState();
const { codeVerifier, codeChallenge } = generatePKCE();
req.session!.oauthState = state;
req.session!.codeVerifier = codeVerifier;
const url = buildAuthorizationUrl(githubConfig, state, codeChallenge);
return res.redirect(url);
});
oauthRouter.get("/github/callback", async (req: Request, res: Response) => {
const { code, state } = req.query;
if (state !== req.session!.oauthState) {
return res.status(403).json({ message: "Invalid state parameter" });
}
try {
const tokens = await exchangeCodeForTokens(
githubConfig,
code as string,
req.session!.codeVerifier
);
const userInfo = await fetchUserInfo(githubConfig.userInfoUrl, tokens.access_token);
// GitHub may not return email in profile — fetch from emails endpoint
let email = userInfo.email as string | null;
if (!email) {
const emails = await fetchUserInfo(githubConfig.userEmailsUrl!, tokens.access_token) as any[];
const primary = emails.find((e: any) => e.primary && e.verified);
email = primary?.email ?? null;
}
if (!email) {
return res.redirect(`${process.env.FRONTEND_URL}/auth/error?reason=no-email`);
}
const user = await findOrCreateUserByOAuth({
provider: "github",
providerId: String(userInfo.id),
email,
name: (userInfo.name as string) ?? (userInfo.login as string),
avatar: userInfo.avatar_url as string,
});
const jwtTokens = await generateTokenPair(user.id, user.role);
delete req.session!.oauthState;
delete req.session!.codeVerifier;
res.cookie("refresh_token", jwtTokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
path: "/api/auth",
});
return res.redirect(`${process.env.FRONTEND_URL}/auth/callback#token=${jwtTokens.accessToken}`);
} catch (error) {
console.error("OAuth callback error:", error);
return res.redirect(`${process.env.FRONTEND_URL}/auth/error`);
}
});// modules/users/users.service.ts
interface OAuthProfile {
provider: string;
providerId: string;
email: string;
name: string;
avatar?: string;
}
export async function findOrCreateUserByOAuth(profile: OAuthProfile) {
// Check if OAuth account already linked
let account = await findOAuthAccount(profile.provider, profile.providerId);
if (account) {
return findUserById(account.userId);
}
// Check if user exists with same email
let user = await findUserByEmail(profile.email);
if (!user) {
user = await createUser({
email: profile.email,
name: profile.name,
avatar: profile.avatar,
// No password — OAuth-only account
});
}
// Link OAuth account to user
// NOTE: Add a unique constraint on (provider, provider_account_id) in your database schema
// to prevent duplicate account links and enforce data integrity.
await createOAuthAccount({
provider: profile.provider,
providerId: profile.providerId,
userId: user.id,
});
return user;
}from authlib.integrations.starlette_client import OAuth
from starlette.config import Config
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse
config = Config(".env")
oauth = OAuth(config)
oauth.register(
name="google",
client_id=config("GOOGLE_CLIENT_ID"),
client_secret=config("GOOGLE_CLIENT_SECRET"),
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": "openid email profile"},
)
router = APIRouter(prefix="/auth")
@router.get("/google")
async def google_login(request: Request):
redirect_uri = request.url_for("google_callback")
return await oauth.google.authorize_redirect(request, redirect_uri)
@router.get("/google/callback")
async def google_callback(request: Request):
token = await oauth.google.authorize_access_token(request)
user_info = token.get("userinfo")
# Find or create user, issue JWT, redirect...
return RedirectResponse(url="/")# Set up environment variables
export GOOGLE_CLIENT_ID=your-google-client-id
export GOOGLE_CLIENT_SECRET=your-google-client-secret
export GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback
export GITHUB_CLIENT_ID=your-github-client-id
export GITHUB_CLIENT_SECRET=your-github-client-secret
export GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback
# Test OAuth flow (open in browser)
open http://localhost:3000/api/auth/google
open http://localhost:3000/api/auth/githubjwt-auth-skill. After OAuth2 callback, issue JWT access+refresh tokens using the JWT skill's generateTokenPair.nextauth-skill instead. NextAuth handles OAuth2 flows out of the box.oauth_accounts table (provider, providerId, userId). Pair with any database skill for the schema.nock (Node.js) or responses (Python) for HTTP mocking.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.