> ## Documentation Index
> Fetch the complete documentation index at: https://starforge.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Get a bearer token, keep it fresh, and choose the right kind of credential.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -X POST https://starforge.your-company.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "alice", "password": "..."}'
```

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": { "username": "alice", "role": "user" }
}
```

Send that token as `Authorization: Bearer <token>` on every other request.

## Four kinds of credential

They are not interchangeable, and using the wrong one is the most common integration mistake.

<ParamField path="Access token" type="JWT, 8 hours">
  What `POST /api/auth/login` and the CLI device flow return. Identifies **a person**. Carries their
  role, so an admin's token opens admin endpoints. This is the one you want for scripts that act on
  your own behalf.
</ParamField>

<ParamField path="Refresh token" type="30 days">
  Issued alongside the access token, and only ever as an `httpOnly` cookie — never in a response
  body, so browser JavaScript cannot read it. `POST /api/auth/refresh` exchanges it for a new access
  token. A command-line client re-runs the device flow instead.
</ParamField>

<ParamField path="Ingest token" type="30 days, one run">
  Injected into a training container as `STARFORGE_TOKEN`. Scoped to a single run and only accepted
  by `/api/ingest/*`. A job holds this and nothing else — it never sees your account credentials.
  See [Reporting from training code](/en/api-reference/ingest).
</ParamField>

<ParamField path="Deployment token" type="revocable, no expiry">
  Authorizes application traffic to one [model deployment](/en/api-reference/inference) and nothing
  else. Independent of any login session, so a service keeps working after the person who created it
  leaves. Revoke it from the console.
</ParamField>

<Info>
  Roles are `admin` and `operator` (the console labels the latter "Member"). Admin-only endpoints
  return `403` for an `operator` token — not `401`, which
  would wrongly suggest the token is bad.
</Info>

## Log in from a browser or a script

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
SF_TOKEN=$(curl -sX POST https://starforge.your-company.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "alice", "password": "'"$SF_PASSWORD"'"}' | jq -r .token)
```

Ten attempts per IP per minute. Deployments using OIDC disable this route; check
`GET /api/auth/config` to see which login methods a deployment offers before assuming.

## Log in from a machine with no browser

The device flow, RFC 8628. This is what `sf login --device-flow` uses, and what you want on an SSH
session or a build agent.

<Steps>
  <Step title="Ask for a code">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    curl -sX POST https://starforge.your-company.com/api/cli/device/code \
      -H "Content-Type: application/json" -d '{}'
    ```

    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "device_code": "5f3a...",
      "user_code": "WDJB-MJHT",
      "verification_uri": "https://starforge.your-company.com/cli/device",
      "verification_uri_complete": "https://starforge.your-company.com/cli/device?user_code=WDJB-MJHT"
    }
    ```
  </Step>

  <Step title="Approve it in a browser">
    Open `verification_uri_complete` anywhere you are already signed in, on any device.
  </Step>

  <Step title="Poll until it is approved">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    curl -sX POST https://starforge.your-company.com/api/cli/device/token \
      -H "Content-Type: application/json" \
      -d '{"device_code": "5f3a..."}'
    ```

    Returns `authorization_pending` until somebody approves it, then the access token and
    `expires_in`. Poll no faster than every five seconds; twenty polls per minute is the ceiling.
  </Step>
</Steps>

## Confirm it worked

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -H "Authorization: Bearer $SF_TOKEN" \
  https://starforge.your-company.com/api/auth/me
```

Returns your username, role, and quota. A `401` here means the token is absent, malformed, or
expired — try logging in again before looking anywhere else.

## Handling expiry

An access token lasts eight hours by default (`FORGE_JWT_HOURS` on the deployment). When one
expires, endpoints return `401`. Two ways to recover, depending on what you are:

* **A browser session**: `POST /api/auth/refresh` with the refresh cookie.
* **A script or CI job**: log in again, or re-run the device flow. Do not cache a token across runs
  and hope; check `GET /api/auth/me` and re-authenticate on `401`.

`POST /api/auth/logout` revokes the refresh token and, on deployments with Redis configured,
invalidates the access token immediately rather than at expiry.

<Accordion title="Why the refresh token never appears in a response body">
  A token that JavaScript can read is a token an injected script can steal, and a 30-day refresh
  token is worth far more than an 8-hour access token. Keeping it in an `httpOnly` cookie means an
  XSS bug costs a session rather than a month of access. It also means a command-line client cannot
  use it at all — which is why the device flow exists as a separate path rather than an afterthought.
</Accordion>
