
Oauth Flow
- 1 installs
- 1 repo stars
- Updated June 17, 2026
- cleanexpo/unite-hub
Helps with ai & agent building tasks.
About
oauth-flow is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- oauth-flow
- AI & Agent Building
- AI-coding skill
Oauth Flow by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,102 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cleanexpo/unite-hub --skill oauth-flowAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 1 |
| Last updated | June 17, 2026 |
| Repository | cleanexpo/unite-hub ↗ |
What it does
Helps with ai & agent building tasks.
Files
OAuth Flow
OAuth 2.0 and OIDC integration patterns with PKCE, provider configuration, and session management for NodeJS-Starter-V1.
---
Metadata
| Field | Value |
|---|---|
| Skill ID | oauth-flow |
| Category | Authentication & Security |
| Complexity | High |
| Complements | api-client, rbac-patterns, secret-management |
| Version | 1.0.0 |
| Locale | en-AU |
---
Description
Codifies OAuth 2.0 and OpenID Connect patterns for NodeJS-Starter-V1: authorisation code flow with PKCE, provider configuration for Google and GitHub, Supabase Auth integration, session management, token refresh, account linking, and security best practices for redirect URI validation.
---
When to Apply
Positive Triggers
- Adding social login (Google, GitHub) to the application
- Implementing OAuth 2.0 authorisation code flow with PKCE
- Configuring Supabase Auth with external providers
- Managing OAuth tokens, refresh, and session lifecycle
- Linking multiple OAuth providers to a single user account
Negative Triggers
- JWT creation and validation for internal auth (use existing
auth/jwt.py) - Role-based access control (use
rbac-patternsskill) - API key management and rotation (use
secret-managementskill) - CSRF protection for form submissions (use
csrf-protectionskill)
---
Core Principles
The Three Laws of OAuth
1. PKCE Always: Every authorisation code flow must use PKCE (Proof Key for Code Exchange). The implicit flow is deprecated — never use it. 2. Validate Redirect URIs: Redirect URIs must be whitelisted and validated on every request. Open redirects are a critical vulnerability. 3. Tokens Are Secrets: Access and refresh tokens must never appear in URLs, logs, or client-side storage. Use httpOnly cookies or server-side session storage.
---
Pattern 1: Supabase Auth Provider Configuration
Google and GitHub Setup
// apps/web/lib/supabase/auth-config.ts
export const oauthProviders = [
{
provider: "google" as const,
label: "Google",
scopes: "openid email profile",
queryParams: {
access_type: "offline", // Request refresh token
prompt: "consent", // Force consent screen
},
},
{
provider: "github" as const,
label: "GitHub",
scopes: "read:user user:email",
},
] as const;
export type OAuthProvider = (typeof oauthProviders)[number]["provider"];Project Reference: apps/web/components/auth/oauth-providers.tsx — the existing component renders Google and GitHub buttons. apps/web/app/auth/callback/route.ts — the callback handler exchanges the authorisation code for a Supabase session.
---
Pattern 2: Authorisation Code Flow with PKCE
Initiating the Flow
import { createClient } from "@/lib/supabase/client";
async function signInWithProvider(
provider: OAuthProvider,
redirectTo?: string,
): Promise<void> {
const supabase = createClient();
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback${
redirectTo ? `?next=${encodeURIComponent(redirectTo)}` : ""
}`,
queryParams: provider === "google"
? { access_type: "offline", prompt: "consent" }
: undefined,
},
});
if (error) {
throw new Error(`OAuth sign-in failed: ${error.message}`);
}
}Callback Handler
// apps/web/app/auth/callback/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
const next = searchParams.get("next") ?? "/dashboard";
const error = searchParams.get("error");
if (error) {
const description = searchParams.get("error_description") ?? error;
return NextResponse.redirect(
`${origin}/login?error=${encodeURIComponent(description)}`,
);
}
if (!code) {
return NextResponse.redirect(
`${origin}/login?error=${encodeURIComponent("No authorisation code")}`,
);
}
const supabase = await createClient();
const { error: exchangeError } =
await supabase.auth.exchangeCodeForSession(code);
if (exchangeError) {
return NextResponse.redirect(
`${origin}/login?error=${encodeURIComponent(exchangeError.message)}`,
);
}
return NextResponse.redirect(`${origin}${next}`);
}Rule: The code parameter is single-use. If the exchange fails, redirect to login with the error — never retry code exchange.
---
Pattern 3: Token Management
Secure Token Storage and Refresh
import { createClient } from "@/lib/supabase/client";
export async function getSession() {
const supabase = createClient();
const { data: { session }, error } = await supabase.auth.getSession();
if (error || !session) {
return null;
}
// Check if token needs refresh (within 60 seconds of expiry)
const expiresAt = session.expires_at ?? 0;
const now = Math.floor(Date.now() / 1000);
if (expiresAt - now < 60) {
const { data: { session: refreshed } } =
await supabase.auth.refreshSession();
return refreshed;
}
return session;
}
// Auth state listener
export function onAuthStateChange(
callback: (event: string, session: unknown) => void,
) {
const supabase = createClient();
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
callback(event, session);
},
);
return subscription;
}Rule: Never store tokens in localStorage. Supabase client handles storage via httpOnly cookies when configured with the server-side client.
---
Pattern 4: Account Linking
Multiple Providers per User
async function linkProvider(provider: OAuthProvider): Promise<void> {
const supabase = createClient();
const { error } = await supabase.auth.linkIdentity({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback?next=/settings`,
},
});
if (error) {
throw new Error(`Account linking failed: ${error.message}`);
}
}
async function unlinkProvider(identityId: string): Promise<void> {
const supabase = createClient();
const { error } = await supabase.auth.unlinkIdentity({
id: identityId,
// Prevent unlinking the last identity
});
if (error) {
throw new Error(`Unlink failed: ${error.message}`);
}
}
async function getLinkedProviders(): Promise<string[]> {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user?.identities) return [];
return user.identities.map((i) => i.provider);
}---
Pattern 5: Backend Token Validation (FastAPI)
Verifying Supabase JWT on API Requests
from fastapi import Depends, HTTPException, Request
from jose import jwt, JWTError
SUPABASE_JWT_SECRET = settings.SUPABASE_JWT_SECRET
async def get_current_user_from_oauth(request: Request):
"""Validate Supabase JWT from the Authorization header."""
auth_header = request.headers.get("authorization", "")
if not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing bearer token")
token = auth_header.removeprefix("Bearer ")
try:
payload = jwt.decode(
token,
SUPABASE_JWT_SECRET,
algorithms=["HS256"],
audience="authenticated",
)
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid token claims")
# Fetch or create user in local database
user = await get_or_create_user(user_id, payload)
return userComplements: rbac-patterns skill — after extracting the user from the JWT, apply permission checks via require_permission().
---
Pattern 6: Redirect URI Security
Whitelist Validation
const ALLOWED_REDIRECT_HOSTS = new Set([
"localhost",
"127.0.0.1",
process.env.NEXT_PUBLIC_APP_URL
? new URL(process.env.NEXT_PUBLIC_APP_URL).hostname
: "",
].filter(Boolean));
function isValidRedirectUri(uri: string): boolean {
try {
const url = new URL(uri);
return ALLOWED_REDIRECT_HOSTS.has(url.hostname);
} catch {
// Relative paths are allowed
return uri.startsWith("/") && !uri.startsWith("//");
}
}Rule: Always validate the next or redirectTo parameter against the whitelist. Open redirect attacks use OAuth callbacks to phish users.
---
Anti-Patterns
| Pattern | Problem | Correct Approach |
|---|---|---|
| Implicit flow (no PKCE) | Token exposed in URL fragment | Authorisation code + PKCE |
| Tokens in localStorage | XSS can steal tokens | httpOnly cookies via Supabase |
| No redirect URI validation | Open redirect vulnerability | Whitelist allowed hosts |
| Retry failed code exchange | Code is single-use, replay attack risk | Redirect to login on failure |
| Hardcoded client secrets in frontend | Secret exposed in bundle | Server-side only, env variables |
| No account linking | Users create duplicate accounts | Support multiple providers per user |
---
Checklist
Before merging oauth-flow changes:
- [ ] Authorisation code flow with PKCE (no implicit flow)
- [ ] Supabase Auth configured for Google and GitHub providers
- [ ] Callback handler with error handling and redirect validation
- [ ] Token refresh within 60 seconds of expiry
- [ ] Account linking for multiple providers per user
- [ ] Backend JWT validation with
joseor equivalent - [ ] Redirect URI whitelist validation
- [ ] No tokens in localStorage, URLs, or logs
---
Response Format
When applying this skill, structure implementation as:
### OAuth Flow Implementation
**Flow**: [authorisation code + PKCE / device code]
**Providers**: [Google, GitHub / custom]
**Auth Library**: [Supabase Auth / NextAuth / custom]
**Token Storage**: [httpOnly cookies / server session]
**Account Linking**: [enabled / disabled]
**Redirect Validation**: [whitelist / regex / none]