| name | google-sso-setup |
| description | Wire Sign in with Google end-to-end: Google Cloud OAuth client + consent screen + redirect URIs + scopes, then via Supabase Auth's Google provider (and a note on the Auth.js / direct path). Step-by-step with verification checkpoints. |
Skill: google-sso-setup
Invoked by: the auth-identity team's identity-setup agent. Also consulted by ravenclaude-core/security-reviewer when reviewing any OAuth client registration change.
When to invoke: new project needs Google SSO; switching from password auth to Google-first; adding Google as a second provider to an existing Supabase project; debugging a redirect-URI mismatch or consent-screen error.
Output: working Google SSO flow (OAuth client registered, Supabase provider enabled, redirect URIs set, scopes justified, test login passes) + security checklist signed off.
Boundary
This skill authenticates the person (verifies their Google identity). It does not scope data access. Once the user is authenticated and auth.uid() is available, data-row scoping hands off to the data-platform plugin's RLS skills (rls-policy-authoring, jwt-embed-issuance). Do not duplicate RLS or embed-JWT mechanics here.
Overview: two paths
| Path | When to use |
|---|
| Supabase Auth → Google provider (recommended) | You are already using Supabase (Postgres + RLS). One config screen, managed token storage, auth.uid() available directly in RLS policies. |
| Auth.js (formerly NextAuth) + Google provider | You need framework-level flexibility (multiple providers, custom session shape, non-Supabase backend). More code, more config. |
| Google Identity Services SDK (direct) | Rare. Only when you want maximum control and are not using any managed auth layer. Avoid unless there is a strong reason. |
For this stack (Next.js + Supabase), default to the Supabase Auth path.
Step 1 — Google Cloud: create the OAuth client
Platform: console.cloud.google.com [unverified — verify against current console UI]
- Select or create a Google Cloud project.
- Navigate to APIs & Services → OAuth consent screen.
- Fill in the consent screen:
- App name, user support email, developer contact email — required.
- Scopes: add only what you need. For "Sign in with Google" you need
openid, email, profile — nothing more. Adding extra scopes (Drive, Calendar, etc.) triggers extended verification and delays publishing.
- Authorized domains: add your app's root domain (e.g.,
example.com). This controls what redirect URIs are allowed.
- Publishing status: leave as Testing during development. Move to Production before launch (required for users outside your test-user list).
- Navigate to Credentials → Create Credentials → OAuth 2.0 Client ID.
- Application type: Web application.
- Authorized JavaScript origins: your app's origin(s), e.g.
https://app.example.com (and http://localhost:3000 for local dev).
- Authorized redirect URIs: this is the most common source of errors — see Step 3 for the exact values.
- Save. Copy Client ID and Client Secret — you will need them in Step 2.
Verification checkpoint: the consent screen shows no "needs verification" banner for the three scopes above (openid, email, profile). If it does, you added a sensitive scope by mistake.
Step 2 — Supabase: enable the Google provider
[unverified — verify against current Supabase dashboard UI at supabase.com/docs/guides/auth/social-login/auth-google]
- Open the Supabase dashboard for your project.
- Go to Authentication → Providers → Google.
- Toggle Enable Sign in with Google.
- Paste the Client ID and Client Secret from Step 1.
- The Supabase dashboard will show you the callback URL you must add to Google Cloud. It looks like:
https://<project-ref>.supabase.co/auth/v1/callback
Copy it for Step 3.
- Save.
Step 3 — Google Cloud: set the redirect URI
Back in Google Cloud Console → Credentials → your OAuth client:
- Add the Supabase callback URL from Step 2 to Authorized redirect URIs:
https://<project-ref>.supabase.co/auth/v1/callback
- For local development also add:
http://localhost:54321/auth/v1/callback (Supabase local emulator default) [unverified — confirm your local port]
- Save.
Common error: redirect_uri_mismatch — the URI in the OAuth flow does not exactly match one in the authorized list. Trailing slashes, http vs https, and port numbers all matter. Copy-paste exactly.
Step 4 — Next.js: wire the sign-in call
The following is a conceptual sketch. Security-review before shipping any auth code to production.
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}
"use client";
import { createClient } from "@/lib/supabase/client";
export function GoogleSignInButton() {
const supabase = createClient();
async function signInWithGoogle() {
await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${location.origin}/auth/callback`,
},
});
}
return <button onClick={signInWithGoogle}>Sign in with Google</button>;
}
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get("code");
const next = requestUrl.searchParams.get("next") ?? "/dashboard";
if (code) {
const cookieStore = cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => cookieStore.getAll(), setAll: (c) => c.forEach(({ name, value, options }) => cookieStore.(name, value, options)) } },
);
supabase..(code);
}
.( (next, requestUrl.));
}
Security note: tokens are stored in HttpOnly cookies by @supabase/ssr — do not extract them to localStorage. See session-and-token-management skill and the never-store-tokens-in-localstorage best-practice.
Step 5 — Retrieve the session and auth.uid()
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export default async function DashboardPage() {
const cookieStore = cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => cookieStore.getAll(), setAll: () => {} } },
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
return <div>Welcome, {user.email}</div>;
}
The authenticated user's user.id maps to auth.uid() in Postgres RLS policies. Data scoping (which rows this user can read) is governed by the data-platform plugin's rls-policy-authoring skill — not by this plugin.
Scope discipline
| Scope | When to include |
|---|
openid | Always — required for OIDC / ID token issuance |
email | Always — your app needs the user's email |
profile | Usually — name + avatar |
https://www.googleapis.com/auth/drive | Only if your app genuinely reads/writes Google Drive. Triggers Google's extended verification. |
| Any other Google API scope | Only if you have a concrete feature requirement. Minimise scope. |
Auth.js path (alternative)
If you are not using Supabase or need framework flexibility:
npm install next-auth [unverified — confirm current package name; may be next-auth@beta for v5]
- Configure
[...nextauth]/route.ts with GoogleProvider({ clientId, clientSecret }).
- The redirect URI registered in Google Cloud is:
https://app.example.com/api/auth/callback/google.
- Use
getServerSession / auth() (v5) in server components.
- For cookie storage of the session, use the
cookies option with HttpOnly flags.
The token-storage and PKCE mechanics in oauth-oidc-flow-design and session-and-token-management skills apply regardless of which path you take.
Anti-patterns this skill flags
- Redirect URI contains a trailing slash when Google expects none (or vice versa) — causes
redirect_uri_mismatch
- Consent screen left in Testing mode at launch — external users cannot sign in (blocked after 100 test-user invitations)
- Client Secret exposed in front-end code or checked into git — must live in a server-side env var
- Scopes wider than
openid email profile without a documented feature requirement
- Token stored in
localStorage after the OAuth callback — use HttpOnly cookies (Supabase SSR handles this automatically)
signInWithOAuth called server-side — it triggers a browser redirect; call it from a client component or a route handler
- PKCE disabled — Supabase Auth enables it by default; do not disable it [unverified — confirm PKCE default in current Supabase Auth version]
Verification checklist
See also