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

# Authentication

> JWT sessions, OAuth providers, email 2FA, external API keys, and desktop pairing for OpenSail

<img src="https://mintcdn.com/tesslate/VT6tbZolrCfpx26M/images/opensail-banner.png?fit=max&auto=format&n=VT6tbZolrCfpx26M&q=85&s=66579c47537b3464fb65b229cc9ab0fd" alt="Tesslate OpenSail" width="4712" height="1612" data-path="images/opensail-banner.png" />

## Overview

OpenSail supports five authentication paths, all enforced by the orchestrator:

1. Password (bcrypt) with optional email 2FA
2. OAuth login (GitHub, Google)
3. OAuth connection for third-party services (deployments, git providers, MCP platform apps)
4. External API keys (`tsk_*`) for the external agent API and gateway
5. Desktop pairing (`tsk_*` exchanged via deep link and stored in Stronghold)

Sessions use short-lived JWT access tokens with refresh token rotation. CSRF protection is a double-submit cookie pattern. Secrets at rest (OAuth tokens, channel credentials, deployment credentials) are encrypted with Fernet.

## Password and email 2FA

<Steps>
  <Step title="Sign up">
    `POST /api/auth/register` with `email`, `username`, `password`. The orchestrator hashes the password with bcrypt and returns an access token. The first account on a fresh install is promoted to admin.
  </Step>

  <Step title="Log in">
    `POST /api/auth/login` with username or email and password. Returns an access token (default 30 min) and sets a refresh cookie (default 14 days).
  </Step>

  <Step title="Email 2FA (optional)">
    When `TWO_FA_ENABLED=true`, login triggers `POST /api/auth/2fa/send` which emails a 6-digit code. Submit with `POST /api/auth/2fa/verify`. Codes are Argon2 hashed, limited to 5 attempts, and expire in minutes.
  </Step>

  <Step title="Refresh">
    Access tokens renew automatically via the refresh cookie. No explicit refresh call is needed from the client.
  </Step>
</Steps>

Required environment variables for email flows: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_SENDER_EMAIL`, `SMTP_USE_TLS`.

## OAuth login providers

Two providers ship out of the box. Both are optional; absence of credentials disables the provider gracefully.

<Tabs>
  <Tab title="GitHub">
    Scopes: `user:email`, `read:user`.

    | Variable                    | Purpose                                                       |
    | --------------------------- | ------------------------------------------------------------- |
    | `GITHUB_CLIENT_ID`          | Client ID from github.com/settings/developers                 |
    | `GITHUB_CLIENT_SECRET`      | Client secret                                                 |
    | `GITHUB_OAUTH_REDIRECT_URI` | Must match the OAuth app exactly, including protocol and path |

    Local dev callback: `http://localhost/api/auth/github/callback`.
  </Tab>

  <Tab title="Google">
    Scopes: `openid`, `email`, `profile`.

    | Variable                    | Purpose                              |
    | --------------------------- | ------------------------------------ |
    | `GOOGLE_CLIENT_ID`          | OAuth 2.0 client ID                  |
    | `GOOGLE_CLIENT_SECRET`      | Client secret                        |
    | `GOOGLE_OAUTH_REDIRECT_URI` | Must match the Cloud Console setting |

    Local dev callback: `http://localhost/api/auth/google/callback`.
  </Tab>
</Tabs>

<Note>
  Multiple providers can link to the same account. If the OAuth email matches an existing user, providers attach automatically.
</Note>

## OAuth for deployment targets and git

Connect providers for push-based deployments and private repo import. Configure client IDs and secrets under `Settings -> Integrations`. Tokens are Fernet-encrypted before being stored in `DeploymentCredential`.

| Provider     | Env variables                                                                             |
| ------------ | ----------------------------------------------------------------------------------------- |
| Vercel       | `VERCEL_CLIENT_ID`, `VERCEL_CLIENT_SECRET`, `VERCEL_OAUTH_REDIRECT_URI`                   |
| Netlify      | `NETLIFY_CLIENT_ID`, `NETLIFY_CLIENT_SECRET`, `NETLIFY_OAUTH_REDIRECT_URI`                |
| Heroku       | `HEROKU_CLIENT_ID`, `HEROKU_CLIENT_SECRET`, `HEROKU_OAUTH_REDIRECT_URI`                   |
| DigitalOcean | `DIGITALOCEAN_CLIENT_ID`, `DIGITALOCEAN_CLIENT_SECRET`, `DIGITALOCEAN_OAUTH_REDIRECT_URI` |

Git providers (GitHub, GitLab, Bitbucket) are optional. Without them you can still import public repos.

## External API keys

The external agent API lets machine users invoke agents with a bearer token. Keys are minted from `Settings -> API Keys` (`POST /api/external-api-keys`). They are SHA-256 hashed before storage (`ExternalAPIKey`). Only the newly minted secret is ever shown.

<Steps>
  <Step title="Mint a key">
    Visit `Settings -> API Keys` and click **Create key**. Name it, pick scope (project-scoped or team-scoped), and copy the `tsk_*` secret immediately.
  </Step>

  <Step title="Invoke the agent">
    `POST /api/external/agent/invoke` with `Authorization: Bearer tsk_...`. Returns `task_id` and `events_url` right away.
  </Step>

  <Step title="Stream events">
    Subscribe to `GET /api/external/agent/events/{task_id}` (SSE) or poll `GET /api/external/agent/status/{task_id}`.
  </Step>

  <Step title="Revoke">
    Delete the key in the UI or `DELETE /api/external-api-keys/{id}`. The hash is wiped; in-flight tasks abort on their next iteration.
  </Step>
</Steps>

See the [external agent API guide](/guides/api-keys) for scopes, rate limits, and webhook callbacks.

## Desktop pairing

The desktop app can pair to a cloud instance (tesslate.com or your own self-hosted cluster). Pairing unlocks the marketplace, bidirectional sync, and the remote K8s runtime.

<Steps>
  <Step title="Mint a pairing key">
    In the cloud instance, `Settings -> API Keys -> Create desktop key`. This returns a short-lived deep-link URL (`opensail://pair?token=...`).
  </Step>

  <Step title="Open the link">
    Click the URL on the machine running the desktop app. The Tauri deep-link handler exchanges the token for a long-lived `tsk_*` key.
  </Step>

  <Step title="Store in Stronghold">
    The key is written to the Tauri Stronghold vault (encrypted on disk with a per-machine passphrase). The sidecar never sees the plaintext key at rest; it is injected into the per-request Authorization header by the Rust shell.
  </Step>

  <Step title="Verify">
    `GET /api/desktop/session/ping` confirms pairing. The tray icon turns green.
  </Step>
</Steps>

Every cloud call made by the desktop app uses this key. Unpair by deleting it in the desktop `Settings` page, which also revokes it on the cloud side.

## Session management

<AccordionGroup>
  <Accordion icon="clock" title="Session duration">
    Access tokens default to 30 minutes (`ACCESS_TOKEN_EXPIRE_MINUTES`). Refresh tokens default to 14 days (`REFRESH_TOKEN_EXPIRE_DAYS`). Refresh is automatic while the browser tab is open.
  </Accordion>

  <Accordion icon="cookie" title="Cookies">
    Production sets `COOKIE_SECURE=true`, `COOKIE_SAMESITE=lax`, and `COOKIE_DOMAIN=.your-apex.tld` so subdomain project URLs authenticate cleanly.
  </Accordion>

  <Accordion icon="arrow-right-from-bracket" title="Logging out">
    `POST /api/auth/logout` clears the refresh cookie and invalidates the server-side refresh record.
  </Accordion>
</AccordionGroup>

## Security model

| Concern          | Mechanism                                                                                                     |
| ---------------- | ------------------------------------------------------------------------------------------------------------- |
| Password storage | bcrypt with per-user salt                                                                                     |
| 2FA codes        | Argon2 hashed, 5-attempt cap, minute TTL                                                                      |
| Refresh rotation | One-time-use refresh tokens, detected reuse invalidates the session                                           |
| Secrets at rest  | Fernet (`CHANNEL_ENCRYPTION_KEY`, `DEPLOYMENT_ENCRYPTION_KEY`, falls back to a key derived from `SECRET_KEY`) |
| CSRF             | Double-submit cookie pattern with `X-CSRF-Token` header                                                       |
| OAuth state      | Signed, single-use state token; mismatches abort the flow                                                     |
| API keys         | `tsk_*` prefix, SHA-256 hashed in DB, shown once at creation                                                  |
| Desktop pairing  | Short-lived deep-link token; long-lived key lives in Stronghold                                               |

<Warning>
  Rotating `SECRET_KEY` invalidates existing JWTs and may make Fernet-encrypted data unrecoverable unless you also rotate `CHANNEL_ENCRYPTION_KEY` and `DEPLOYMENT_ENCRYPTION_KEY` in sync.
</Warning>

## Troubleshooting

<AccordionGroup>
  <Accordion icon="circle-exclamation" title="Forgot password">
    Click **Forgot Password** on login, enter your email, and follow the link. Resets require a working SMTP configuration.
  </Accordion>

  <Accordion icon="ban" title="Cannot log in">
    Check the caps-lock state, try username instead of email, and check the email inbox (and spam) for a 2FA code if `TWO_FA_ENABLED=true`. Browser cache flushes often resolve stale cookies.
  </Accordion>

  <Accordion icon="key" title="OAuth redirect error">
    The OAuth app's callback URL must exactly match `*_OAUTH_REDIRECT_URI`, including protocol (`http` vs `https`) and path.
  </Accordion>

  <Accordion icon="rotate" title="Session expired">
    Just log in again. Data is safe; sessions can expire after extended inactivity. Check that `COOKIE_DOMAIN` covers the subdomain you are on in production.
  </Accordion>

  <Accordion icon="laptop" title="Desktop pairing fails">
    Verify the deep-link scheme (`opensail://`) is registered on the OS. On Windows, reinstall with admin rights if the registration is missing. The token expires in minutes; mint a fresh one.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="API keys guide" icon="key" href="/guides/api-keys">
    Mint keys, invoke the external agent API, set scopes.
  </Card>

  <Card title="Desktop install" icon="laptop" href="/guides/desktop-install">
    Install the desktop app and pair it to your cloud.
  </Card>

  <Card title="Configuration" icon="gear" href="/self-hosting/configuration">
    OAuth, SMTP, and cookie settings for self-hosted.
  </Card>

  <Card title="Security model" icon="shield-halved" href="/self-hosting/architecture">
    Full security overview in the architecture page.
  </Card>
</CardGroup>
