# Authentication and API tokens

> Local session login, OAuth Google and GitHub, default admin account, password change, Bearer API tokens for REST and GraphQL, and permission-scoped token lifecycle.

- Repository: vxcontrol/pentagi
- GitHub: https://github.com/vxcontrol/pentagi
- Human docs: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5
- Complete Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/llms-full.txt

## Source Files

- `backend/pkg/server/auth/auth_middleware.go`
- `backend/pkg/server/auth/api_token_jwt.go`
- `backend/pkg/server/services/auth.go`
- `backend/pkg/server/services/api_tokens.go`
- `frontend/src/pages/settings/settings-api-tokens.tsx`
- `backend/pkg/graph/schema.graphqls`
- `README.md`

---

---
title: "Authentication and API tokens"
description: "Local session login, OAuth Google and GitHub, default admin account, password change, Bearer API tokens for REST and GraphQL, and permission-scoped token lifecycle."
---

PentAGI authenticates browser clients with an encrypted `auth` session cookie and automation clients with HS256 JWT Bearer API tokens. Both paths run under `/api/v1`, share `COOKIE_SIGNING_SALT` for key derivation, and attach role privileges (`prm`) used by REST handlers and GraphQL permission checks.

## Auth model

| Mode | Client surface | Credential | Middleware | Session type (`tid`) |
|---|---|---|---|---|
| Local login | Web UI | Email + password | Cookie session after `POST /auth/login` | `local` |
| OAuth | Web UI (Google / GitHub) | OAuth code exchange | Cookie session after callback | `oauth` |
| API token | REST / GraphQL / scripts | `Authorization: Bearer <jwt>` | `AuthTokenRequired` | `api` |

```mermaid
flowchart TB
  subgraph clients [Clients]
    UI[Web UI]
    CLI[Scripts / automation]
  end

  subgraph api ["/api/v1"]
    Pub["TryAuth: /info, /auth/*"]
    Pwd["AuthUserRequired + local: PUT /user/password"]
    Priv["AuthTokenRequired: GraphQL, flows, tokens REST, …"]
  end

  subgraph store [State]
    Cookie["Cookie store name=auth"]
    JWT["JWT API tokens in api_tokens"]
    Users[(users + privileges)]
  end

  UI --> Pub
  UI --> Pwd
  UI --> Priv
  CLI --> Priv
  Pub --> Cookie
  Pwd --> Cookie
  Priv --> Cookie
  Priv --> JWT
  Cookie --> Users
  JWT --> Users
```

### Middleware selection

| Handler | Behavior |
|---|---|
| `TryAuth` | Optional: Bearer token, then cookie; continues as guest if neither succeeds |
| `AuthUserRequired` | Cookie session only (password change) |
| `AuthTokenRequired` | Bearer API token first, then cookie; 401 if neither succeeds |

Request context keys after successful auth: `uid`, `uhash`, `rid`, `tid`, `prm`, `gtm`, `exp` (and for API tokens: `cpt=automation`, `uuid`).

## Default admin account

A fresh database seed creates one local administrator:

| Field | Value |
|---|---|
| Email | `admin@pentagi.com` |
| Password | `admin` |
| Role | Admin (`role_id = 1`) |
| Status | `active` |
| `password_change_required` | `true` |

There is no public self-service sign-up on the login page. Additional local users are created by an administrator via the Users REST API (`/api/v1/users/`). If the admin password is lost, use the installer maintenance path to reset `admin@pentagi.com`.

<Warning>
Change the default password before production use. Leave `COOKIE_SIGNING_SALT` as the example value `salt` only for throwaway local demos — API token create/validate is disabled while that default remains.
</Warning>

## Local session login

:::endpoint POST /api/v1/auth/login Local email/password login
**Body**

| Field | Type | Rules |
|---|---|---|
| `mail` | string | Required; user email |
| `password` | string | Required |

**Success:** `200` with empty data object; sets the `auth` session cookie.

**Failure cases**

| HTTP | Condition |
|---|---|
| 400 | Invalid body |
| 401 | Unknown user or wrong password |
| 403 | Inactive user, or external-style role (`role_id = 100`) |
| 500 | Privilege load / session save failure |

On success the server loads privileges for the user’s role, stores them in the session, and returns:

- Cookie name: `auth`
- `HttpOnly: true`
- `Secure: true` when the request is TLS
- `SameSite: Lax`
- `Path: /api/v1`
- `MaxAge`: session timeout (4 hours)
:::

### Session claims

| Key | Meaning |
|---|---|
| `uid` | User ID |
| `uhash` | Per-user hash (invalidates sessions if the user row hash changes) |
| `rid` | Role ID |
| `tid` | User type: `local` / `oauth` / (API path sets `api`) |
| `prm` | Privilege name list |
| `gtm` | Issued-at (unix) |
| `exp` | Expires-at (unix) |
| `uuid` / `uname` | Display identity |

Cookie keys are derived from `COOKIE_SIGNING_SALT` with PBKDF2-HMAC-SHA512 (210000 iterations).

### Session lifetime and refresh

- **Timeout:** 4 hours (`SessionTimeout` in the auth service config).
- **Validation:** middleware rejects expired sessions, blocked/created users, deleted users, and `uhash` mismatches.
- **Sliding refresh:** `GET /api/v1/info` refreshes the cookie when the session is at least 5 minutes old (unless `refresh_cookie=false`). Near final expiry, a failed refresh returns auth required.

:::endpoint GET /api/v1/info Current auth and system info
Optional auth. Returns guest info when no valid session/token is present.

| Response field | Notes |
|---|---|
| `type` | `guest`, `user`, or `api` |
| `user` / `role` | Present when authenticated |
| `privileges` | Effective privilege list |
| `providers` | Configured OAuth provider names |
| `oauth` | `true` when `tid` is `oauth` |
| `issued_at` / `expires_at` | Session window |
| `develop` | Develop-mode flag |

For API-token auth (`cpt=automation`), privilege names starting with `users.`, `roles.`, `settings.user.`, or `settings.tokens.` are stripped from the `/info` response.
:::

:::endpoint GET /api/v1/auth/logout End session
Clears the cookie (`MaxAge: -1`) and redirects to `return_uri` (default `/`).
:::

## OAuth (Google and GitHub)

OAuth clients are registered only when both of these are true:

1. `PUBLIC_URL` parses successfully (redirect base).
2. Provider client ID and secret are non-empty.

| Env var | Provider |
|---|---|
| `OAUTH_GOOGLE_CLIENT_ID` / `OAUTH_GOOGLE_CLIENT_SECRET` | Google |
| `OAUTH_GITHUB_CLIENT_ID` / `OAUTH_GITHUB_CLIENT_SECRET` | GitHub |
| `PUBLIC_URL` | Redirect base (example: `https://localhost:8443`) |

Callback URL registered with the IdP:

```text
{PUBLIC_URL}/api/v1/auth/login-callback
```

Google POST callbacks also require CORS allow for `accounts.google.com` when origins are not `*`.

### Flow

<Steps>
  <Step title="Start authorize">
    Browser opens `GET /api/v1/auth/authorize?provider=google|github&return_uri=…`.

    Server sets short-lived `state` and `nonce` cookies (5 minutes). State is HMAC-signed JSON including `provider`, optional `return_uri`, and expiry. Google uses `SameSite=None` (POST form callback); GitHub uses `SameSite=Lax` (GET callback).
  </Step>
  <Step title="IdP callback">
    - Google: `POST /api/v1/auth/login-callback` (code + state in form body)
    - GitHub: `GET /api/v1/auth/login-callback?code=…&state=…`

    State cookie must match; signature and expiry are checked.
  </Step>
  <Step title="User provision">
    Email is resolved from the OAuth token. If no `type=oauth` user exists for that email, one is created with:

    - `role_id` = User (`2`)
    - `status` = `active`
    - `type` = `oauth`
    - default user preferences row
  </Step>
  <Step title="Session">
    Same cookie session shape as local login. Temporary `state`/`nonce` cookies are cleared. Redirect adds `status=success` when `return_uri` was provided.
  </Step>
</Steps>

The UI launches OAuth in a popup to `/api/v1/auth/authorize?provider=…&return_uri=/oauth/result`. Available providers are listed in `/info` → `providers`.

<Note>
OAuth users cannot call `PUT /user/password` (local type required). Password change UI is limited to `type === 'local'`.
</Note>

## Password change

:::endpoint PUT /api/v1/user/password Change current local user password
**Guards:** `AuthUserRequired` + local user type (`tid=local`).

**Body**

| Field | Type | Rules |
|---|---|---|
| `current_password` | string | Required; min 5, max 100; must differ from new password |
| `password` | string | Required; max 100; strong-password (`stpass`) |
| `confirm_password` | string | Must equal `password` |

**Strong password (`stpass`)** — either:

- length **> 15**, or
- length **≥ 8** and contains all of: digit, lowercase, uppercase, special from `!@#$&*`

Frontend mirrors the same rules in the password-change form.

**Effects**

- bcrypt-hashes the new password
- sets `password_change_required = false`
- returns `200` empty success body

**Errors**

| HTTP | Case |
|---|---|
| 400 | Validation failure / invalid new password |
| 403 | Wrong current password, or not a local user |
| 404 | User not found |
:::

Default admin seeds with `password_change_required=true`. The login page can show an “Update Password” step after first local login (skippable in the UI, but production instances should complete the change).

## Bearer API tokens

API tokens are long-lived JWTs for programmatic access to the private REST surface and GraphQL. Metadata lives in `api_tokens`; the secret JWT string is returned **only at creation**.

### Prerequisites

| Requirement | Detail |
|---|---|
| `COOKIE_SIGNING_SALT` | Must be set and **not** equal to `salt` or empty |
| Auth for create | Cookie session (`local` or `oauth`) via UI / GraphQL; REST create requires authenticated principal |
| Privileges (GraphQL) | `settings.tokens.create` / `view` / `edit` / `delete` / `subscribe` |
| Admin privilege | `settings.tokens.admin` — Admin role only; list/manage across users |

Role privilege seeds:

| Privilege | Admin | User |
|---|---|---|
| `settings.tokens.create` | ✓ | ✓ |
| `settings.tokens.view` | ✓ | ✓ |
| `settings.tokens.edit` | ✓ | ✓ |
| `settings.tokens.delete` | ✓ | ✓ |
| `settings.tokens.subscribe` | ✓ | ✓ |
| `settings.tokens.admin` | ✓ | — |

### Token lifecycle

```mermaid
stateDiagram-v2
  [*] --> active: create
  active --> revoked: update status=revoked
  active --> expired: created_at + ttl elapsed
  revoked --> active: update status=active
  active --> [*]: soft delete
  revoked --> [*]: soft delete
  expired --> [*]: soft delete
```

| Status | Stored in DB? | Meaning |
|---|---|---|
| `active` | yes | Usable if JWT not expired and TTL window not elapsed |
| `revoked` | yes | Rejected by middleware |
| `expired` | derived in API responses | `created_at + ttl` in the past while status was still `active` |

TTL constraints: **60 … 94_608_000** seconds (1 minute … 3 years). Optional `name` is unique per user among non-deleted tokens (max 100 REST / 255 UI). Soft delete sets `deleted_at`.

### JWT shape

Signed with HS256; signing key = PBKDF2 from `COOKIE_SIGNING_SALT`.

| Claim | Description |
|---|---|
| `tid` | Public token ID (10-char id, not the JWT) |
| `uid` | Owner user ID |
| `rid` | Role ID snapshot |
| `uhash` | User hash at issue time |
| `sub` | `api_token` |
| `iat` / `exp` | Issued / expires (from TTL) |

Validation path:

1. `Authorization: Bearer <token>` present
2. Salt is not default
3. JWT signature + expiry valid
4. Row exists, not deleted, `status=active`
5. User not blocked/deleted; `uhash` still matches
6. Privileges = role privileges **plus** `pentagi.automation`

Token status lookups are cached for 5 minutes; create/update/delete invalidates cache.

### REST token endpoints

All under `/api/v1/tokens` with `AuthTokenRequired` (cookie or Bearer).

| Method | Path | Behavior |
|---|---|---|
| `POST` | `/tokens/` | Create; body `{ "name"?: string, "ttl": number }`; returns `APITokenWithSecret` including `token` |
| `GET` | `/tokens/` | List; non-admins see own tokens only |
| `GET` | `/tokens/:tokenID` | Get one |
| `PUT` | `/tokens/:tokenID` | Update `name` and/or `status` (`active` / `revoked`; `expired` input is stored as revoked) |
| `DELETE` | `/tokens/:tokenID` | Soft delete |

Create is rejected with “token creation is disabled with default salt” when salt is unset or `salt`.

### GraphQL token surface

Token management mutations/queries require a **user session** (`local` or `oauth`). API tokens (`tid=api`) cannot create, update, delete, list, or subscribe to token events.

| Operation | Permission |
|---|---|
| `createAPIToken(input: { name, ttl })` | `settings.tokens.create` |
| `apiTokens` / `apiToken(tokenId)` | `settings.tokens.view` |
| `updateAPIToken(tokenId, input)` | `settings.tokens.edit` |
| `deleteAPIToken(tokenId)` | `settings.tokens.delete` |
| `apiTokenCreated` / `Updated` / `Deleted` | `settings.tokens.subscribe` |

Types: `APIToken`, `APITokenWithSecret` (includes `token` once), `TokenStatus`, `CreateAPITokenInput`, `UpdateAPITokenInput`.

### UI

Settings → **API tokens** (`/settings/api-tokens`):

- Create with name + expiration date (UI converts to TTL, minimum 60s)
- List with active / revoked / expired display
- Edit name/status, revoke, delete
- Copy secret once after create
- Links to GraphQL Playground and Swagger under `/api/v1`

### Using a token

```bash
# REST
curl -sk \
  -H "Authorization: Bearer $PENTAGI_TOKEN" \
  https://localhost:8443/api/v1/info

# GraphQL
curl -sk \
  -H "Authorization: Bearer $PENTAGI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ providers { name type } }"}' \
  https://localhost:8443/api/v1/graphql
```

API-token requests receive the owner’s role privileges plus `pentagi.automation`. GraphQL permission checks use that privilege list the same way as cookie sessions, except token-management operations explicitly reject non-user sessions.

## Configuration

| Variable | Role | Notes |
|---|---|---|
| `COOKIE_SIGNING_SALT` | Cookie + JWT key material | Change from `salt`; required for API tokens |
| `PUBLIC_URL` | OAuth redirect base | Required for OAuth client registration |
| `OAUTH_GOOGLE_CLIENT_ID` / `SECRET` | Google SSO | Optional |
| `OAUTH_GITHUB_CLIENT_ID` / `SECRET` | GitHub SSO | Optional |

Session duration is fixed at **4 hours** in server wiring (not an env key today).

## Failure modes and troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| Login 401 | Wrong credentials or no local password | Use `admin@pentagi.com` / `admin` on fresh install; reset via installer if needed |
| Login 403 inactive | User status not `active` | Unblock/activate user |
| OAuth “provider not initialized” | Missing client ID/secret or invalid `PUBLIC_URL` | Set env vars and restart |
| OAuth state errors | Expired/mismatched state cookie (>5 min) or cookie SameSite issues | Retry quickly; ensure HTTPS for Google (`SameSite=None` + Secure) |
| Password change 403 | OAuth user or wrong current password | Only `local` users; re-enter current password |
| Token create fails (default salt) | `COOKIE_SIGNING_SALT` empty or `salt` | Set a strong unique salt and redeploy |
| Bearer 401 “token validation disabled” | Same default salt | Same as above |
| Bearer 401 revoked / not found | Soft-deleted or revoked token | Create a new token |
| Bearer 401 hash mismatch | User hash changed or token from another install | Re-issue tokens after salt/user changes |
| GraphQL token mutations unauthorized | Called with API token instead of browser session | Manage tokens from UI or cookie session |
| Session dropped after salt change | Cookie keys re-derived | Users must log in again; re-issue API tokens |

## Operational checklist

<Steps>
  <Step title="Harden signing salt">
    Set `COOKIE_SIGNING_SALT` to a unique value before enabling automation tokens.
  </Step>
  <Step title="First admin login">
    Open `https://localhost:8443`, sign in as `admin@pentagi.com` / `admin`, change password under the forced or settings flow.
  </Step>
  <Step title="Optional OAuth">
    Configure `PUBLIC_URL` and Google/GitHub client credentials; confirm providers appear on the login page and in `/info`.
  </Step>
  <Step title="Issue API tokens">
    Settings → API tokens (or GraphQL `createAPIToken`). Copy the secret once. Verify with `GET /api/v1/info` using `Authorization: Bearer …`.
  </Step>
  <Step title="Revoke when done">
    Set status to `revoked` or delete the token; cache invalidation is immediate for that `token_id`.
  </Step>
</Steps>

## Related pages

<CardGroup cols={2}>
  <Card title="Quickstart" href="/quickstart">
    First login, change default admin password, and run a flow.
  </Card>
  <Card title="Interactive installer" href="/installer">
    Guided deploy and admin password reset maintenance paths.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    `COOKIE_SIGNING_SALT`, `PUBLIC_URL`, and OAuth keys.
  </Card>
  <Card title="REST API" href="/rest-api">
    Full Gin route map including auth, users, and tokens.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    Queries, mutations, and subscriptions for API tokens and live streams.
  </Card>
  <Card title="Overview" href="/overview">
    Product surface: UI, REST/GraphQL under `/api/v1`, and first-flow path.
  </Card>
</CardGroup>
