# 15 · Token lifecycle

The auth code path exercised end to end on a control plane that is
**not** in open mode. An admin token mints a client token, the CLI
saves it, and a Python worker consumes it from the environment.

Starting with a new server? Follow [Setting Up Lakeshore Service](https://docs.dreamlake.ai/lakeshore/setting-up-lakeshore-service) for the full setup sequence.

## Exercises

- `LAKESHORE_ADMIN_TOKEN` on the control-plane side — admin-bearer
  validation.
- `lakeshore admin tokens create` — the explicit mint path.
- `lakeshore auth login` — both the `--token` path and the
  admin-mint-on-the-fly path, saving to `auth.yml`.
- `LAKESHORE_CLIENT_TOKEN` — the env var the Python worker reads,
  independent of `auth.yml`.

## Requires

- A control plane with `LAKESHORE_ADMIN_TOKEN` set (so it is not in
  open mode). For details, see
  [Auth and secrets](/api/auth-and-secrets.md).
- `LAKESHORE_URL` **unset** while you test the token path — when it is
  set, non-`auth` commands send no bearer token at all.

## Run

```bash
export LAKESHORE_ADMIN_TOKEN=<admin-token-from-.env>

# ─── Option A: mint explicitly, then log in with the plaintext ───────
lakeshore admin tokens create cli-$USER
# prints the plaintext once, plus a ready-made `auth login` line

lakeshore auth login \
  --server http://localhost:8080 \
  --namespace dev \
  --token <plaintext-from-above>

# ─── Option B: let `auth login` mint and save in one step ────────────
# With LAKESHORE_ADMIN_TOKEN set and --token omitted, login mints a
# fresh per-namespace token itself. --name is the label base; the CLI
# appends a timestamp so re-running doesn't collide.
lakeshore auth login \
  --server http://localhost:8080 \
  --namespace dev \
  --name cli-$USER

lakeshore auth status     # expect authMode: token, namespace: dev
```

Then drive a worker from the environment alone — no `auth.yml`
involved:

```bash
unset LAKESHORE_ADMIN_TOKEN
export LAKESHORE_URL=http://localhost:8080
export LAKESHORE_NAMESPACE=dev
export LAKESHORE_CLIENT_TOKEN=$(grep '^token:' ~/.config/lakeshore/auth.yml | awk '{print $2}')

lakeshore worker once --queue default --dry-run   # prints the child argv
lakeshore worker once --queue default
```

> **Warning:** `lakeshore worker` never reads `~/.config/lakeshore/auth.yml`. The
> plane it drains has to come from `--url` / `LAKESHORE_URL`, and the
> token from `--token` / `LAKESHORE_CLIENT_TOKEN`. That is why this step
> re-exports the token explicitly rather than relying on step 1.

## Expected output

`lakeshore auth status`:

```text
Auth OK.
  config path: /Users/you/.config/lakeshore/auth.yml
  server:      http://localhost:8080
  namespace:   dev
  token:       dlk_abcd... (cli-you-2026-01-02T03-04-05)
  lastUsedAt:  2026-01-02T03:05:11.000Z
  authMode:    token
```

`worker once` starts, drains at most one job, and exits without
authentication errors.

## If it fails

| Symptom | Likely cause |
| ------- | ------------ |
| `error: LAKESHORE_ADMIN_TOKEN is required for admin commands.` | Not exported in this shell. Every `admin` subcommand hard-exits 1 without it. |
| `mint failed (401)` on `auth login` | The control plane doesn't recognize the admin token. Make sure your shell value matches what the CP read from `.env` at boot, and restart the CP after changing `.env`. |
| `whoami failed (403)` | The token is scoped to a different namespace than `--namespace`. |
| `authMode: open` | The CP booted with a blank `LAKESHORE_ADMIN_TOKEN` and enforces nothing. Set it and restart. |
| The worker authenticates as nobody | `LAKESHORE_CLIENT_TOKEN` is empty. Re-export it from `auth.yml`. |

## Status

Manual.

## Loop back

That's the full cascade. Once all 15 are green you have working
verification of every major surface. Re-run as features change.

→ Back to [Overview](/dev/happy-paths.md)
