# Sushii OAuth — integration guide for AI assistants

Use this document to integrate any application with **Sushii OAuth** (https://oauth.sushii.dev).
Sushii OAuth is a universal identity layer: your app redirects users to Sushii, they pick a
sign-in provider (Google, GitHub, Discord, Twitch, GitLab), and you receive standardized user
data. You do **not** need your own Google/GitHub/etc. OAuth apps — Sushii manages upstream
provider credentials.

---

## Quick checklist

1. Create a Sushii account at https://oauth.sushii.dev/signup
2. Open the developer console at https://oauth.sushii.dev/console
3. Create a project → save `client_id` and `client_secret` (secret shown once)
4. Register at least one **redirect URI** (must match exactly, character for character)
5. Choose allowed sign-in providers and data scopes on the project
6. Implement authorization code flow (steps below)
7. Exchange the code server-side at `POST /api/oauth/token`
8. (Optional) Configure a webhook URL for real-time `auth.completed` events

---

## Base URLs

| Environment | Base URL |
|-------------|----------|
| Production  | `https://oauth.sushii.dev` |
| Local dev   | `http://localhost:3000` |

All endpoints below are relative to the base URL.

---

## Credentials

After creating a project in the console you receive:

| Credential      | Format / notes |
|-----------------|----------------|
| `client_id`     | Public identifier, e.g. `sushii_xxxxxxxx` |
| `client_secret` | Secret string — shown **once** at creation. Store in env vars only. |

**Never** put `client_secret` in frontend code, mobile apps, or public repos.

### Required environment variables (integrator app)

```env
SUSHII_OAUTH_URL=https://oauth.sushii.dev
SUSHII_CLIENT_ID=sushii_xxxxxxxx
SUSHII_CLIENT_SECRET=your_client_secret
SUSHII_REDIRECT_URI=https://yourapp.com/auth/callback
```

For local development, typical redirect URI: `http://localhost:3000/auth/callback` or
`http://localhost:4000/callback` — register the exact URL in the console.

---

## OAuth 2.0 authorization code flow

### Step 1 — Redirect user to authorize

Send the user's browser to:

```
GET {BASE_URL}/oauth/authorize
```

Query parameters (all required):

| Parameter       | Value |
|-----------------|-------|
| `client_id`     | Your client ID |
| `redirect_uri`  | Must match a URI registered on the project **exactly** |
| `response_type` | Must be `code` |
| `state`         | Random CSRF token you generate and verify on callback |

Example:

```
https://oauth.sushii.dev/oauth/authorize?client_id=sushii_abc123&redirect_uri=https%3A%2F%2Fyourapp.com%2Fauth%2Fcallback&response_type=code&state=RANDOM_HEX_32_CHARS
```

Generate `state` with a cryptographically secure random string (e.g. 16+ bytes hex).
Store it in the user's session before redirecting.

### Step 2 — User consent

The user sees:

- Your project name
- Available sign-in methods (subset of providers you enabled on the project)
- A clear list of data fields your app will receive (based on configured data scopes)

They pick a provider and complete authentication with that upstream provider.

### Step 3 — Callback to your redirect URI

On success, the user is redirected to your `redirect_uri`:

```
{YOUR_REDIRECT_URI}?code=AUTHORIZATION_CODE&state=RANDOM_STATE
```

**Before trusting the callback:**

1. Verify `state` matches the value you stored in step 1
2. Read `code` from query string
3. Exchange `code` server-side immediately (see step 4)

Authorization codes expire in **5 minutes** and are **single-use**.

On failure upstream, you may receive `?error=...` instead of a code — handle gracefully.

### Step 4 — Token exchange (server-side only)

```
POST {BASE_URL}/api/oauth/token
Content-Type: application/x-www-form-urlencoded
```

Body (form-urlencoded):

```
grant_type=authorization_code
&code=AUTHORIZATION_CODE
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&redirect_uri=YOUR_REDIRECT_URI
```

The `redirect_uri` must be the **same** value used in step 1 and registered on the project.

JSON body is also accepted with the same fields.

#### Success response (200)

```json
{
  "access_token": "opaque_token_string",
  "token_type": "Bearer",
  "expires_in": 3600,
  "provider": "google",
  "user": {
    "email": "user@example.com",
    "name": "Jane Doe"
  }
}
```

The `user` object only contains fields allowed by your project's **data scopes** (see below).
`provider` is one of: `google`, `github`, `discord`, `twitch`, `gitlab`.

#### Error responses

| HTTP | `error` | Meaning |
|------|---------|---------|
| 400  | `invalid_request` | Missing parameters |
| 400  | `unsupported_grant_type` | `grant_type` must be `authorization_code` |
| 400  | `invalid_grant` | Bad/expired code or redirect URI mismatch |
| 401  | `invalid_client` | Wrong `client_id` or `client_secret` |
| 500  | `server_error` | Internal error |

---

## Data scopes

Configure which user fields your project receives. Set these when creating/editing a project
in the console.

| Scope ID       | Field in `user` | Description |
|----------------|-----------------|-------------|
| `email`        | `email`         | Verified email from the identity provider |
| `name`         | `name`          | Display name |
| `avatar`       | `avatar`        | Profile picture URL |
| `provider_id`  | `provider_id`   | Stable ID from the upstream provider |

Only requested scopes are included in `user`. Example with all scopes:

```json
{
  "email": "user@example.com",
  "name": "Jane Doe",
  "avatar": "https://...",
  "provider_id": "12345678"
}
```

---

## Sign-in providers

Available upstream providers (enable per project in console):

- `google`
- `github`
- `discord`
- `twitch`
- `gitlab`

Users choose one at the Sushii consent screen. You receive `provider` in the token response.

---

## Webhooks (optional)

If you set a **webhook URL** on the project, Sushii POSTs to it **immediately after**
authentication and **before** redirecting the user to your `redirect_uri`. Use this to
provision accounts server-side in real time.

### Request

```
POST {your_webhook_url}
Content-Type: application/json
X-Sushii-Timestamp: 1717934400
X-Sushii-Signature: hex_hmac_sha256
User-Agent: Sushii-OAuth-Webhook/1.0
```

Body:

```json
{
  "event": "auth.completed",
  "project_id": "uuid",
  "provider": "github",
  "user": { "email": "...", "name": "..." },
  "timestamp": "2026-06-09T12:00:00.000Z"
}
```

### Signature verification

```
signed_payload = timestamp + "." + raw_request_body
expected_signature = HMAC_SHA256(client_secret, signed_payload) as lowercase hex
```

Compare `expected_signature` to the `X-Sushii-Signature` header using a **timing-safe**
comparison.

Reject requests where `X-Sushii-Timestamp` is older than **5 minutes**.

### Node.js example

```javascript
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(rawBody, signature, timestamp, clientSecret) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (age > 300) return false;

  const signed = `${timestamp}.${rawBody}`;
  const expected = createHmac("sha256", clientSecret)
    .update(signed)
    .digest("hex");

  try {
    return timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(signature, "hex"),
    );
  } catch {
    return false;
  }
}
```

---

## Rate limits

- Default: **3,000 successful OAuth sign-ins per month** per project
- Resets on the **1st of each month (UTC)**
- Commercial project types may require admin approval for a higher limit
- View usage in the console
- When exceeded, users see an error on the authorize page (`rate_limit_exceeded`)

---

## Security requirements

1. Store `client_secret` in environment variables or a secrets manager only
2. Always validate `state` on callback (CSRF protection)
3. Exchange authorization codes **only on your backend**
4. Use **HTTPS** for redirect URIs and webhooks in production
5. Authorization codes and access tokens are time-limited and single-use
6. Register redirect URIs explicitly — no wildcards

---

## Minimal reference implementation (Node.js)

```javascript
import { randomBytes } from "crypto";
import { URLSearchParams } from "url";

const OAUTH_URL = process.env.SUSHII_OAUTH_URL;
const CLIENT_ID = process.env.SUSHII_CLIENT_ID;
const CLIENT_SECRET = process.env.SUSHII_CLIENT_SECRET;
const REDIRECT_URI = process.env.SUSHII_REDIRECT_URI;

// 1. Start login — redirect user here
export function loginUrl(state) {
  const params = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    response_type: "code",
    state,
  });
  return `${OAUTH_URL}/oauth/authorize?${params}`;
}

// 2. Handle callback — call from your /auth/callback route
export async function handleCallback(code) {
  const body = new URLSearchParams({
    grant_type: "authorization_code",
    code,
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    redirect_uri: REDIRECT_URI,
  });

  const res = await fetch(`${OAUTH_URL}/api/oauth/token`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body,
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.error_description ?? err.error);
  }

  return res.json(); // { access_token, token_type, expires_in, provider, user }
}

// Generate state before redirect
export function newState() {
  return randomBytes(16).toString("hex");
}
```

---

## Local test client (included in repo)

The repository includes `test-client/` — a local-only Node server for end-to-end testing.

```bash
cd test-client
cp .env.example .env
# Set CLIENT_ID, CLIENT_SECRET, OAUTH_URL
npm install
npm run dev
```

- Runs at http://localhost:4000
- Register redirect URI: `http://localhost:4000/callback`
- Saves every sign-in to `test-client/data.db` (SQLite)

---

## Console project settings summary

When creating a project, configure:

| Setting | Notes |
|---------|-------|
| Name | Shown to users on the consent screen |
| Type | Personal/open-source = instant. Commercial/SaaS = admin approval |
| Redirect URIs | One or more exact callback URLs |
| Allowed providers | Which sign-in buttons appear |
| Data scopes | Which `user` fields you receive |
| Webhook URL | Optional POST endpoint for `auth.completed` |

---

## API endpoints reference

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/oauth/authorize` | Start OAuth flow (browser redirect) |
| POST | `/api/oauth/token` | Exchange authorization code for user data |

Human-readable docs: https://oauth.sushii.dev/docs
Privacy policy: https://oauth.sushii.dev/privacy

---

## Prompt to give an AI

Copy and paste this when asking an AI to integrate Sushii OAuth:

```
Integrate Sushii OAuth into this app using the official integration guide:
https://oauth.sushii.dev/sushii-oauth-integration.md

Use authorization code flow. Store client_id and client_secret in env vars.
My redirect URI is: [YOUR_REDIRECT_URI]
My stack is: [e.g. Next.js App Router / Express / etc.]
```

Replace the bracketed values with your actual redirect URI and framework.
