بنقرة واحدة
oauth-flow
OAuth 2.0 and OIDC integration with PKCE, Supabase Auth providers, and redirect URI validation
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
OAuth 2.0 and OIDC integration with PKCE, Supabase Auth providers, and redirect URI validation
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Apply this skill for Unite-Hub Supabase migrations, PostgREST/Data API visibility, founder-scoped Playwright journeys, or errors such as PGRST205, access=denied, stale Supabase linked refs, or migration history drift. Prevents repeating the SQL/cache/auth loop by enforcing the exact verification sequence for core journeys.
The compass for Unite-Hub's road to /shipit. Defines the single NorthStar (a real, comprehensive, working founder CRM in production, every section GREEN), the binding definition of GREEN, and the No-Invaders Manifest that keeps the build honest and surgical. Consult BEFORE deciding what to build/skip/finish — it resolves "200 ≠ real" temptations and scope-creep pressure. P1, auto-loaded.
Apply this skill WHEN scaffolding a new cron "pull" route that syncs external/derived data into Supabase on a schedule (Vercel cron). Encodes the Unite-Hub cron invariants: CRON_SECRET auth, FOUNDER_USER_ID actor, overlap safety, idempotent upsert, last-sync timestamp, and failure surfacing. Generic `cron-scheduler` covers scheduling; this covers the PULL handler body. P3.
Apply this skill WHEN verifying that a route, page, or integration serves REAL data and not silent mock/placeholder data. Detects the "false-green" failure mode: an endpoint returns 200 (or a page renders) while the underlying data is fabricated because a provider is unconnected. Trigger WHENEVER classifying a section's readiness, reviewing integration wrappers, or before marking anything GREEN. P2 — load on audit/verify tasks.
Manifest-first context isolation — each subagent receives only its scope, never the full codebase
Apply this skill for ANY decision with non-obvious tradeoffs: architectural choices, debugging without a clear root cause, performance strategies, security decisions, feature design with competing constraints, refactoring scope decisions. Forces multi-perspective analysis before committing to a solution. P1 auto-load — always active on complex reasoning tasks.
| name | oauth-flow |
| type | skill |
| version | 1.0.0 |
| priority | 2 |
| domain | security |
| description | OAuth 2.0 and OIDC integration with PKCE, Supabase Auth providers, and redirect URI validation |
OAuth 2.0 and OIDC integration patterns with PKCE, provider configuration, and session management for NodeJS-Starter-V1.
| 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 |
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.
auth/jwt.py)rbac-patterns skill)secret-management skill)csrf-protection skill)// 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.
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}`);
}
}
// 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.
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.
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);
}
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 user
Complements: rbac-patterns skill — after extracting the user from the JWT, apply permission checks via require_permission().
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.
| 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 |
Before merging oauth-flow changes:
jose or equivalentWhen 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]