Oauth auth
Universal AI development toolkit. 74 production-ready skills for every coding agent. Works with Claude Code, Cursor, Codex.
npx -y skills add medy-gribkov/arcana --skill oauth-authAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
OAuth2, OIDC, JWT validation, session management, PKCE flows, refresh token rotation, and auth middleware
SKILL.md
13.8 KB, ~3.3k tokens by cl100k_base, as published. Nobody here has run it
OAuth and Authentication
Implement secure OAuth2 flows, JWT validation, session management, and authentication patterns.
OAuth2 Authorization Code Flow with PKCE
BAD: No PKCE, state stored in localStorage
// Client initiates OAuth without PKCE
const authUrl = `https://provider.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code`;
localStorage.setItem('oauth_state', Math.random().toString());
window.location.href = authUrl;
GOOD: PKCE with httpOnly cookie state
import crypto from 'crypto';
// Generate PKCE challenge
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// Initiate OAuth with PKCE
export async function initiateOAuth(res: Response) {
const { verifier, challenge } = generatePKCE();
const state = crypto.randomBytes(32).toString('base64url');
// Store verifier and state in httpOnly cookie
res.setHeader('Set-Cookie', [
`pkce_verifier=${verifier}; HttpOnly; Secure; SameSite=Lax; Max-Age=600`,
`oauth_state=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600`
]);
const params = new URLSearchParams({
client_id: process.env.OAUTH_CLIENT_ID!,
redirect_uri: process.env.OAUTH_REDIRECT_URI!,
response_type: 'code',
scope: 'openid profile email',
state,
code_challenge: challenge,
code_challenge_method: 'S256'
});
return `https://provider.com/oauth/authorize?${params}`;
}
Token Exchange and Validation
BAD: No validation, symmetric JWT with weak secret
// Exchange code without validating state
const response = await fetch('https://provider.com/oauth/token', {
method: 'POST',
body: JSON.stringify({ code, client_id: clientId, client_secret: secret })
});
const { access_token } = await response.json();
// Weak symmetric JWT
import jwt from 'jsonwebtoken';
const token = jwt.sign({ userId }, 'weak-secret', { expiresIn: '1h' });
GOOD: State validation, PKCE verification, asymmetric JWT
import { SignJWT, jwtVerify, importSPKI, importPKCS8 } from 'jose';
export async function handleOAuthCallback(
code: string,
state: string,
cookies: Record<string, string>
) {
// Validate state to prevent CSRF
if (state !== cookies.oauth_state) {
throw new Error('Invalid state parameter');
}
// Exchange code with PKCE verifier
const response = await fetch('https://provider.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: process.env.OAUTH_REDIRECT_URI!,
client_id: process.env.OAUTH_CLIENT_ID!,
code_verifier: cookies.pkce_verifier
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const tokens = await response.json();
return tokens;
}
// Create asymmetric JWT (RS256)
export async function createAccessToken(payload: Record<string, any>) {
const privateKey = await importPKCS8(
process.env.JWT_PRIVATE_KEY!,
'RS256'
);
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
.setIssuedAt()
.setIssuer(process.env.JWT_ISSUER!)
.setAudience(process.env.JWT_AUDIENCE!)
.setExpirationTime('15m')
.sign(privateKey);
}
// Verify JWT with public key
export async function verifyAccessToken(token: string) {
const publicKey = await importSPKI(
process.env.JWT_PUBLIC_KEY!,
'RS256'
);
const { payload } = await jwtVerify(token, publicKey, {
issuer: process.env.JWT_ISSUER!,
audience: process.env.JWT_AUDIENCE!
});
return payload;
}
Refresh Token Rotation with Reuse Detection
BAD: No rotation, long-lived refresh tokens
// Single refresh token, no rotation
const refreshToken = crypto.randomBytes(32).toString('hex');
await db.saveRefreshToken(userId, refreshToken, { expiresIn: '30d' });
GOOD: Rotation with family tracking and reuse detection
interface RefreshToken {
tokenHash: string;
userId: string;
family: string;
expiresAt: Date;
revokedAt?: Date;
replacedBy?: string;
}
export async function createRefreshToken(userId: string, family?: string) {
const token = crypto.randomBytes(32).toString('base64url');
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const tokenFamily = family || crypto.randomBytes(16).toString('hex');
await db.refreshTokens.create({
tokenHash,
userId,
family: tokenFamily,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
});
return { token, family: tokenFamily };
}
export async function rotateRefreshToken(token: string) {
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const existing = await db.refreshTokens.findOne({ tokenHash });
if (!existing || existing.expiresAt < new Date()) {
throw new Error('Invalid or expired refresh token');
}
// Reuse detection: if token already replaced, revoke entire family
if (existing.revokedAt || existing.replacedBy) {
await db.refreshTokens.updateMany(
{ family: existing.family },
{ revokedAt: new Date() }
);
throw new Error('Token reuse detected, family revoked');
}
// Create new token in same family
const { token: newToken, family } = await createRefreshToken(
existing.userId,
existing.family
);
// Mark old token as replaced
await db.refreshTokens.updateOne(
{ tokenHash },
{
revokedAt: new Date(),
replacedBy: crypto.createHash('sha256').update(newToken).digest('hex')
}
);
return { accessToken: await createAccessToken({ userId: existing.userId }), refreshToken: newToken };
}
Session Management
BAD: Tokens in localStorage, no CSRF protection
// Client stores tokens in localStorage
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
// No CSRF protection
fetch('/api/sensitive', {
headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` }
});
GOOD: httpOnly cookies with CSRF tokens
import { serialize } from 'cookie';
export function setAuthCookies(
res: Response,
accessToken: string,
refreshToken: string
) {
const csrfToken = crypto.randomBytes(32).toString('base64url');
res.setHeader('Set-Cookie', [
serialize('access_token', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 15 * 60, // 15 minutes
path: '/'
}),
serialize('refresh_token', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60, // 7 days
path: '/api/auth/refresh'
}),
serialize('csrf_token', csrfToken, {
httpOnly: false, // Accessible to JS for request headers
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 15 * 60,
path: '/'
})
]);
return csrfToken;
}
// Middleware validates CSRF token
export function validateCSRF(req: Request) {
const cookieCSRF = req.cookies.csrf_token;
const headerCSRF = req.headers.get('X-CSRF-Token');
if (!cookieCSRF || cookieCSRF !== headerCSRF) {
throw new Error('CSRF validation failed');
}
}
NextAuth.js/Auth.js Pattern
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { JWT } from 'next-auth/jwt';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
prompt: 'consent',
access_type: 'offline',
response_type: 'code'
}
}
})
],
session: {
strategy: 'jwt',
maxAge: 7 * 24 * 60 * 60 // 7 days
},
callbacks: {
async jwt({ token, account, user }) {
if (account && user) {
return {
...token,
accessToken: account.access_token,
refreshToken: account.refresh_token,
accessTokenExpires: account.expires_at! * 1000,
userId: user.id
};
}
// Return previous token if access token not expired
if (Date.now() < (token.accessTokenExpires as number)) {
return token;
}
// Refresh access token
return await refreshAccessToken(token);
},
async session({ session, token }) {
session.user.id = token.userId as string;
session.accessToken = token.accessToken as string;
return session;
}
}
});
async function refreshAccessToken(token: JWT) {
try {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
grant_type: 'refresh_token',
refresh_token: token.refreshToken as string
})
});
const refreshed = await response.json();
if (!response.ok) {
throw refreshed;
}
return {
...token,
accessToken: refreshed.access_token,
accessTokenExpires: Date.now() + refreshed.expires_in * 1000,
refreshToken: refreshed.refresh_token ?? token.refreshToken
};
} catch (error) {
return {
...token,
error: 'RefreshAccessTokenError'
};
}
}
Password Hashing with bcrypt
BAD: Plain text or weak hashing
// Plain text password
await db.users.create({ email, password });
// Weak hash
import crypto from 'crypto';
const hash = crypto.createHash('md5').update(password).digest('hex');
GOOD: bcrypt with salt rounds
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return await bcrypt.compare(password, hash);
}
// Usage in registration
export async function registerUser(email: string, password: string) {
const existingUser = await db.users.findOne({ email });
if (existingUser) {
throw new Error('User already exists');
}
const passwordHash = await hashPassword(password);
return await db.users.create({ email, passwordHash });
}
Authentication Middleware
import { NextRequest, NextResponse } from 'next/server';
export async function authMiddleware(req: NextRequest) {
const token = req.cookies.get('access_token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', req.url));
}
try {
const payload = await verifyAccessToken(token);
// Attach user to request headers for route handlers
const requestHeaders = new Headers(req.headers);
requestHeaders.set('X-User-Id', payload.userId as string);
return NextResponse.next({
request: { headers: requestHeaders }
});
} catch (error) {
// Token invalid or expired, try refresh
const refreshToken = req.cookies.get('refresh_token')?.value;
if (!refreshToken) {
return NextResponse.redirect(new URL('/login', req.url));
}
try {
const { accessToken, refreshToken: newRefreshToken } =
await rotateRefreshToken(refreshToken);
const response = NextResponse.next();
setAuthCookies(response, accessToken, newRefreshToken);
return response;
} catch (refreshError) {
return NextResponse.redirect(new URL('/login', req.url));
}
}
}
// middleware.ts
export { authMiddleware as middleware };
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*']
};
Token Revocation
// Revoke all tokens for a user (on password change, logout all devices)
export async function revokeAllUserTokens(userId: string) {
await db.refreshTokens.updateMany(
{ userId, revokedAt: null },
{ revokedAt: new Date() }
);
}
// Revoke specific token
export async function revokeRefreshToken(token: string) {
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
await db.refreshTokens.updateOne(
{ tokenHash },
{ revokedAt: new Date() }
);
}
// Cleanup expired tokens (run daily)
export async function cleanupExpiredTokens() {
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days
await db.refreshTokens.deleteMany({
expiresAt: { $lt: cutoff }
});
}
OIDC ID Token Validation
import { jwtVerify, createRemoteJWKSet } from 'jose';
export async function validateIDToken(
idToken: string,
clientId: string,
issuer: string
) {
const JWKS = createRemoteJWKSet(
new URL(`${issuer}/.well-known/jwks.json`)
);
const { payload } = await jwtVerify(idToken, JWKS, {
issuer,
audience: clientId,
maxTokenAge: '10m'
});
// Validate nonce if using implicit flow
if (payload.nonce) {
// Compare with stored nonce from initial request
}
return {
sub: payload.sub,
email: payload.email as string,
email_verified: payload.email_verified as boolean,
name: payload.name as string
};
}
Run workflow: detect auth pattern needed, choose flow (OAuth2 code + PKCE for third-party, session cookies for first-party), implement token storage with httpOnly cookies, add CSRF protection, implement refresh rotation with reuse detection, add middleware guards, validate all tokens with asymmetric keys.
Gives 1 of the 12 instructions most auth identity skills give in ~3.3k tokens
Counted across 409 of the 410 authors here whose files we hold, read 2026-08-06
- hash passwords with bcrypt or argon2in 53 of 409, across 43 files
- use parameterized queriesin 47 of 409, across 39 files
- load SECRET_KEY from environment variablesin 23 of 409, across 14 files
- validate all input server-sidein 19 of 409, across 11 files
- refresh access tokens before expiryin 17 of 409, across 9 files
- store tokens in httponly cookieshere, and in 17 of 409, across 16 files
- store refresh tokens securelyin 16 of 409, across 6 files
- validate webhook signatures before processingin 15 of 409, across 5 files
- sanitize user inputsin 15 of 409, across 9 files
- implement rate limiting on auth endpointsin 14 of 409, across 9 files
- encrypt sensitive data at restin 13 of 409, across 10 files
- validate uploaded file extensions and sizesin 12 of 409, across 5 files
Said here and by no other author read
- use asymmetric algorithms for JWTs
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.