# OAuth Providers Guide (Preconfigured + Built-in)

This repository has two provider layers:

1. Built-in provider constructors in `crates/api/src/plugins/oauth/providers.rs`
2. Providers preconfigured by the server binary in `src/bin/server.rs` (`build_auth`)

Provider IDs are used in API payloads such as `POST /sign-in/social` and `POST /link-social`.

For SDK method-to-route mapping details, see [Athena JS Auth SDK Bindings](./athena-js-auth-bindings.md).

## Current Provider Matrix

| Provider ID | Built-in constructor | Preconfigured in server binary | Notes |
|---|---|---|---|
| `google` | Yes | Yes | Enabled by default in `build_auth(...)`. |
| `apple` | Yes | Yes | Enabled by default in `build_auth(...)`. |
| `microsoft` | Yes | Yes | Enabled by default in `build_auth(...)`. |
| `github` | Yes | Yes (when credentials set) | Registered when `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` (or aliases) are non-empty. |
| `cloudflare` | Yes | Yes (when credentials set) | Registered when `CLOUDFLARE_CLIENT_ID` and a matching secret / publisher alias are non-empty. |
| `discord` | Yes | No | Available in code, not wired in server default plugin chain. |

## Generic OAuth Model

Each provider is represented by `OAuthProvider` with:

- `client_id`
- `client_secret`
- `auth_url`
- `token_url`
- `user_info_url`
- `scopes`
- `map_user_info` (maps provider JSON to internal `OAuthUserInfo`)

Flow shape for any provider:

1. `POST /sign-in/social` or `POST /link-social` with `provider`
2. Receive redirect URL from server
3. Provider redirects to `GET /callback/{provider}?code=...&state=...`
4. Server exchanges code for tokens, fetches user info, maps identity
5. Server creates/links account, creates session, returns token + session cookie

## Preconfigured Credentials (Server Binary Defaults)

These env vars are read by the current server preconfiguration:

| Provider | Client ID env var | Client secret env var | Notes |
|---|---|---|---|
| Google | `GOOGLE_CLIENT_ID` | `GOOGLE_CLIENT_SECRET` | Falls back to built-in defaults if unset. |
| Apple | `APPLE_CLIENT_ID` | `APPLE_CLIENT_SECRET` | Apple secret is typically a signed JWT client secret. |
| Microsoft | `MICROSOFT_CLIENT_ID` | `MICROSOFT_CLIENT_SECRET` | Legacy alias `MICROSOFT_SECRET` is accepted for compatibility. |
| GitHub | `GITHUB_CLIENT_ID` | `GITHUB_CLIENT_SECRET` | Aliases: `GITHUB_OAUTH_CLIENT_ID`, `GITHUB_OAUTH_CLIENT_SECRET`, `GITHUB_APP_CLIENT_ID`, `GITHUB_APP_CLIENT_SECRET`. Only registered when both id and secret are set. |
| Cloudflare | `CLOUDFLARE_CLIENT_ID` | `CLOUDFLARE_CLIENT_SECRET` | Aliases: `CLOUDFLARE_OAUTH_CLIENT_ID`, `CLOUDFLARE_OAUTH_CLIENT_SECRET`, `CLOUDFLARE_OAUTH_CLIENT_PUBLISHER`, `cloudflare_oauth_client_publisher`. Only registered when both id and secret-equivalent value are set. |

Resolution order for each configured credential:

1. Primary env var
2. Alias env var(s) where documented
3. Built-in fallback value in server source (Google/Apple/Microsoft only)

**Not used for social OAuth sign-in:** `GITHUB_ACCESS_TOKEN`, `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_SLUG`, `GITHUB_WEBHOOK_SECRET` (GitHub App / webhook automation — separate from OAuth App login).

Env examples are in `.env.example`.

## Callback Route Convention

Callbacks are provider-specific:

- `GET /callback/google`
- `GET /callback/apple`
- `GET /callback/microsoft`
- `GET /callback/github` (when GitHub credentials are configured)
- `GET /callback/cloudflare` (when Cloudflare credentials are configured)
- `GET /callback/discord` (if you wire Discord)

Default callback URL when `callbackURL` is omitted (social sign-in / link-social):

1. `{PROVIDER}_CALLBACK_URL` env var if set (e.g. `GITHUB_CALLBACK_URL=https://xbp.app/api/callback/github,https://xbp.app/`). Comma-separated values are supported; the first non-empty URL is used as the default `redirect_uri`.
2. Otherwise `AuthConfig::public_auth_url("callback/{provider}")` — combines `BASE_URL` with `PUBLIC_AUTH_BASE_PATH` (or `AUTH_BASE_PATH` fallback)

Examples with `BASE_URL=https://xbp.app` and `PUBLIC_AUTH_BASE_PATH=/api`:

- `https://xbp.app/api/callback/github`
- `https://xbp.app/api/callback/cloudflare`

Examples with default `PUBLIC_AUTH_BASE_PATH=/api/auth`:

- `https://xbp.app/api/auth/callback/google`

Register the same URL in your OAuth provider app (GitHub OAuth App → Authorization callback URL).

Local examples (no public path override):

- `http://localhost:3000/callback/google`
- `http://localhost:3000/callback/github`
- `http://localhost:3000/callback/cloudflare`

## Managed Callback URL Registry

For app-managed social-login callbacks, Athena Auth now exposes an admin OAuth client registry:

- `POST /admin/oauth-client/create`
- `GET /admin/oauth-client/list`

These records are used to allowlist explicit `callbackURL` values passed to:

- `POST /sign-in/social`
- `POST /link-social`

Behavior:

1. If `callbackURL` is omitted, Athena Auth uses `{PROVIDER}_CALLBACK_URL` or `public_auth_url("callback/{provider}")`.
2. If `callbackURL` is provided, it must match an active OAuth client entry for the same `provider`.
3. The OAuth client registry does not turn Athena Auth into a full OAuth authorization server; it is a managed callback/redirect allowlist around the existing social-provider flow.

## Rejections and Removed Behavior

The social OAuth routes now reject these cases:

- `POST /sign-in/social` with a non-empty `callbackURL` that is not registered for the same `provider`
- `POST /link-social` with a non-empty `callbackURL` that is not registered for the same `provider`
- admin OAuth client creation with blank or duplicate redirect URIs after normalization

The previously permissive behavior of passing arbitrary third-party `callbackURL` values to social OAuth routes should be treated as deprecated and removed. Callers must register callback URLs first.

Unchanged behavior:

- omitting `callbackURL` on social OAuth still uses `{PROVIDER}_CALLBACK_URL` or `public_auth_url("callback/{provider}")`
- email verification, password reset, and other non-social callback flows are unchanged by this registry

## SDK Impact

Payload shape for social OAuth is unchanged:

- `POST /sign-in/social`
- `POST /link-social`

Both still accept:

- `provider`
- `callbackURL`
- `scopes`

But SDK callers now need to do this when they send a custom `callbackURL`:

1. Create an active OAuth client entry with `provider` and `redirectUris`
2. Reuse one of those exact registered `redirectUris` in the social sign-in or link request

No new provider-callback handler method is required in SDKs. The upstream provider callback into Athena Auth remains `public_auth_url("callback/{provider}")` unless `{PROVIDER}_CALLBACK_URL` overrides it.

### GitHub OAuth App setup (xbp.app)

1. In the GitHub OAuth App settings, register these **Authorization callback URLs** (GitHub supports multiple):
   - `https://xbp.app/api/callback/github` (Athena Auth server callback; must match the first entry in `GITHUB_CALLBACK_URL` or `public_auth_url` output)
   - `https://xbp.app/` (homepage / app root callback, if used by your client)
2. Set **Homepage URL** to `https://xbp.app`.
3. Set `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` in Worker secrets / `.dev.vars`.

## Provider Catalog (From Current Code)

### Google

- Auth URL: `https://accounts.google.com/o/oauth2/v2/auth`
- Token URL: `https://oauth2.googleapis.com/token`
- User info URL: `https://www.googleapis.com/oauth2/v3/userinfo`
- Scopes: `openid email profile`
- Mapping:
  - `id` <- `sub`
  - `email` <- `email`
  - `name` <- `name`
  - `image` <- `picture`
  - `email_verified` <- `email_verified` (bool, default `false`)

### Apple

- Auth URL: `https://appleid.apple.com/auth/authorize`
- Token URL: `https://appleid.apple.com/auth/token`
- User info URL: `https://appleid.apple.com/auth/userinfo`
- Scopes: `openid email name`
- Mapping:
  - `id` <- `sub`
  - `email` <- `email`
  - `name` <- `name`, or fallback `given_name + family_name`
  - `email_verified` <- bool or string `"true"/"false"` (default `false`)

### Microsoft

- Auth URL: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize`
- Token URL: `https://login.microsoftonline.com/common/oauth2/v2.0/token`
- User info URL: `https://graph.microsoft.com/v1.0/me`
- Scopes: `openid profile email offline_access User.Read`
- Mapping:
  - `id` <- `id`
  - `email` <- `mail`, fallback `userPrincipalName`
  - `name` <- `displayName`, fallback `givenName + surname`
  - `email_verified` <- forced `true` in current mapper

### GitHub (Preconfigured when credentials are set)

- Auth URL: `https://github.com/login/oauth/authorize`
- Token URL: `https://github.com/login/oauth/access_token`
- User info URL: `https://api.github.com/user` (falls back to `/user/emails` when email is private)
- Scopes: `user:email`
- Mapping:
  - `id` <- numeric/string `id`
  - `email` <- `email` (may be resolved via `/user/emails`)
  - `name` <- `name`
  - `image` <- `avatar_url`
  - `email_verified` <- forced `true` in current mapper

### Cloudflare (Preconfigured when credentials are set)

- Auth URL: `https://dash.cloudflare.com/oauth2/auth`
- Token URL: `https://dash.cloudflare.com/oauth2/token`
- User info URL: `https://dash.cloudflare.com/oauth2/userinfo`
- Scopes: `openid email profile`
- Mapping:
  - `id` <- `sub`
  - `email` <- `email`
  - `name` <- `name`, fallback `preferred_username`
  - `image` <- `picture`
  - `email_verified` <- `email_verified` (bool, default `false`)

### Discord (Built-in, Not Preconfigured by Default)

- Auth URL: `https://discord.com/api/oauth2/authorize`
- Token URL: `https://discord.com/api/oauth2/token`
- User info URL: `https://discord.com/api/users/@me`
- Scopes: `identify email`
- Mapping:
  - `id` <- `id`
  - `email` <- `email`
  - `name` <- `username`
  - `image` <- derived from avatar hash
  - `email_verified` <- `verified` (bool, default `false`)

## Wiring Discord in Server Defaults

If you want `discord` preconfigured like the others, extend the server plugin chain in `build_auth(...)`:

```rust
let oauth_plugin = OAuthPlugin::new()
    .add_provider("google", OAuthProvider::google(&google_client_id, &google_client_secret))
    .add_provider("apple", OAuthProvider::apple(&apple_client_id, &apple_client_secret))
    .add_provider(
        "microsoft",
        OAuthProvider::microsoft(&microsoft_client_id, &microsoft_client_secret),
    )
    .add_provider("github", OAuthProvider::github(&github_client_id, &github_client_secret))
    .add_provider(
        "cloudflare",
        OAuthProvider::cloudflare(&cloudflare_client_id, &cloudflare_client_secret),
    )
    .add_provider("discord", OAuthProvider::discord(&discord_client_id, &discord_client_secret));
```

Recommended env var names for Discord:

- `DISCORD_CLIENT_ID`
- `DISCORD_CLIENT_SECRET`

GitHub is already wired in `build_auth(...)` when `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` (or documented aliases) are set.
Cloudflare is already wired in `build_auth(...)` when `CLOUDFLARE_CLIENT_ID` and `CLOUDFLARE_CLIENT_SECRET` (or documented aliases) are set.

## Generic Request Examples

Start sign-in:

```bash
curl -X POST http://localhost:3000/sign-in/social \
  -H "Content-Type: application/json" \
  -d '{"provider":"github"}'
```

Link provider for an authenticated user:

```bash
curl -X POST http://localhost:3000/link-social \
  -H "Content-Type: application/json" \
  -H "Cookie: session_token=<token>" \
  -d '{"provider":"discord"}'
```

## Operational Notes

- Provider credential changes require a process restart.
- If callback routing fails, validate `BASE_URL`, provider callback registration, and any reverse proxy/base path setup.
