OAuth API reference

API reference for OAuth/SSO provider authentication.

API reference for OAuth/SSO provider authentication.

Overview

Volcano supports OAuth 2.0 authentication with major identity providers:

  • Google
  • GitHub
  • Microsoft (Azure AD / Personal accounts)
  • Apple Sign In

Before upgrading an existing redirect-based integration, register every callback URL in the project's allowed_redirect_urls. Authorization and in-flight callbacks fail closed when the allowlist is empty or the exact scheme, host, port, path, and query do not match. Flows that omit redirect_url and receive the token response as JSON are unchanged.

Endpoints

Public OAuth flow

Start OAuth authorization

GET /auth/oauth/{provider}/authorize

Redirects user to OAuth provider for authorization.

Path Parameters:

  • project_id - UUID of your project
  • provider - One of: google, github, microsoft, apple

Query Parameters:

  • anon_key - Project anon key (required)
  • redirect_url - URL to redirect to after OAuth completes. Must exactly match an entry in the project's allowed_redirect_urls, including the query string (see Managed hosted pages), or be the project's own managed hosted-auth page URL. Omit redirect_url to get the token response as JSON from the callback instead of a redirect.
  • client_state - Optional application nonce that is echoed after the callback
  • response_mode - Set to code to return a short-lived code to the registered redirect_url, then exchange it at POST /auth/oauth/exchange

Response: 307 Temporary Redirect to OAuth provider

Errors:

  • 404 - OAuth provider not configured
  • 400 - OAuth provider is disabled
  • 400 - redirect_url is not registered in allowed_redirect_urls

Example:

curl -i \
  'http://localhost:8000/auth/oauth/google/authorize?anon_key=<anon_key>&redirect_url=https%3A%2F%2Fround-lake.dustinice.workers.dev%3A443%2Fhttp%2Flocalhost%3A3000%2Fcallback&response_mode=code'

Handle OAuth callback

GET /auth/oauth/{provider}/callback?code={code}&state={state}

OAuth provider calls this endpoint after user authorization.

Path Parameters:

  • project_id - UUID of your project
  • provider - OAuth provider used

Query Parameters:

  • code - Authorization code from provider (required)
  • state - CSRF protection state (required)
  • error - Error code if authorization failed

Response:

  • 303 See Other - Registered browser redirect with a single-use code and optional application state; no session tokens are placed in the URL
  • 200 OK - Existing user signed in when redirect_url was omitted
  • 201 Created - New user signed in when redirect_url was omitted

Response Body:

{
  "access_token": "eyJhbGci...",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "refresh_abc123...",
  "user": {
    "id": "uuid",
    "email": "user@example.com",
    "email_confirmed": true,
    "raw_user_meta_data": {
      "name": "John Doe",
      "picture": "https://...",
      "provider": "google",
      "email_verified": true
    },
    "status": "active",
    "created_at": "2026-01-03T10:00:00Z"
  }
}

Errors:

  • 400 - Missing/invalid code or state
  • 400 - State parameter expired (10 min timeout)
  • 400 - OAuth provider is disabled: the provider was turned off after the sign-in or link started (re-checked at callback time)
  • 400 - the redirect URL stored with this flow is no longer registered in allowed_redirect_urls (re-checked at callback time, in case the allowlist changed after authorization started)
  • 404 - OAuth provider not configured: the provider's configuration was removed after the sign-in or link started
  • 404 - user not found: the account a link started from was deleted before the callback
  • 403 - the provider's email domain is not in allowed_email_domains. Creating an account is refused under signup and signup_and_signin; signing in an already-linked account is refused under signup_and_signin, as it is when the account's canonical email is outside the list. A new account at a reserved domain (anonymous.volcano.internal, volcano.local) is refused in every project. See Email Domain Allowlist
  • 409 - Email already exists (requires linking)

Exchange callback code

POST /auth/oauth/exchange
Authorization: Bearer {anon_key}
Content-Type: application/json

{
  "code": "{callback_code}",
  "redirect_url": "https://yourapp.com/auth/callback"
}

Atomically consumes the short-lived callback code and returns the AuthTokenResponse shown above. The project and exact redirect URL must match the authorization request. A replay, expired code, or redirect mismatch returns 400 Bad Request.

The code outlives the callback that issued it, so email confirmation and the email domain policy are both re-checked here: either returns 403 Forbidden.


User OAuth management

List linked providers

GET /auth/oauth/providers
Authorization: Bearer {access_token}

Get list of OAuth providers linked to current user, with the user's own account ID at each provider.

Headers:

  • Authorization - Bearer token (user's access token)

Response:

{
  "providers": [
    {
      "provider": "google",
      "provider_user_id": "108123456789012345678",
      "linked_at": "2026-01-03T10:00:00Z",
      "updated_at": "2026-01-03T10:00:00Z"
    },
    {
      "provider": "github",
      "provider_user_id": "583231",
      "provider_login": "octocat",
      "linked_at": "2026-01-05T14:30:00Z",
      "updated_at": "2026-01-05T14:30:00Z"
    }
  ]
}
FieldDescription
provider_user_idThe user's account ID at the provider. It never changes, so match on it. For GitHub it's the numeric user ID, as in a webhook's sender.id.
provider_loginThe login when the account was linked, such as a GitHub username. It isn't updated after a rename, so display it but don't match on it. Omitted for providers without logins.

Errors:

  • 401 - Not authenticated or invalid token

POST /auth/oauth/{provider}/link
Authorization: Bearer {access_token}

Link OAuth provider to current authenticated user.

Path Parameters:

  • provider - Provider to link: google, github, microsoft, apple

Query Parameters (Optional):

  • redirect_url - URL to redirect to after linking completes. Same allowed_redirect_urls requirement as the authorize endpoint above.

Headers:

  • Authorization - Bearer token (user's access token)

Response:

{
  "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?..."
}

User should be redirected to authorization_url. After authorization, provider will be linked.

Errors:

  • 401 - Not authenticated
  • 404 - OAuth provider not configured
  • 409 - Provider already linked
  • 400 - Provider is disabled
  • 400 - redirect_url is not registered in allowed_redirect_urls

DELETE /auth/oauth/{provider}/unlink
Authorization: Bearer {access_token}

Remove OAuth provider from user's account.

Path Parameters:

  • provider - Provider to unlink

Headers:

  • Authorization - Bearer token (user's access token)

Response: 204 No Content

Errors:

  • 401 - Not authenticated
  • 404 - Provider not linked
  • 400 - Cannot unlink last authentication method

Security: Users cannot remove their last way to sign in. They must keep another linked OAuth provider that's enabled for the project, or a password while email/password is on for the project (enable_email_password). See Last authentication method.


Admin OAuth configuration

List OAuth configurations

GET /projects/{project_id}/oauth/configs
Authorization: Bearer {platform_token}

List all OAuth configurations for a project.

Headers:

  • Authorization - Bearer token (platform admin token)

Response:

{
  "configs": [
    {
      "id": "uuid",
      "provider": "google",
      "client_id": "123456.apps.googleusercontent.com",
      "enabled": true,
      "redirect_url": "https://yourapp.com/callback",
      "scopes": ["openid", "email", "profile"],
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  ]
}

Note: client_secret is NOT included for security.


Get OAuth configuration

GET /projects/{project_id}/oauth/configs/{provider}
Authorization: Bearer {platform_token}

Get specific OAuth configuration.

Response:

{
  "id": "uuid",
  "provider": "google",
  "client_id": "123456.apps.googleusercontent.com",
  "client_secret": "GOCS...****", // Masked
  "enabled": true,
  "redirect_url": "https://yourapp.com/callback",
  "scopes": ["openid", "email", "profile"],
  "created_at": "2026-01-01T00:00:00Z",
  "updated_at": "2026-01-01T00:00:00Z"
}

Note: Secret is masked (first 4 + last 4 chars shown).


Create OAuth configuration

POST /projects/{project_id}/oauth/configs
Authorization: Bearer {platform_token}
Content-Type: application/json

{
  "provider": "google",
  "client_id": "123456.apps.googleusercontent.com",
  "client_secret": "GOCSPX-your-secret",
  "redirect_url": "https://yourapp.com/auth/callback",
  "scopes": ["openid", "email", "profile"]
}

Request Fields:

  • provider - Required: google, github, microsoft, apple, device
  • client_id - Required: Client identifier
  • client_secret - Required for non-device providers, not used for device
  • redirect_url - Required for non-device providers, not used for device
  • scopes - Optional for non-device providers (uses defaults), not used for device

For provider=device (RFC8628 CLI/device flow), a minimal payload is:

{
  "provider": "device",
  "client_id": "myapp-cli"
}

Custom verification page: the device-approval page is configured on the project's auth config, not on the device client. Set device_verification_url via PATCH /auth/config to point device logins at your own RFC 8628 page; otherwise the managed device page is used. See Auth endpoints and Custom verification page.

Response: 201 Created with OAuth configuration (secret masked)

Errors:

  • 400 - Invalid provider or redirect URL
  • 409 - Provider already configured

Redirect URL validation:

URLValidReason
https://yourapp.com/callbackYesHTTPS with valid domain
http://localhost:8080/callbackYesDevelopment only
http://evil.com@yourapp.com/callbackNoContains @ character
javascript:alert(1)NoInvalid protocol

Update OAuth configuration

PUT /projects/{project_id}/oauth/configs/{provider}
Authorization: Bearer {platform_token}
Content-Type: application/json

{
  "client_id": "new-client-id",
  "client_secret": "new-secret",
  "redirect_url": "https://newapp.com/callback",
  "scopes": ["openid", "email"],
  "enabled": false
}

All fields are optional. Only provided fields will be updated.

For provider=device, use query parameter client_id to identify which device client to update:

PUT /projects/{project_id}/oauth/configs/device?client_id=myapp-cli

Device clients only support updating:

  • client_id
  • enabled

client_secret, redirect_url, and scopes are not supported for provider=device.

Response: 200 OK with updated configuration


Delete OAuth configuration

DELETE /projects/{project_id}/oauth/configs/{provider}
Authorization: Bearer {platform_token}

Remove OAuth configuration for a provider.

For provider=device, use query parameter client_id to identify which device client to delete:

DELETE /projects/{project_id}/oauth/configs/device?client_id=myapp-cli

Response: 204 No Content


List available providers

GET /projects/{project_id}/oauth/providers
Authorization: Bearer {platform_token}

Get list of supported OAuth providers and their default scopes.

Response:

{
  "providers": [
    {
      "id": "google",
      "name": "Google",
      "default_scopes": ["openid", "email", "profile"]
    },
    {
      "id": "github",
      "name": "GitHub",
      "default_scopes": ["read:user", "user:email"]
    },
    {
      "id": "microsoft",
      "name": "Microsoft",
      "default_scopes": ["openid", "email", "profile"]
    },
    {
      "id": "apple",
      "name": "Apple",
      "default_scopes": ["email", "name"]
    }
  ]
}

Security

CSRF protection

OAuth flow uses state parameter to prevent CSRF attacks:

  • State is randomly generated (32 bytes, base64 encoded)
  • State is verified in callback
  • State expires after 10 minutes
  • State is one-time use only

Open redirect prevention

Redirect URLs are validated:

  • Must use http or https protocol
  • Cannot contain @ character (prevents http://evil.com@yourapp.com)
  • No wildcards allowed

Client secret protection

  • Secrets are encrypted with AES-256-GCM before database storage
  • Automatic encryption/decryption in database layer
  • Secrets are masked in API responses
  • Secrets not included in list endpoints
  • Only first 4 and last 4 characters shown in GET requests
  • Requires ENCRYPTION_KEY environment variable (32 bytes)

Email conflict handling

If OAuth email already exists:

  • Returns 409 Conflict error
  • User must sign in with password
  • User can then link OAuth provider via /link endpoint
  • Prevents account takeover

Last authentication method

An unlink that would leave the account no way to sign in is refused with 400. What counts as a way to sign in:

  • Another linked OAuth provider that's enabled for the project
  • A password, while email/password is on for the project (enable_email_password)

A guest (anonymous) login doesn't count. To unlink a user's only provider, link another provider first, or set a password while email/password is on.

Provider differences

Google

  • User ID: Numeric string
  • Email: Always provided
  • Email verified: Yes
  • Scopes: openid, email, profile

GitHub

  • User ID: Numeric
  • Email: Might be private (user setting)
  • Email verified: Assumed true if provided
  • Scopes: read:user, user:email

Microsoft

  • User ID: UUID
  • Email: From mail or userPrincipalName
  • Email verified: Assumed true
  • Works with: Personal accounts AND Azure AD
  • Scopes: openid, email, profile

Apple

  • User ID: Stable subject identifier
  • Email: Might be Apple relay address
  • Email verified: Explicitly provided
  • Name: Only sent on first authorization
  • Scopes: email, name

Error codes

CodeMeaningSolution
400Bad request (invalid params)Check request parameters
401Not authenticatedProvide valid access token
404Provider not configuredConfigure OAuth provider first
409Conflict (duplicate)Provider already exists or email conflict

Examples

See complete examples:

On this page