Skip to content
Merged
10 changes: 8 additions & 2 deletions dashboard/src/features/auth/components/auth-gate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAuthStatus, useWhoami } from "../hooks";
/**
* Wraps the authenticated portion of the dashboard.
*
* - When the server reports auth disabled, children render immediately.
* - When setup is required, redirects to ``/login`` (which shows the setup
* form).
* - When the user isn't signed in, redirects to ``/login``.
Expand All @@ -20,16 +21,21 @@ export function AuthGate({ children }: { children: ReactNode }) {
const status = useAuthStatus();
const whoami = useWhoami();

const authDisabled = status.data?.auth_enabled === false;
const setupRequired = status.data?.setup_required === true;
const authenticated = !!whoami.data?.user;
const loading = status.isLoading || whoami.isLoading;

useEffect(() => {
if (loading) return;
if (loading || authDisabled) return;
if (setupRequired || !authenticated) {
void navigate({ to: "/login" });
}
}, [loading, setupRequired, authenticated, navigate]);
}, [loading, authDisabled, setupRequired, authenticated, navigate]);

if (authDisabled) {
return <>{children}</>;
}

if (loading || setupRequired || !authenticated) {
return (
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/features/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface SetupResponse {

export interface AuthStatus {
setup_required: boolean;
/** Absent on older servers, which always enforce auth. */
auth_enabled?: boolean;
}

export interface WhoamiResponse {
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function LoginPage() {
const status = useAuthStatus();
const whoami = useWhoami();

if (whoami.data?.user) {
if (status.data?.auth_enabled === false || whoami.data?.user) {
return <Navigate to="/" />;
}

Expand Down
9 changes: 6 additions & 3 deletions docs/content/docs/java/guides/integrations/spring.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ taskito:
dashboard:
enabled: true
port: 8080
# token: ${DASH_TOKEN:} # omit for session auth (default); set to use legacy shared-token mode
# auth-enabled: true # opt in to session auth; the dashboard serves openly by default
# token: ${DASH_TOKEN:} # legacy shared-token mode; overrides auth-enabled
# static-dir: /opt/taskito/dashboard-static
# secure-cookies: true # false drops the Secure cookie attribute for local HTTP
```
Expand All @@ -92,11 +93,13 @@ taskito:
|---|---|---|---|
| `taskito.dashboard.enabled` | `boolean` | `false` | Auto-start the dashboard server |
| `taskito.dashboard.port` | `int` | `8081` | Bind port (`0` for ephemeral); defaults off Spring Boot's own `8080` |
| `taskito.dashboard.token` | `String` | none | Legacy shared token gating `/api/*`; unset enables [session auth](/java/guides/operations/dashboard#session-auth-default) |
| `taskito.dashboard.auth-enabled` | `boolean` | `false` | Enable [session auth](/java/guides/operations/dashboard#session-auth-opt-in) (login/setup, CSRF, roles); off means the dashboard serves openly |
| `taskito.dashboard.token` | `String` | none | Legacy shared token gating `/api/*`; overrides `auth-enabled` |
| `taskito.dashboard.static-dir` | `String` | auto-discovered | Directory of a prebuilt SPA, overriding the jar's extracted copy |
| `taskito.dashboard.secure-cookies` | `boolean` | `true` | Drop the `Secure` cookie attribute for local HTTP development |

OAuth/OIDC env vars (`TASKITO_DASHBOARD_OAUTH_*`, see
[SSO](/java/guides/operations/sso)) and the admin bootstrap
(`TASKITO_DASHBOARD_ADMIN_USER`/`_PASSWORD`) are read the same way regardless
of Spring — they aren't Spring properties.
of Spring — they aren't Spring properties, and they only apply when
`auth-enabled` is set.
8 changes: 5 additions & 3 deletions docs/content/docs/java/guides/operations/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ Read commands print pretty JSON, so the output pipes straight into `jq`.
## Serving the dashboard

```bash
taskito --url taskito.db dashboard --port 8080 --token "$DASH_TOKEN"
taskito --url taskito.db dashboard --port 8080 --auth
```

Serves the bundled [dashboard](/java/guides/operations/dashboard) until
interrupted. `--static <dir>` overrides the SPA assets; `--token` gates the
API.
interrupted. This example enables session authentication with `--auth`;
omit the flag to serve openly (the default). `--token <token>` gates the
API with a legacy shared token instead; `--static <dir>` overrides the
SPA assets.

## Other backends

Expand Down
59 changes: 41 additions & 18 deletions docs/content/docs/java/guides/operations/dashboard.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Dashboard
description: "Serve the bundled web dashboard and its REST API — session auth, OAuth/SSO, and legacy token mode."
description: "Serve the bundled web dashboard and its REST API — open by default, with opt-in session auth, OAuth/SSO, and legacy token mode."
---

The jar bundles the web dashboard SPA; `DashboardServer` serves it plus a JSON
Expand All @@ -19,10 +19,11 @@ try (Taskito taskito = Taskito.builder().sqlite("taskito.db").open();
}
```

`DashboardServer.start(queue, port)` opens in [session-auth
mode](#session-auth-default). `start(queue, port, token)` switches to
[legacy shared-token mode](#legacy-shared-token-mode); four- and five-argument
overloads add an unpacked SPA directory and a `secureCookies` flag.
`DashboardServer.start(queue, port)` serves openly — no authentication.
`start(queue, port, true)` enables [session auth](#session-auth-opt-in);
`start(queue, port, token)` switches to
[legacy shared-token mode](#legacy-shared-token-mode). The full variant is
`start(queue, port, token, staticDir, secureCookies, authEnabled)`.

</Tab>
<Tab value="Taskito.dashboard()">
Expand All @@ -33,12 +34,16 @@ try (Taskito taskito = Taskito.builder().sqlite("taskito.db").open();
// ...
}

// Session auth (login/setup, CSRF, roles):
taskito.dashboard(8080, true);

// Legacy shared-token mode, gating /api/* as a fixed admin identity:
taskito.dashboard(8080, System.getenv("DASH_TOKEN"));
```

`Taskito.dashboard(port)` / `dashboard(port, token)` are convenience defaults
over `DashboardServer.start(...)` for the common case — one fewer import.
`Taskito.dashboard(port)` / `dashboard(port, authEnabled)` /
`dashboard(port, token)` are convenience defaults over
`DashboardServer.start(...)` for the common case — one fewer import.

</Tab>
<Tab value="CLI">
Expand All @@ -50,6 +55,7 @@ taskito --url taskito.db dashboard --port 8080
| Flag | Default | Description |
|---|---|---|
| `--port` | `8080` | Bind port (`0` for ephemeral) |
| `--auth` | off | Enable session authentication (login/setup, CSRF, roles) |
| `--token` | none | Legacy shared token gating `/api/*` — disables session auth and OAuth |
| `--static` | bundled SPA | Directory of a prebuilt SPA, overriding the jar's extracted copy |
| `--insecure-cookies` | off | Drop the `Secure` cookie attribute — for local HTTP development |
Expand All @@ -71,21 +77,35 @@ assets only `/api/*` responds.

## Auth

Two modes, chosen by whether a `token` is passed to `start(...)` /
`dashboard(...)`.
Three modes: **open** (the default — no authentication), **session auth**
(opt-in via `authEnabled` / `--auth`), and **legacy shared-token** (a
non-null `token`, which overrides `authEnabled`).

### Open mode (default)

### Session auth (default)
With neither `authEnabled` nor a `token`, the dashboard serves openly —
no setup screen, no login, no CSRF or roles. The auth endpoints respond
`404 {"error": "auth_disabled"}`, except `GET /api/auth/status`, which
returns `{"auth_enabled": false, "setup_required": false}` so the SPA
skips the login flow. Suits local development; production deployments
should enable session auth or keep the port on a private network.

With no token, the dashboard runs password sign-in (and optionally
[OAuth/OIDC](/java/guides/operations/sso)) with server-side sessions. Users
and sessions live in the queue's settings key/value store — no dedicated
tables — so the model is identical across SQLite, PostgreSQL, and Redis.
### Session auth (opt-in)

With `authEnabled=true` (or `--auth`), the dashboard runs password sign-in
(and optionally [OAuth/OIDC](/java/guides/operations/sso)) with server-side
sessions. Users and sessions live in the queue's settings key/value store —
no dedicated tables — so the model is identical across SQLite, PostgreSQL,
and Redis.

- **First-run setup.** On a fresh database every route except the public set
(`/api/auth/status`, `/api/auth/login`, `/api/auth/setup`,
`/api/auth/providers`, `/health`, `/readiness`, `/metrics`) returns
`503 setup_required` until an admin exists. `POST /api/auth/setup` creates
it (and signs it in); the route locks itself after the first user.
`GET /api/auth/status` reports
`{"auth_enabled": true, "setup_required": bool}` so the SPA knows which
screen to show.
- **Env-admin bootstrap.** Set both `TASKITO_DASHBOARD_ADMIN_USER` and
`TASKITO_DASHBOARD_ADMIN_PASSWORD` before starting the process to seed the
first admin without visiting a browser — useful for containers. It's
Expand All @@ -95,9 +115,11 @@ tables — so the model is identical across SQLite, PostgreSQL, and Redis.
```bash
export TASKITO_DASHBOARD_ADMIN_USER=admin
export TASKITO_DASHBOARD_ADMIN_PASSWORD='change-me-on-first-login'
taskito --url taskito.db dashboard --port 8080
taskito --url taskito.db dashboard --port 8080 --auth
```

The env vars are only read when session auth is enabled.

<Callout type="warning">
Unlike a scripting-language runtime, the JVM cannot scrub a variable out
of its own process environment once it has been read — the password
Expand Down Expand Up @@ -190,7 +212,8 @@ the same contract the bundled SPA consumes. All paths below are relative to
| Settings | `settings`, `settings/{key}` |

<Callout type="warning">
Every route above is auth-gated: session mode requires a valid session (plus
CSRF on writes) except the public auth routes; legacy mode requires the
matching token. See [Auth](#auth).
Gating depends on the [auth mode](#auth): open mode (the default) serves
every route without credentials; session mode requires a valid session
(plus CSRF on writes) except the public auth routes; legacy mode requires
the matching token.
</Callout>
19 changes: 11 additions & 8 deletions docs/content/docs/java/guides/operations/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,16 @@ permits any path**, so always set roots in production.

## Dashboard

The [dashboard](/java/guides/operations/dashboard) requires sign-in by
default: on a fresh database every route except a small public set returns
`503 setup_required` until an admin exists, and every route after that needs
a valid session (admin/viewer RBAC, CSRF on writes) or, if configured,
The [dashboard](/java/guides/operations/dashboard) serves **openly by
default** — anyone who reaches the port has full control. Production
deployments should enable session auth (`authEnabled=true` / `--auth`): on a
fresh database every route except a small public set then returns
`503 setup_required` until an admin exists, and every non-public route after
that needs a valid session (admin/viewer RBAC, CSRF on writes) or, if configured,
[OAuth/OIDC](/java/guides/operations/sso). Passing a `token` instead switches
Comment thread
pratyush618 marked this conversation as resolved.
to legacy shared-token mode — no users, no sessions, no RBAC — kept for
back-compat; anyone with the token has full control. Either way, bind the
port to a trusted network or front it with a reverse proxy for
back-compat; anyone with the token has full control. Whatever the mode, bind
the port to a trusted network or front it with a reverse proxy for
defense in depth.

## Mesh gossip
Expand All @@ -87,7 +89,8 @@ hosts with a `noexec` `/tmp`.
- [ ] Payloads signed or encrypted where the storage host isn't fully trusted.
- [ ] Webhook receivers verify the HMAC signature; webhook URLs are trusted.
- [ ] `FileProxyHandler` configured with allowlisted roots.
- [ ] Dashboard admin account created (or env-bootstrapped) on first deploy;
bound to a trusted network regardless of auth mode.
- [ ] Dashboard auth enabled (`authEnabled=true` / `--auth`) with an admin
created (or env-bootstrapped) on first deploy; bound to a trusted
network regardless of auth mode.
- [ ] Logs don't echo sensitive payloads (redact in `onEnqueue`
[middleware](/java/guides/extensibility/middleware)).
8 changes: 5 additions & 3 deletions docs/content/docs/java/guides/operations/sso.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ title: SSO (OAuth & OIDC)
description: "Sign in to the dashboard with Google, GitHub, or any OIDC provider. Domain/org allowlists, OAuth-only mode."
---

The dashboard's [session auth](/java/guides/operations/dashboard#session-auth-default)
The dashboard's [session auth](/java/guides/operations/dashboard#session-auth-opt-in)
supports native sign-in for **Google**, **GitHub**, and any
**OIDC-compliant** provider (Okta, Auth0, Keycloak, Microsoft Entra, Dex, …).
Multiple OIDC providers can run side by side, each its own button on the
login screen. OAuth is off by default — setting any provider's env vars turns
it on, alongside password login unless you opt out.
login screen. OAuth requires session auth to be enabled
(`authEnabled=true` / `--auth`; the dashboard serves openly by default) and
is itself off by default — setting any provider's env vars turns it on,
alongside password login unless you opt out.

<Callout title="Optional dependency for Google and OIDC" type="info">
ID-token validation for Google and generic OIDC needs
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/node/guides/operations/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ over them. Use it as your container entrypoint for worker processes.
taskito --db taskito.db dashboard --port 8787
```

Serves openly by default. `--auth` enables session authentication
(login/setup, CSRF, roles); `--token <token>` uses the legacy shared-token
gate instead — see [Dashboard](/node/guides/operations/dashboard).

## KEDA scaler

```bash
Expand Down
29 changes: 20 additions & 9 deletions docs/content/docs/node/guides/operations/dashboard-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,29 @@ returns a page, fields in snake_case:

## Auth

By default the dashboard runs in **open mode**: `GET /api/auth/status` reports
`{ setup_required: false }` and `GET /api/auth/whoami` returns an admin identity.
By default the dashboard runs in **open mode**: every route serves without
credentials, `GET /api/auth/status` reports
`{ auth_enabled: false, setup_required: false }`, and the other auth
endpoints respond `404 { "error": "auth_disabled" }`.

Pass `auth: { token }` to [`serveDashboard`](/node/guides/operations/dashboard) to
require a bearer token. Then every `/api/*` request except `GET /api/auth/status`
must present the token via `Authorization: Bearer <token>`, an `X-Taskito-Token`
header, a `?token=` query, or the `taskito_token` cookie; otherwise it returns
`401`. This is a single shared token — there is no per-user login, RBAC, or SSO.
Pass `authEnabled: true` to
[`serveDashboard`](/node/guides/operations/dashboard) for **session mode**:
first-run setup, password login with server-side sessions, CSRF on writes,
and admin/viewer roles. `GET /api/auth/status` then reports
`{ auth_enabled: true, setup_required: <bool> }`, and every route outside
the public set requires a valid session.

Pass `auth: { token }` instead for the **legacy token mode**: every `/api/*`
request except `GET /api/auth/status` must present the token via
`Authorization: Bearer <token>`, an `X-Taskito-Token` header, a `?token=`
query, or the `taskito_token` cookie; otherwise it returns `401`. This is a
single shared token — no per-user login or roles — and it overrides
`authEnabled`.

<Callout type="warning">
Open mode means anyone who can reach the port has full control. Don't expose it
on an untrusted network — bind to localhost, or mount it behind your own auth
with the [Express](/node/guides/integrations/express#dashboard) /
on an untrusted network — enable session auth, bind to localhost, or mount it
behind your own auth with the
[Express](/node/guides/integrations/express#dashboard) /
[Fastify](/node/guides/integrations/fastify) helpers.
</Callout>
28 changes: 24 additions & 4 deletions docs/content/docs/node/guides/operations/dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,28 @@ It serves the SPA plus the `/api/*` REST contract over the queue:
See the [REST API reference](/node/guides/operations/dashboard-api) for the full
endpoint list.

Auth runs **open** by default. Pass `auth: { token }` to require a bearer token
on every `/api/*` request (except `/api/auth/status`):
Auth runs **open** by default — no setup screen, no login, no CSRF or
roles. The auth endpoints respond `404 {"error": "auth_disabled"}`, except
`GET /api/auth/status`, which reports
`{ auth_enabled: false, setup_required: false }` so the SPA skips the login
flow. Production deployments should enable session authentication (or keep
the dashboard on a private network behind their own auth):

```ts
serveDashboard(queue, { port: 8787, authEnabled: true });
```

(or `taskito dashboard --auth` from the CLI). With `authEnabled: true` the
full session flow runs: a one-time setup screen creates the first admin —
or bootstrap it headlessly with `TASKITO_DASHBOARD_ADMIN_USER` /
`TASKITO_DASHBOARD_ADMIN_PASSWORD` — then password login with server-side
sessions, CSRF protection on writes, and admin/viewer roles. OAuth/SSO
providers configured via `TASKITO_DASHBOARD_OAUTH_*` env vars apply only in
this mode.

The legacy shared-token gate is unchanged: pass `auth: { token }` to require
a bearer token on every `/api/*` request (except `/api/auth/status`). It
overrides `authEnabled`.

```ts
serveDashboard(queue, { port: 8787, auth: { token: process.env.DASH_TOKEN! } });
Expand All @@ -41,8 +61,8 @@ serveDashboard(queue, { port: 8787, auth: { token: process.env.DASH_TOKEN! } });
Requests authenticate via `Authorization: Bearer <token>`, an `X-Taskito-Token`
header, or `?token=<token>`. Opening `/?token=<token>` once sets an httpOnly
cookie so the SPA works for the rest of the session. The token is compared in
constant time. For full login / RBAC / SSO, mount the dashboard inside an existing
app behind your own auth via the
constant time. You can also mount the dashboard inside an existing app behind
your own auth via the
[Express](/node/guides/integrations/express#dashboard) or
[Fastify](/node/guides/integrations/fastify) helpers.

Expand Down
13 changes: 8 additions & 5 deletions docs/content/docs/node/guides/operations/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ request timeout.
## Dashboard

The [dashboard](/node/guides/operations/dashboard) runs in **open mode** by
default — anyone who reaches the port has full control. Pass `auth: { token }` to
gate the API with a shared bearer token, bind it to localhost, or mount it behind
your own auth (login / RBAC / SSO) via the
[Express](/node/guides/integrations/express#dashboard) /
default — anyone who reaches the port has full control. For production,
pass `authEnabled: true` (or `--auth`) to enable the session flow —
first-run setup, password login, CSRF, admin/viewer roles — or
`auth: { token }` to gate the API with a legacy shared bearer token. Either
way, bind it to localhost or a private network, or mount it behind your own
auth via the [Express](/node/guides/integrations/express#dashboard) /
[Fastify](/node/guides/integrations/fastify) helpers.

## Hardening checklist
Expand All @@ -64,6 +66,7 @@ your own auth (login / RBAC / SSO) via the
- [ ] Enqueue behind your own authn/authz — never expose storage to clients.
- [ ] No secrets in task args; fetch them inside the handler.
- [ ] Webhook receivers verify the HMAC signature; webhook URLs are trusted.
- [ ] Dashboard bound to localhost or behind auth.
- [ ] Dashboard auth enabled (`authEnabled: true` / `--auth`), and the port
bound to localhost or behind auth.
- [ ] Logs don't echo sensitive payloads (use `onEnqueue` to
[redact](/node/guides/resources/interception)).
3 changes: 2 additions & 1 deletion docs/content/docs/python/api-reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ shutdown — in-flight tasks are allowed to complete before the process exits.
Start the web dashboard (SPA + REST API) for the queue.

```bash
taskito dashboard --app <module:attribute> [--host <addr>] [--port <port>] [--insecure-cookies]
taskito dashboard --app <module:attribute> [--host <addr>] [--port <port>] [--auth] [--insecure-cookies]
```

| Flag | Default | Description |
|---|---|---|
| `--app` | — | Python path to the `Queue` instance |
| `--host` | `127.0.0.1` | Bind address |
| `--port` | `8080` | Bind port |
| `--auth` | `False` | Enable session authentication (login/setup, CSRF, roles); off by default |
| `--insecure-cookies` | `False` | Drop the `Secure` flag on session cookies (local HTTP dev only) |

**Example:**
Expand Down
Loading