> ## Documentation Index
> Fetch the complete documentation index at: https://docs.steward.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth Providers

> Sign in with Google, Discord, or Twitter/X using OAuth with PKCE.

# OAuth Providers

Steward supports OAuth sign-in with **Google**, **Discord**, and **Twitter/X**. All flows use the PKCE (Proof Key for Code Exchange) extension for security, with no client secret exposed to the browser.

## Supported Providers

| Provider  | SDK Name    | Status      |
| --------- | ----------- | ----------- |
| Google    | `"google"`  | ✅ Supported |
| Discord   | `"discord"` | ✅ Supported |
| Twitter/X | `"twitter"` | ✅ Supported |

## How It Works (PKCE Flow)

```
1. SDK generates a random code_verifier + code_challenge (SHA-256)
2. Opens a popup to /auth/oauth/{provider}/authorize with the challenge
3. User authorizes in the provider's UI
4. Provider redirects back with an authorization code
5. SDK exchanges code + code_verifier at /auth/oauth/{provider}/token
6. Steward returns a JWT session
```

The popup-based flow keeps the user on your page while authentication happens in a separate window.

## SDK Usage

```typescript theme={null}
import { StewardAuth } from "@stwd/sdk";

const auth = new StewardAuth({
  baseUrl: "https://api.steward.fi",
  storage: localStorage,
});

// Opens a popup for the provider's sign-in page
const result = await auth.signInWithOAuth("google");
console.log(result);
// {
//   token: "eyJhbGci...",
//   refreshToken: "stwd_rt_...",
//   expiresIn: 900,
//   user: { id: "usr_...", email: "user@gmail.com", walletAddress: "0x..." },
//   provider: "google"
// }
```

### Configuration Options

```typescript theme={null}
const result = await auth.signInWithOAuth("discord", {
  redirectUri: "https://myapp.com/auth/callback", // custom callback URL
  tenantId: "my-tenant",                           // specific tenant
  popupWidth: 500,                                  // popup dimensions
  popupHeight: 600,
});
```

### Redirect Flow (Non-Popup)

For environments where popups are blocked, use the redirect flow:

```typescript theme={null}
// Step 1: Start the flow (this will throw with the authorization URL)
try {
  await auth.signInWithOAuth("google");
} catch (err) {
  // In non-browser environments, the error contains the redirect URL
  window.location.href = err.message.match(/Redirect to: (.+)/)?.[1];
}

// Step 2: On your callback page, complete the exchange
const params = Object.fromEntries(new URLSearchParams(window.location.search));
const result = await auth.handleOAuthCallback("google", params);
```

## React Usage

The `<StewardLogin>` component renders OAuth buttons based on which providers are enabled on the server:

```tsx theme={null}
<StewardLogin
  showGoogle    // enabled by default
  showDiscord   // enabled by default
  showPasskey={false}
  showEmail={false}
  title="Sign in"
/>
```

The component automatically queries `GET /auth/providers` to discover which OAuth providers are available. Buttons are only shown for providers that are both enabled on the server and not disabled via props.

## Setting Up OAuth Apps

### Google

1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
2. Create a new OAuth 2.0 Client ID (Web application)
3. Add authorized redirect URI:
   ```
   https://your-steward-url/auth/oauth/google/callback
   ```
4. Copy the Client ID and Client Secret

```bash theme={null}
# Server environment variables
GOOGLE_CLIENT_ID=xxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxx
```

### Discord

1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
2. Create a new application
3. Go to OAuth2 settings
4. Add redirect URI:
   ```
   https://your-steward-url/auth/oauth/discord/callback
   ```
5. Copy the Client ID and Client Secret

```bash theme={null}
DISCORD_CLIENT_ID=1234567890
DISCORD_CLIENT_SECRET=xxxx
```

### Twitter/X

1. Go to [Twitter Developer Portal](https://developer.twitter.com/en/portal)
2. Create a project and app
3. Enable OAuth 2.0 with PKCE
4. Add callback URL:
   ```
   https://your-steward-url/auth/oauth/twitter/callback
   ```
5. Copy the Client ID

```bash theme={null}
TWITTER_CLIENT_ID=xxxx
TWITTER_CLIENT_SECRET=xxxx
```

## Redirect URIs

All OAuth providers require a registered redirect URI. The pattern is:

```
https://your-steward-url/auth/oauth/{provider}/callback
```

For local development:

```
http://localhost:3200/auth/oauth/google/callback
http://localhost:3200/auth/oauth/discord/callback
http://localhost:3200/auth/oauth/twitter/callback
```

## API Endpoints

| Endpoint                           | Method | Description                                                       |
| ---------------------------------- | ------ | ----------------------------------------------------------------- |
| `/auth/providers`                  | GET    | Discover enabled auth methods                                     |
| `/auth/oauth/{provider}/authorize` | GET    | Start OAuth flow (redirect to provider)                           |
| `/auth/oauth/{provider}/callback`  | GET    | Provider redirect target                                          |
| `/auth/oauth/{provider}/token`     | POST   | Exchange code for session (PKCE)                                  |
| `/auth/device/code`                | POST   | Issue a short-lived device code and user code                     |
| `/auth/device/verify`              | POST   | Approve or deny a user code with an authenticated browser session |
| `/auth/device/token`               | POST   | Poll `urn:ietf:params:oauth:grant-type:device_code` for a session |

## Device Authorization Flow

For CLIs, TVs, and other input-constrained clients, Steward supports a bounded RFC 8628-style device flow. Device codes expire after 10 minutes, polling starts at a 5 second interval, rapid polling returns `slow_down`, and an approved code is consumed on first successful token exchange.

```typescript theme={null}
const auth = new StewardAuth({
  baseUrl: "https://api.steward.fi",
  tenantId: "my-tenant",
});

const issued = await auth.requestDeviceCode({
  clientId: "device-cli",
});

console.log(`Open ${issued.verification_uri} and enter ${issued.user_code}`);

let interval = issued.interval;
while (true) {
  await new Promise((resolve) => setTimeout(resolve, interval * 1000));
  const result = await auth.pollDeviceToken({
    deviceCode: issued.device_code,
    clientId: "device-cli",
  });
  if (result.ok) break;
  if (result.error === "slow_down" && result.interval) interval = result.interval;
  if (result.error === "access_denied" || result.error === "expired_token") {
    throw new Error(result.error);
  }
}
```

The verification UI should authenticate the user normally, then approve or deny the displayed code:

```typescript theme={null}
await auth.verifyDeviceCode("ABCD-2345", "approve");
```

When `clientId` is supplied, issuance requires an enabled tenant app client and token polling must present the same client id. Polling returns RFC-style errors: `authorization_pending`, `slow_down`, `access_denied`, `expired_token`, `invalid_client`, `invalid_request`, and `unsupported_grant_type`.

Native app clients can additionally send platform identifiers on device-flow
requests:

```bash theme={null}
X-Steward-Native-Bundle-Id: com.example.ios
X-Steward-Native-Package-Name: com.example.android
```

If either header or the matching JSON fields (`native_bundle_id`,
`native_package_name`, `nativeBundleId`, `nativePackageName`) is supplied,
Steward validates the identifier format and checks it against the enabled app
client's `allowedBundleIds` or `allowedPackageNames`. Accepted identifiers are
bound to the issued device code, so `/auth/device/token` must present the same
identifier. A different identifier returns `invalid_client` and leaves the
approval state unchanged. This is deterministic server allowlist enforcement;
full device attestation remains the responsibility of the OS, app store, or
attestation provider used by the native app.

Minimal iOS-bound device-code request:

```bash theme={null}
curl -X POST https://api.steward.fi/auth/device/code \
  -H "Content-Type: application/json" \
  -H "X-Steward-Native-Bundle-Id: com.example.ios" \
  -d '{
    "tenantId": "my-tenant",
    "client_id": "ios-prod"
  }'
```

The response echoes the accepted identifier using snake\_case:

```json theme={null}
{
  "device_code": "stwd_dc_...",
  "user_code": "ABCD-2345",
  "verification_uri": "https://app.steward.fi/auth/device",
  "expires_in": 600,
  "interval": 5,
  "client_id": "ios-prod",
  "native_bundle_id": "com.example.ios"
}
```

The token poll must repeat the same app client and native identifier, either as
headers or JSON fields:

```bash theme={null}
curl -X POST https://api.steward.fi/auth/device/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
    "device_code": "stwd_dc_...",
    "client_id": "ios-prod",
    "native_bundle_id": "com.example.ios"
  }'
```

## Security

* All flows use PKCE (S256 challenge method) to prevent authorization code interception
* State parameter prevents CSRF attacks
* Code verifier is stored client-side and never sent to the provider
* Popup polling includes a 5-minute timeout
* Device authorization codes are tenant/app-client bound, short-lived, rate-limited, and single-use
* Provider tokens are encrypted before storage; Steward sessions return short-lived access tokens and rotated refresh tokens

## Related

* [Auth Overview](/auth/overview) — Session management
* [Passkeys](/auth/passkeys) — Alternative passwordless method
* [Self-Hosting](/guides/self-hosting) — Environment variable reference
