| title | Authentication |
|---|---|
| description | JWT sessions, OAuth providers, email 2FA, external API keys, and desktop pairing for OpenSail |
OpenSail supports five authentication paths, all enforced by the orchestrator:
- Password (bcrypt) with optional email 2FA
- OAuth login (GitHub, Google)
- OAuth connection for third-party services (deployments, git providers, MCP platform apps)
- External API keys (
tsk_*) for the external agent API and gateway - 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.
`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. `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). 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. Access tokens renew automatically via the refresh cookie. No explicit refresh call is needed from the client.Required environment variables for email flows: SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SENDER_EMAIL, SMTP_USE_TLS.
Two providers ship out of the box. Both are optional; absence of credentials disables the provider gracefully.
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`.
| 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`.
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.
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.
See the external agent API guide for scopes, rate limits, and webhook callbacks.
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.
In the cloud instance, `Settings -> API Keys -> Create desktop key`. This returns a short-lived deep-link URL (`opensail://pair?token=...`). Click the URL on the machine running the desktop app. The Tauri deep-link handler exchanges the token for a long-lived `tsk_*` key. 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. `GET /api/desktop/session/ping` confirms pairing. The tray icon turns green.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.
| 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 |
