Skip to content

[bug] --api-key is ignored on the --host <remote> path; commands exit 1 with a misleading "Session expired" #6315

Open

Description

@jaseemts

Summary

--api-key is documented as a global option — "Use a Superset API key (sk_live_…) instead of OAuth login" — but it is ignored on any command that targets a remote host via --host <machineId>. Those commands fall back to the interactive OAuth session and, when none is present, exit 1 with:

Error: Session expired
Hint: Run: superset auth login

The message is misleading in two ways: the API key is not expired (the same key succeeds against the local host in the same shell, seconds apart), and no session was ever established, so nothing "expired". The real condition is this code path does not accept API-key auth.

Version: 1.20.0 · macOS 27.0.0 (arm64) · key type sk_live_…

Reproduction

Four commands, same shell, same key. A and D fail; B, C and E succeed.

$ OCI=<remote machineId>        # from `superset hosts list`

# A — remote host + API key flag  → FAILS
$ superset workspaces list --host "$OCI" --json --api-key "$SUPERSET_API_KEY"
Error: Session expired
Hint: Run: superset auth login
$ echo $?
1

# B — remote host + interactive OAuth session, no --api-key  → WORKS
$ superset workspaces list --host "$OCI" --json | jq length
17

# C — local host + the SAME API key  → WORKS (proves the key is valid, not expired)
$ superset workspaces list --json --api-key "$SUPERSET_API_KEY" | jq length
18

# D — remote host + env-var form of the same key  → FAILS identically
$ SUPERSET_API_KEY=sk_live_… superset workspaces list --host "$OCI" --json
Error: Session expired

# E — `hosts list` + API key  → WORKS
$ superset hosts list --json --api-key "$SUPERSET_API_KEY" | jq length
2

superset projects list --host "$OCI" --json --api-key … fails the same way (exit 1, same message), so this is not specific to workspaces list.

C is the load-bearing case. It runs immediately after A with the identical key and succeeds. Whatever A is reporting, an expired credential is not it.

E is the sharp edge. An unattended agent can enumerate remote hosts with an API key, gets back real machineIds, and then cannot query a single one of them. The API surface advertises the capability and then refuses it one call later.

Expected

Either of these would resolve it — the first is much preferable:

  1. --api-key (and SUPERSET_API_KEY) authenticate the --host <remote> path, exactly as they already do for local commands and for hosts list.
  2. If remote host access genuinely cannot support API-key auth, then say so precisely — e.g. Error: --host requires an interactive OAuth session; API-key auth is not supported on this path. Run: superset auth login — and note the restriction in --help for --host and for the global --api-key.

Right now --help states the opposite of the behaviour: --api-key is listed under Global options with no carve-out, and --host <string> carries no auth caveat.

Why this matters

API keys exist for unattended use; OAuth login is interactive by definition. Any scheduled job that reasons across more than one machine is therefore stuck: it can run locally, it can list the hosts, and it cannot read them.

Concretely, this is a multi-host cleanup job. It must confirm that a branch is not checked out in a workspace on any host before it proposes deleting that branch. With the remote host unreadable it cannot establish that, so it correctly fails closed and withholds those targets — the job stalls on a safety check it has no unattended way to satisfy. The only workaround is a human running superset auth login before each scheduled run, which defeats the schedule.

The misleading error message compounds the cost: Session expired / Run: superset auth login reads as an ordinary credential-rotation problem, so the first response is to reissue the key — which changes nothing, because the key was never the problem.

Activity

  1. github-actions commented on Aug 10, 2026

    @github-actions
    Contributor

    Summary

    The report is consistent with the code, and the mechanism is exactly as described: --api-key / SUPERSET_API_KEY is picked up by the CLI's auth middleware, but the remote-host transport forwards that raw sk_live_… string in an Authorization: Bearer header to the relay, and the relay authenticates that path only by verifying a JWT against JWKS. An API key is not a JWT, so verification fails, the relay returns tRPC UNAUTHORIZED, and the CLI's generic error handler renders any UNAUTHORIZED as Session expired / Run: superset auth login. So both halves of the complaint hold: the --host <remote> path does not accept API-key auth, and the error text is a blanket mapping that has nothing to do with the credential actually expiring.

    Affected code

    1. The API key is resolved correctly. packages/cli/src/commands/middleware.ts:7 calls resolveAuth(options.apiKey), and packages/cli/src/lib/resolve-auth.ts:29-31 takes the override branch, setting bearer to the literal sk_live_… value. Nothing here throws — the reported "Session expired" is not coming from resolve-auth.ts:39 or :54.

    2. The remote transport sends the key as a bearer JWT. packages/cli/src/lib/host-target/resolveHostTarget.ts:75-91:

    headers: {
        Authorization: `Bearer ${options.userJwt}`,   // ← userJwt is ctx.bearer, i.e. sk_live_…
        "x-superset-client-machine-id": localHostId,
    },

    The option is even named userJwt (resolveHostTarget.ts:34), but every caller passes ctx.bearer unconditionally — workspaces/list/command.ts:31, projects/list/command.ts:33, plus workspaces/{get,create,delete,open}, terminals/*, agents/{list,create}, projects/{create,setup}, hosts/set-wake, and automations/resolveAutomationTarget.ts:56. All of them are affected, matching the reporter's note that projects list fails identically.

    3. The relay rejects it. apps/relay/src/index.ts:166-170 → apps/relay/src/auth.ts:18-51 (jwtVerify against the JWKS, returns null on any failure) → trpcErrorResponse(c, "UNAUTHORIZED", "Unauthorized"). apps/relay2/src/auth.ts is identical, so this is not a v1-vs-v2 relay difference.

    4. The misleading message. packages/cli-framework/src/runner.ts:84-88 maps any UNAUTHORIZED tRPC code to:

    `Error: Session expired\nHint: Run: ${cliName} auth login\n`

    Why B, C and E succeed

    • E (hosts list) goes through packages/cli/src/lib/api-client.ts:24-28, which correctly sends sk_live_… in x-api-key (better-auth's apiKey plugin reads that header, not Authorization). API-key auth against api.superset.sh works.
    • C (local host) takes the kind: "local" branch at resolveHostTarget.ts:43-72, which authenticates with the host-service manifest's local authToken — the credential from resolveAuth is never used on the wire. So C shows the key was accepted by resolveAuth; E is what actually proves the key is valid server-side.
    • B (remote + OAuth) works because the CLI stores the OAuth access token, which is already a JWKS-signed JWT carrying organizationIds, so the relay verifies it directly. That is documented in packages/host-service/src/providers/auth/JwtAuthProvider/JwtAuthProvider.ts:56-64.

    Suspected root cause

    The --host <remote> path is missing the API-key → JWT exchange that every other relay client already performs. The working pattern exists twice in the repo:

    • packages/host-service/src/providers/auth/JwtAuthProvider/JwtAuthProvider.ts:46-81 — passes JWTs through, and for sk_live_… does GET {api}/api/auth/token with x-api-key, caching the result for 55 min.
    • packages/sdk/src/client.ts:590-630 — same exchange with in-memory cache and single-flight, specifically so SDK host calls can use an API key.

    The CLI has no equivalent; grepping packages/cli/src for auth/token, mintJwt, or looksLikeJwt returns nothing. So this looks like an omission rather than a deliberate restriction — and since the relay's own checkHostAccess (apps/relay/src/access.ts:47-76) works off the JWT's organizationIds and sub, a minted JWT should satisfy it exactly as the host-service's does.

    Reproduction

    Manual, matching the report — the discriminating pair is:

    $ superset workspaces list --host "$REMOTE_MACHINE_ID" --json --api-key "$SUPERSET_API_KEY"   # UNAUTHORIZED → "Session expired"
    $ superset hosts list --json --api-key "$SUPERSET_API_KEY"                                     # works (x-api-key path)

    A unit-level bun:test reproduction that needs no relay: call resolveHostTarget({ requestedHostId: <non-local id>, organizationId, userJwt: "sk_live_test" }) with a stubbed fetch/getHostId, drive one query through the returned client, and assert on the captured request headers. Today it asserts Authorization: Bearer sk_live_test; after a fix it should carry a minted JWT (and never the raw sk_live_ value in Authorization). A second small test can pin runner.ts's UNAUTHORIZED mapping so the message change doesn't regress.

    Suggested fix direction

    Expectation 1 from the issue looks achievable without touching the relay:

    1. Add a JWT-minting helper to the CLI (mirroring JwtAuthProvider.getJwt): pass tokens that already look like JWTs straight through; for sk_live_/sk_test_ prefixes, GET ${SUPERSET_API_URL}/api/auth/token with x-api-key and cache in-process.
    2. Thread it into resolveHostTarget's remote branch. httpBatchLink accepts an async headers() function, so this can stay a synchronous factory — take a getHostJwt: () => Promise<string> instead of the userJwt string and await it inside headers(). That confines the change to resolveHostTarget.ts plus the ~12 call sites swapping userJwt: ctx.bearer for the getter (naturally supplied from middleware.ts alongside bearer/authSource, which already knows authSource === "override").
    3. Fix the error text regardless of whether (1)/(2) land: runner.ts:85-88 shouldn't assert "expired" for every UNAUTHORIZED. Something like Error: Not authorized with a hint that varies by credential source (authSource is already on ctx) avoids sending users to rotate a key that was never the problem.

    If for some reason API keys must stay unsupported here, then the fallback is the reporter's expectation 2: fail fast in the remote branch of resolveHostTarget with an explicit message, and add the carve-out to the apiKey global description at packages/cli/cli.config.ts:26-28 and to apps/docs/content/docs/cli/env-vars.mdx:13, both of which currently promise unconditional equivalence to OAuth login.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions