Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ jobs:
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
OAUTH_TOKEN_FAMILY_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
BETTER_AUTH_SECRET: oauth-postgres-ci-secret-at-least-32-characters
OAUTH_PROVIDER_ENABLED: 'true'
NEXT_PUBLIC_APP_URL: https://test.sim.ai
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000'

Expand Down
18 changes: 8 additions & 10 deletions apps/docs/content/docs/cli/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ sim login --method oauth
sim login --method api-key
```

`--method oauth` requires OAuth support and never falls back to an API key. The
server must enable `OAUTH_PROVIDER_ENABLED=true`. Explicit OAuth selection also
overrides SSH/headless detection; your browser still needs to reach the CLI's
`--method oauth` requires a server with OAuth support and authentication enabled;
it never falls back to an API key. Explicit OAuth selection also overrides
SSH/headless detection; your browser still needs to reach the CLI's
local callback. `--method api-key` uses pairing-code approval to create a new
permanent API key. To supply an existing key, set `SIM_API_KEY` instead.

Expand Down Expand Up @@ -257,13 +257,11 @@ Save it to avoid repeating the flag:
sim configure --set-endpoint http://localhost:3000 --profile local
```

A deployment offers OAuth sign-in when its global `oauth-provider` feature flag
is enabled. With AppConfig, enable it in the existing `feature-flags` document
using `"oauth-provider": { "enabled": true }`. When AppConfig is disabled or no
AppConfig document has been loaded, `OAUTH_PROVIDER_ENABLED=true` supplies the fallback.
With the provider off, the CLI uses the pairing-code handoff; `DISABLE_AUTH=true`
always forces OAuth off. Operators must apply the database migration and drain
older app instances before enabling it. See [Sign in with Sim](/platform/self-hosting/authentication#sign-in-with-sim).
OAuth sign-in is available by default when server authentication is enabled.
`DISABLE_AUTH=true` disables OAuth. Older servers without OAuth support use the
pairing-code handoff. Keep the database schema current and drain app instances
that predate the OAuth token-family lifecycle before accepting OAuth traffic.
See [Sign in with Sim](/platform/self-hosting/authentication#sign-in-with-sim).

## Where the login is stored

Expand Down
5 changes: 3 additions & 2 deletions apps/docs/content/docs/platform/enterprise/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,14 @@ The **Chat Deployment** row also carries an **auth-mode allowlist** — *Auth mo
| API Keys | Revokes workspace API keys. Members cannot list, create, or revoke one. |
| Personal API Keys | Prevents members from using a personal API key against this workspace. |
| Personal Credentials | Prevents connecting personal credentials, leaving only workspace-shared ones. |
| CLI Access | Prevents approving a CLI login, which mints a key for the public API. |
| CLI Access | Prevents approving a CLI login or using Sim CLI OAuth tokens for the public API. Existing API keys retain their own restrictions. |
| OAuth App Access | Prevents OAuth apps from accessing the group's workspaces. The organization default group also governs authorization, token issuance and refresh, and account-level billing and audit reads. |

##### Rows read from the organization default group

Two rows — **Workspace Creation** and **Member Directory** — are read only from the organization's **default group**, because the act they govern names no workspace. On any other group the editor renders them inert, tags them **Organization**, and skips them in **Select All**. Set them on the default group.

Five more rows — **Integrations**, **API Keys**, **Invitations**, **Personal API Keys**, and **CLI Access** — apply on the group in front of you for anything scoped to one of its workspaces. The account-level path of the same action falls back to the default group: minting a personal key, an organization-wide invitation, an account-level CLI login. To close one of these completely, set it on the default group as well.
Six more rows — **Integrations**, **API Keys**, **Invitations**, **Personal API Keys**, **CLI Access**, and **OAuth App Access** — apply on the group in front of you for anything scoped to one of its workspaces. Account-level actions use the organization's default group, including minting a personal key, sending an organization-wide invitation, approving a CLI login, and authorizing or refreshing an OAuth app.

### 4. Choose who it applies to

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/platform/enterprise/self-hosted.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Persist that value as `CRON_SECRET` on the app **and** on whatever calls these e
<Callout type="warn">
Both shipped deployments schedule the data-drain dispatcher and OAuth token cleanup, but **not** the three configurable data-retention endpoints. Setting `DATA_RETENTION_ENABLED=true` alone deletes no retained product data — those windows are evaluated only when one of the three endpoints is called. Add them to `cronjobs.jobs` yourself, or drive them from an external scheduler.

OAuth token cleanup continues when the global `oauth-provider` feature flag is off, so rows created while the provider was enabled do not become permanent. See [Sign in with Sim](/platform/self-hosting/authentication#sign-in-with-sim) for AppConfig and fallback configuration.
OAuth token cleanup runs independently of sign-in activity, removing expired and revoked credentials. See [Sign in with Sim](/platform/self-hosting/authentication#sign-in-with-sim) for provider configuration.
</Callout>

```bash
Expand Down
58 changes: 20 additions & 38 deletions apps/docs/content/docs/platform/self-hosting/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,50 +81,32 @@ See the [SSO guide](/platform/enterprise/sso) for identity-provider setup and th

## Sign in with Sim

Your deployment can act as an OAuth 2.0 authorization server using authorization
code with PKCE and current OAuth security guidance. The Sim CLI uses it when
enabled; see [CLI authentication](/cli/authentication).
Your deployment acts as an OAuth 2.0 authorization server using authorization
code with PKCE. OAuth sign-in is available whenever authentication is enabled;
see [CLI authentication](/cli/authentication).

The global `oauth-provider` feature flag controls availability. Keep it off while
applying the database migration, then deploy and drain every older app instance
before enabling it.
Apply database migrations before deploying a new app version. When upgrading
from a version without the OAuth token-family lifecycle, drain older app
instances before accepting OAuth traffic so every instance enforces the same
refresh and revocation rules.

If your deployment uses AWS AppConfig, add this entry to the existing
`feature-flags` document and deploy that configuration:

```json
{
"oauth-provider": { "enabled": true }
}
```

Preserve the document's other entries. This flag is global: use `enabled`, not
workspace, organization, user, or admin targeting. Set `enabled` to `false` to
turn it off; changes take effect as instances refresh their AppConfig cache.

When AppConfig is disabled or no AppConfig document has been loaded, the
fallback is:

```bash
OAUTH_PROVIDER_ENABLED=true
```

In that fallback mode, unset or false keeps the provider off. An available
AppConfig document takes precedence over this variable, including when the
`oauth-provider` entry is missing or disabled. AppConfig fetch failures retain
the last successfully loaded document.

When the provider is off, discovery at `/.well-known/oauth-authorization-server`
returns 404 and the CLI falls back to the pairing-code handoff.
`DISABLE_AUTH=true` also forces the provider off because the authorization flow
requires a real Better Auth user session.
`DISABLE_AUTH=true` disables OAuth sign-in and discovery because authorization
requires a real Better Auth user session. Older servers without OAuth support
return 404 from `/.well-known/oauth-authorization-server`, and the CLI falls
back to the pairing-code handoff.

Access tokens are opaque and last an hour; refresh tokens rotate on every use.
Each login has a fixed thirty-day lifetime that refreshing does not extend.
Token validation checks current grants, so revoking a grant under
**Settings → General → Authorized apps** stops the app on its very next request. These
settings remain available for reviewing and revoking existing grants while the
provider is off, and scheduled OAuth token cleanup continues.
**Settings → General → Authorized apps** stops the app on its very next request.
Scheduled OAuth token cleanup runs independently of sign-in activity.

Organization admins can restrict **OAuth App Access** under **Credentials & Access**
in [permission groups](/platform/enterprise/access-control). Workspace requests use
the group governing that workspace. Authorizing apps, issuing and refreshing tokens,
and account-level billing and audit reads use the organization's default group.
**CLI Access** also applies to the Sim CLI. Members can still review and revoke
existing grants when OAuth app access is restricted.

### Registering an app

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,6 @@ import { Callout } from 'fumadocs-ui/components/callout'

Google, GitHub, and Microsoft sign-in, their callback URLs, and the `DISABLE_*_AUTH` switches are documented in [Authentication](/platform/self-hosting/authentication#social-login).

## Sign in with Sim

| Variable | Description |
| --- | --- |
| `OAUTH_PROVIDER_ENABLED` | Fallback for the global `oauth-provider` feature flag when AppConfig is disabled or no AppConfig document has been loaded. Set to `true` only after the migration is applied and every older app instance is drained. With AppConfig, use `"oauth-provider": { "enabled": true }` in the existing `feature-flags` document instead. `DISABLE_AUTH=true` always forces it off. See [Authentication](/platform/self-hosting/authentication#sign-in-with-sim) |

## Integration Credentials

<Callout type="warn">
Expand Down
1 change: 0 additions & 1 deletion apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ BETTER_AUTH_URL=http://localhost:3000

# Authentication Bypass (Optional - for self-hosted deployments behind private networks)
# DISABLE_AUTH=true # Uncomment to bypass authentication entirely. Creates an anonymous session for all requests.
# OAUTH_PROVIDER_ENABLED=true # Enable Sim's OAuth authorization server after every app instance runs the matching migration/code. DISABLE_AUTH=true forces it off.

# Private-network egress allowlist (Optional - self-hosted only; ignored on Sim Cloud)
# EGRESS_ALLOWED_HOSTS=host.docker.internal,*.svc.cluster.local # Uncomment to let outbound requests reach these hosts on a private network. Widens the SSRF boundary; only use on a trusted private network.
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/(auth)/oauth/consent/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import type { SearchParams } from 'nuqs/server'
import { getSession } from '@/lib/auth'
import { isOAuthProviderEnabled } from '@/lib/auth/oauth-provider-feature'
import { isAuthDisabled } from '@/lib/core/config/env-flags'
import { OAuthConsentView } from '@/app/(auth)/oauth/consent/consent-view'
import { oauthConsentSearchParamsCache } from '@/app/(auth)/oauth/consent/search-params'

Expand All @@ -22,7 +22,7 @@ export default async function OAuthConsentPage({
}: {
searchParams: Promise<SearchParams>
}) {
if (!(await isOAuthProviderEnabled())) redirect('/')
if (isAuthDisabled) redirect('/')

const [session, raw] = await Promise.all([getSession(), searchParams])

Expand Down
29 changes: 14 additions & 15 deletions apps/sim/app/(auth)/oauth/sign-in/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@ import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const flags = vi.hoisted(() => ({
enabled: true,
authDisabled: false,
registrationDisabled: false,
appUrl: 'https://sim.test',
}))

vi.mock('@/lib/core/config/env-flags', () => ({
...envFlagsMock,
get isAuthDisabled() {
return flags.authDisabled
},
get isRegistrationDisabled() {
return flags.registrationDisabled
},
}))

vi.mock('@/lib/auth/oauth-provider-feature', () => ({
isOAuthProviderEnabled: vi.fn(async () => flags.enabled),
}))

vi.mock('@/lib/core/config/env', () => {
const mock = createEnvMock({ NEXT_PUBLIC_APP_URL: 'https://sim.test' })
return {
Expand All @@ -48,24 +47,24 @@ function redirectParts(response: Response): { destination: URL; callback: URL }

describe('OAuth login bridge', () => {
beforeEach(() => {
flags.enabled = true
flags.authDisabled = false
flags.registrationDisabled = false
flags.appUrl = 'https://sim.test'
})

it.each([true, false])(
'keeps the configured auth origin when Next normalizes loopback hosts (enabled=%s)',
async (enabled) => {
flags.enabled = enabled
'keeps the configured auth origin when Next normalizes loopback hosts (authDisabled=%s)',
async (authDisabled) => {
flags.authDisabled = authDisabled
flags.appUrl = 'http://127.0.0.1:37488'
const incoming = new NextRequest(`${flags.appUrl}/oauth/sign-in?client_id=sim-cli`)
expect(incoming.nextUrl.origin).toBe('http://localhost:37488')

const response = await GET(incoming)
const destination = new URL(response.headers.get('location')!)
expect(destination.origin).toBe(flags.appUrl)
expect(destination.pathname).toBe(enabled ? '/signup' : '/')
if (enabled) expect(redirectParts(response).callback.origin).toBe(flags.appUrl)
expect(destination.pathname).toBe(authDisabled ? '/' : '/signup')
if (!authDisabled) expect(redirectParts(response).callback.origin).toBe(flags.appUrl)
}
)

Expand Down Expand Up @@ -94,12 +93,12 @@ describe('OAuth login bridge', () => {
expect(callback.searchParams.has('prompt')).toBe(false)
})

it('uses login when registration is disabled and hides a disabled provider', async () => {
it('uses login when registration is disabled and hides OAuth when authentication is disabled', async () => {
flags.registrationDisabled = true
const enabled = await GET(request('client_id=sim-cli'))
expect(redirectParts(enabled).destination.pathname).toBe('/login')

flags.enabled = false
flags.authDisabled = true
const disabled = await GET(request('client_id=sim-cli'))
expect(disabled.status).toBe(302)
expect(new URL(disabled.headers.get('location') as string).pathname).toBe('/')
Expand Down Expand Up @@ -155,15 +154,15 @@ describe('OAuth login bridge', () => {
}
})

it('uses the same redirect precedence as the form and ignores a disabled OAuth provider', async () => {
it('uses the same redirect precedence as the form and requires authentication for OAuth', async () => {
const destination = new URL('/login', 'https://sim.test')
destination.searchParams.set('callbackUrl', '/api/auth/oauth2/authorize?client_id=sim-cli')
destination.searchParams.set('redirect', '/workspace')
const headers = { cookie: 'better-auth.session_token=existing.session' }
expect((await proxy(new NextRequest(destination, { headers }))).status).toBe(307)

destination.searchParams.delete('redirect')
flags.enabled = false
flags.authDisabled = true
expect((await proxy(new NextRequest(destination, { headers }))).status).toBe(307)
})
})
5 changes: 2 additions & 3 deletions apps/sim/app/(auth)/oauth/sign-in/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { type NextRequest, NextResponse } from 'next/server'
import { isOAuthProviderEnabled } from '@/lib/auth/oauth-provider-feature'
import { isRegistrationDisabled } from '@/lib/core/config/env-flags'
import { isAuthDisabled, isRegistrationDisabled } from '@/lib/core/config/env-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'
Expand Down Expand Up @@ -40,7 +39,7 @@ function consumeInteractivePrompt(params: URLSearchParams): boolean {
*/
export const GET = withRouteHandler(async (request: NextRequest) => {
/** Avoid sending a newly signed-in user to a disabled provider's JSON 404. */
if (!(await isOAuthProviderEnabled())) {
if (isAuthDisabled) {
return NextResponse.redirect(new URL('/', getBaseUrl()), 302)
}

Expand Down
Loading
Loading