---
title: "Access tokens"
description: "Better Auth sessions, OpenTofu customer and managed-run credentials, and audience-bound MCP OAuth tokens."
---

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

# Access tokens

StackShip uses separate credentials for separate protocol surfaces. A token for
one surface is not a general-purpose API key.

| Credential | Accepted by | Purpose |
| --- | --- | --- |
| Better Auth session | Dashboard and native `/v1` API | Human product access in an active organization |
| Service-account bearer token | OpenTofu `/api/v2` compatibility surface | Workspace- and project-bounded automation |
| OpenTofu OAuth opaque token | OpenTofu `/api/v2` compatibility surface | User access evaluated against current authorized memberships |
| Managed-run opaque credential | OpenTofu `/api/v2` compatibility surface from the managed sandbox | One active plan or apply attempt |
| MCP OAuth JWT | `https://mcp.stackship.run/mcp` and, for the confined MCP automation surface, native `/v1` | Organization-bound reads with `mcp:tools`; mutations also need `mcp:write` |
| Internal capability | A specific internal runner or broker route | One bounded execution action |

The separate StackShip CLI is a roadmap item and is not available in private
beta.

## OpenTofu service accounts

An authenticated principal with `credential:write` can open a workspace,
choose **CLI access**, create a service account, and issue one token
bounded to that workspace and its project. The dashboard offers only explicit
purpose presets:

| Purpose | Exact permissions |
| --- | --- |
| State migration | `workspace:read`, `state:outputs:read`, `state:read`, `state:recover`, `state:write`, `state:lock` |
| Remote plan and apply | `workspace:read`, `state:read`, `run:create`, `run:read`, `run:approve`, `policy:read` |

Expiry is selected explicitly from 1, 7, 30, 90, or 365 days. The API rejects
an empty permission set, an unknown permission, a lifetime beyond 365 days,
and permission escalation beyond the creating principal.

The token secret is returned only by the issuance response. Store it in the
OpenTofu credential location or an external secret manager. StackShip stores a
digest, not the bearer plaintext. An exact issuance retry by the same
authenticated principal with the same request and `Idempotency-Key` re-derives
the same token to recover a lost response; the metadata-only idempotency record
does not contain the token. Reusing that key with changed input conflicts. The
dashboard therefore reuses the exact request and key when a failed issuance is
retried unchanged, but creates a new bounded request and key when the operator
changes the purpose or expiry. It removes the plaintext from its rendered
state after **I saved this token** is acknowledged. It does not provide token
inventory because the v1 API has no list operation. Revocation
invalidates subsequent use through the API.

For non-interactive OpenTofu, provide the one-time value through the
hostname-derived environment variable:

```bash
export TF_TOKEN_stackship_run='<service-account-token>'
tofu init
tofu plan
```

OpenTofu uses that credential with the native cloud protocol under `/api/v2`.
It is not accepted as a Better Auth dashboard session and it is not an MCP
credential.

## Managed-run credentials

StackShip never forwards a customer's OAuth or service-account credential into
the sandbox. For a managed plan or apply, the runner instead receives a separate
opaque `rat_` credential through the same hostname-derived
`TF_TOKEN_stackship_run` environment variable. The API derives this credential
with the dedicated `RUNNER_RUN_TOKEN_HMAC_KEY`; it is distinct from customer
service-account tokens and from runner bootstrap, session, and artifact
capabilities.

The API accepts the credential only for its exact organization, project,
workspace, run, attempt, phase, sandbox, and current workspace fence. Its state
authority is phase-specific:

| Attempt | Read state | Lock state | Publish state |
| --- | --- | --- | --- |
| Managed plan | Current state | For the OpenTofu command | Never |
| Speculative plan | Pinned state version only | Never | Never |
| Managed apply | Current predecessor | For the OpenTofu command | Only with the active reservation, owned lock, expected predecessor, and current fence |

A terminal, revoked, expired, replaced, or stale attempt fails closed. The
credential never grants `run:create` or `state:recover`, is not a customer
credential, and is rejected by the dashboard, native `/v1`, and MCP surfaces.

## OpenTofu OAuth login

Interactive `tofu login stackship.run` uses the fixed public `tofu-cli`
client, authorization code flow, and PKCE. The resulting access token is an
opaque bearer credential whose digest is stored by Better Auth. It expires
after 90 days and has only `tfe:access`. The client cannot request
`offline_access`, so no refresh token is issued.

This user token is not bound to the organization active during consent. For
each cloud-protocol request, StackShip resolves the requested organization and
requires the user to have a current authorized membership there. Removing a
membership therefore prevents access to that organization even while the
opaque token has time remaining.

The OAuth revocation endpoint deletes the opaque access-token record and makes
subsequent requests fail. Removing the credential from OpenTofu's local
configuration only removes that local copy; it does not itself promise
server-side revocation.

## MCP OAuth

The MCP server publishes RFC 9728 protected-resource metadata. Clients obtain
an access token from the API's OAuth 2.1 authorization server using PKCE. The
access token is a signed JWT bound to the organization selected during
authorization and expires after one hour.
Validation requires:

- a valid issuer and signature;
- an unexpired token;
- `mcp:tools`;
- the configured MCP resource as the token audience; and
- one exact organization claim.

Mutating MCP tools additionally require the `mcp:write` scope. Missing or
mismatched claims fail closed with a bearer challenge. Browser cookies and
OpenTofu service-account tokens are not accepted at the MCP endpoint.

The same JWT authenticates the confined MCP automation surface on native
`/v1`, where the API resolves it against its hashed token record and applies
live-membership tenant derivation; see the
[v1 authentication rules](/automation/api#authentication-and-tenancy).

Clients that request `offline_access` receive a hashed, opaque refresh token
with a 30-day expiry. Each successful refresh rotates that token and revokes
the previous value. Revoking a refresh token prevents future refreshes.
Revoking an already issued MCP JWT cannot invalidate the MCP Worker's own
local check immediately because the Worker verifies JWTs without a revocation
list; tool execution still stops immediately, because the API resolves every
tool call against the hashed token record and refuses revoked or deleted
grants. Changing organizations requires a new authorization.

OAuth consent and token protocol records are not product audit events.
StackShip does not currently record consent decisions, issuance, refresh, or
revocation in the audit log, and no organization-settings OAuth revocation UI
is available.

## Handling rules

- Never put a bearer credential in a URL or query parameter.
- Never log tokens, authorization headers, session cookies, managed-run
  credentials, or internal capabilities.
- Give a service-account token only the permissions and lifetime it needs.
- Revoke a token when its consumer is retired or its custody is uncertain.
- Treat a downloaded state file as a credential-bearing artifact even though
  the download itself used authenticated streaming.

## Related pages

- [Authentication](/authentication)
- [OpenTofu state and migration](/workspaces/state)
- [MCP server](/automation/mcp)

Source: https://docs.stackship.run/admin/tokens/index.mdx
