# Social Login (Google OAuth) Zero-setup Google login for your app. Cohesivity handles the entire OAuth flow, stores users, issues tokens, and manages sessions. No Google Cloud Console setup needed. ## Prerequisites Provision this resource before use. ### Provision curl -s -X POST https://cohesivity.ai/api/resources/social-login \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"callback_urls": ["https://yourapp.com/auth/done", "http://localhost:5173/auth/done"]}' **Required body:** `{ "callback_urls": ["..."] }` — array of allowed redirect URLs. Also accepts `{ "callback_url": "..." }` (single string, converted to array). Subdomain wildcards are supported (e.g. `https://*.cohesivity.xyz/auth/callback`). **Replace, not merge:** `POST /api/resources/social-login` **replaces** the entire `callback_urls` list — include every URL you want active in every call. URLs omitted from a re-provision are dropped. Pre-register both production and any localhost callbacks before first deploy so you don't have to re-POST mid-development. Re-provisioning to update callbacks does NOT delete existing users or sessions. **Response:** `{ login_url, callback_urls, events_table, events_endpoint }` — the login URL, registered callback URLs, and the D1 auth-event table (`social_login_events`). Social-login auto-provisions `database` when needed so the event table exists on the tenant. ### Delete curl -s -X DELETE https://cohesivity.ai/api/resources/social-login \ -H "Authorization: Bearer " **WARNING: Deleting social-login permanently removes ALL users, sessions, auth events, and configuration. This is irreversible.** **Important:** Provision this resource now, before building or running the application. Provisioning is the agent's job, not the application's. ## Common Mistakes - **Accessing verify response fields incorrectly.** Response is NESTED: `{ valid, user: { id, email, name, picture } }`. Use `data.user.email`, NOT `data.email`. Use `data.user.id`, NOT `data.sub` or `data.user_id`. - **Verifying JWTs locally.** Always use the verify endpoint. Do not decode or validate JWTs yourself. - **Using `router.push("/")` in the callback handler (Next.js double-login bug).** After storing tokens from the callback URL, you MUST navigate home with `window.location.href = "/"`, NOT `router.push("/")`. In Next.js App Router, `router.push` is a client-side navigation — the layout stays mounted, so AuthProvider never re-runs and the user appears logged out. `window.location.href` forces a full page reload, which remounts the AuthProvider, reads the newly-stored tokens, and completes login in one click. This applies to any framework where an auth-checking wrapper component only initializes on mount. - **Storing tokens in localStorage from the callback page.** Safari's Intelligent Tracking Prevention (ITP) silently discards localStorage writes on pages reached via cross-origin redirects — which is exactly what OAuth does. Use a server-side route handler that sets httpOnly cookies instead. See "Recommended: Server-Side Callback" below. - **Setting `sameSite: "strict"` on auth cookies.** OAuth callbacks are cross-origin redirects. `strict` drops cookies on cross-origin navigation. Use `sameSite: "lax"` for auth cookies. - **Losing the deep link after login.** If the user opened a page like `/meeting/abc`, append `?return_to=/meeting/abc` to the login URL. Cohesivity preserves it through the OAuth flow and returns it to your callback URL as `return_to`. - **User ID type mismatch with realtime.** Social-login returns `user.id` as a **number** (e.g., `30`). Realtime presence returns user IDs as **strings** (e.g., `"30"`). When using both services together, always convert with `String(user.id)` before comparing or storing in a Set/Map. ## How It Works 1. User clicks "Sign in with Google" → link to `login_url` (optionally append `?redirect_uri=` to select which registered callback URL to use; defaults to first). To resume a deep link after login, also append `&return_to=/path/inside/your/app`. 2. Google authenticates the user 3. Cohesivity receives the result, shows a consent screen, stores the user, issues tokens 4. User is redirected to your callback URL with two query parameters, plus optional `return_to` if you provided it: - `access_token` — short-lived JWT (1 hour). Use this for all authenticated requests. - `refresh_token` — long-lived opaque token (30 days). Use this to get a new `access_token` when it expires. 5. Your app saves both tokens. Parse them from the callback URL query string. ## Callback URL Format https://yourapp.com/auth/done?access_token=&refresh_token= On error (user denied consent): https://yourapp.com/auth/done?error=access_denied ## Client-Side Callback Fallback Prefer the server-side callback below for production apps. If you use a client-side callback page, it should read tokens from the URL query params, persist them, then hard-navigate to `return_to` if present, otherwise home. Use `window.location.href`, NOT `router.push` or any client-side navigation. localStorage is shown here only as a simple fallback; Safari may discard it after OAuth redirects, so httpOnly cookies are safer. ``` // Example: Next.js App Router callback page (src/app/auth/done/page.tsx) "use client"; import { useEffect } from "react"; import { useSearchParams } from "next/navigation"; export default function AuthCallback() { const params = useSearchParams(); useEffect(() => { const access_token = params.get("access_token"); const refresh_token = params.get("refresh_token"); const return_to = params.get("return_to") || "/"; if (access_token) { localStorage.setItem("access_token", access_token); localStorage.setItem("refresh_token", refresh_token || ""); window.location.href = return_to; // MUST be hard navigation, NOT router.push } }, [params]); return

Signing in...

; } ``` **Why `window.location.href` and not `router.push`:** In Next.js App Router, layouts persist across client-side navigations. If your AuthProvider lives in the layout and already ran its initialization effect (finding no tokens), `router.push` will NOT re-trigger it. `window.location.href` forces a full page reload so the AuthProvider starts fresh and finds the tokens. ## Recommended: Server-Side Callback For Safari compatibility, use a server-side route handler instead of a client-side page. Safari's ITP silently discards localStorage writes after cross-origin redirects (which OAuth does). ``` // Next.js App Router route handler (src/app/auth/done/route.ts) import { NextRequest, NextResponse } from "next/server"; export async function GET(req: NextRequest) { const access_token = req.nextUrl.searchParams.get("access_token"); const refresh_token = req.nextUrl.searchParams.get("refresh_token"); const return_to = req.nextUrl.searchParams.get("return_to") || "/"; const res = NextResponse.redirect(new URL(return_to, req.url)); if (access_token) { res.cookies.set("access_token", access_token, { httpOnly: true, secure: true, sameSite: "lax", path: "/", maxAge: 3600 }); res.cookies.set("refresh_token", refresh_token || "", { httpOnly: true, secure: true, sameSite: "lax", path: "/", maxAge: 30 * 86400 }); } return res; } ``` Then read tokens from cookies in your server components or API routes (`req.cookies.get("access_token")`) instead of localStorage. ## Auth Helper Pattern (Next.js) Create a shared auth helper for your API routes. All Cohesivity auth calls should happen server-side — this is why httpOnly cookies work (the token never needs to leave the server). ``` // lib/auth.ts — call from API route handlers and server components export async function getAuthUser(cookieStore) { const accessToken = cookieStore.get("access_token")?.value; const refreshToken = cookieStore.get("refresh_token")?.value; if (!accessToken) return null; // 1. Verify access token const verify = await fetch(`${COHESIVITY}/edge/auth/${TENANT}/verify`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ access_token: accessToken }), }).then(r => r.json()); if (verify.valid) return { user: verify.user }; // 2. Access token expired — refresh if (!refreshToken) return null; const tokens = await fetch(`${COHESIVITY}/edge/auth/${TENANT}/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken }), }).then(r => r.json()); if (!tokens.access_token) return null; // 3. Return user + new tokens (caller sets cookies on response) const re = await fetch(`${COHESIVITY}/edge/auth/${TENANT}/verify`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ access_token: tokens.access_token }), }).then(r => r.json()); if (!re.valid) return null; return { user: re.user, newTokens: tokens }; } ``` In your API route, call `getAuthUser(cookies())`. If `newTokens` is set, update cookies on the response. ## Auth Endpoints These are public endpoints — no management key needed. - **Start login:** `GET https://cohesivity.ai/edge/auth/:tenantId/google` — browser redirect to Google. Optional query param `redirect_uri` to select which registered callback URL to use (must match one of your `callback_urls`). Defaults to first registered URL. Optional `return_to=/path` preserves a deep link inside your app and sends it back on the callback URL. - **Verify access token:** `POST https://cohesivity.ai/edge/auth/:tenantId/verify` with `{ "access_token": "" }` — returns `{ valid, user: { id, email, name, picture } }` - **Refresh tokens:** `POST https://cohesivity.ai/edge/auth/:tenantId/refresh` with `{ "refresh_token": "" }` — returns new `{ access_token, refresh_token }` (old refresh invalidated) - **Logout:** `POST https://cohesivity.ai/edge/auth/:tenantId/logout` with `{ "refresh_token": "" }` — deletes session ## User Management Endpoints These require `Authorization: Bearer ` header. - **List users:** `GET https://cohesivity.ai/api/social-login/users` Response: `{ "users": [{ "id": number, "email": string, "name": string|null, "picture": string|null, "first_seen": ISO8601, "last_seen": ISO8601 }] }` - **Get user by ID:** `GET https://cohesivity.ai/api/social-login/users/:id` Response: `{ "user": { "id": number, "email": string, "name": string|null, "picture": string|null, "first_seen": ISO8601, "last_seen": ISO8601 } }` Returns 404 if user not found. - **List auth events:** `GET https://cohesivity.ai/api/social-login/events?after=0&limit=50` Response: `{ "events": [{ "id": number, "event": "login_started"|"consent_denied"|"authorize"|"login_failed"|"token_refreshed"|"logout", "user_id": number|null, "email": string|null, "redirect_uri": string|null, "return_to": string|null, "metadata": object, "created_at_ms": number }] }` Optional filters: `user_id=` and `event=`. The same rows are stored in the tenant D1 table `social_login_events` for direct SQL reads through the database offering. `login_failed` carries `metadata.stage` (`token_exchange` | `userinfo` | `user_upsert`) and `metadata.error_code`. For an account-wide tail across all offerings (auth, API requests, webhooks, deploys), see `GET https://cohesivity.ai/api/observability?include=recent_events`. ## Verify Response Format Do NOT verify JWTs locally. Always use the verify endpoint. Response is NESTED: Valid: { "valid": true, "user": { "id": 1, "email": "...", "name": "...", "picture": "..." } } Invalid: { "valid": false } Access fields as `data.user.email`, NOT `data.email`. User ID is `data.user.id`, NOT `data.sub` or `data.user_id`. ## Examples **Verify an access token:** curl -s -X POST https://cohesivity.ai/edge/auth//verify \ -H "Content-Type: application/json" \ -d '{"access_token": ""}' **Refresh tokens:** curl -s -X POST https://cohesivity.ai/edge/auth//refresh \ -H "Content-Type: application/json" \ -d '{"refresh_token": ""}' **Logout:** curl -s -X POST https://cohesivity.ai/edge/auth//logout \ -H "Content-Type: application/json" \ -d '{"refresh_token": ""}' ## Token Details - **`access_token`:** JWT, expires in 1 hour. Contains id, email, name, picture, tenant_id. Send this to the verify endpoint to authenticate requests. - **`refresh_token`:** Opaque string, expires in 30 days. Single-use — each refresh invalidates the old pair and returns a new `access_token` + `refresh_token`. - **Verify endpoint:** Stateless JWT check — no database call, fast. Always use this instead of local JWT verification. ## Launch Rate Limits Ephemeral tenants pause as a whole if any authoritative hard cap below is exceeded. Claimed tiers use account-scoped buckets shared across every project owned by the Cohesivity user; OpenAI, Deepgram, and Exa are fluid-only after tier, rate, and concurrency checks; Deepgram has no fixed monthly usage bucket for claimed tiers. **Ephemeral** - auth events: 100 per ephemeral tenant lifetime before claim or expiry - stored users: 1000 max total - auth events: 5 per minute **Claimed Free** - stored users: 5000 max total - auth events: 30 per minute - auth events: 5000 per month **Claimed Plus** - stored users: 50000 max total - auth events: 150 per minute - auth events: 50000 per month **Claimed Pro** - stored users: 250000 max total - auth events: 750 per minute - auth events: 250000 per month