# Authentication Configuration Runbook

> Configure authentication modes, personal access tokens, OAuth providers, JWT secrets, token lifetimes, and sign-up controls.

Source: <https://everruns.com/docs/sre/runbooks/authentication/>

## Overview

This runbook covers configuring and managing authentication for Everruns.

## Authentication Modes

### 1. No Authentication (Development)

Use for local development when authentication isn’t needed:

```bash
export AUTH_MODE=none
```

All requests will be allowed with full admin access.

If you later restart the same database with `AUTH_MODE=admin` or `AUTH_MODE=full`, startup revokes personal access tokens owned by the seeded anonymous admin user so a token minted in no-auth development cannot remain privileged after authentication is enabled.

### 2. Admin Mode (Simple Development)

Use for local development with basic access control:

```bash
export AUTH_MODE=admin
export AUTH_ADMIN_EMAIL=admin@example.com
export AUTH_ADMIN_PASSWORD=your-secure-password
export AUTH_JWT_SECRET=$(openssl rand -hex 32)
```

Only the admin user can authenticate.

### 3. Full Authentication (Production)

Use for production deployments:

```bash
export AUTH_MODE=full
export PUBLIC_APP_URL=https://your-domain.com
export AUTH_JWT_SECRET=$(openssl rand -hex 32)


# Optional: Configure OAuth
export AUTH_GOOGLE_CLIENT_ID=your-google-client-id
export AUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret
export AUTH_GITHUB_CLIENT_ID=your-github-client-id
export AUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
```

## Environment Variables Reference

### Core Settings

| Variable            | Required       | Description                                                                        |
| ------------------- | -------------- | ---------------------------------------------------------------------------------- |
| `AUTH_MODE`         | No             | `none`, `admin`, or `full` (default: `none`)                                       |
| `PUBLIC_APP_URL`    | For OAuth      | Public app origin used to derive auth callback URLs                                |
| `AUTH_BASE_URL`     | No             | Override callback base URL when it differs from `PUBLIC_APP_URL` + `API_PREFIX`    |
| `AUTH_LOGIN_ORIGIN` | No             | Trusted remote origin hosting `/login`; set identically for server and UI runtimes |
| `AUTH_JWT_SECRET`   | For admin/full | JWT signing secret (min 32 chars recommended)                                      |

### Admin Mode Settings

| Variable              | Required         | Description         |
| --------------------- | ---------------- | ------------------- |
| `AUTH_ADMIN_EMAIL`    | Yes (admin mode) | Admin user email    |
| `AUTH_ADMIN_PASSWORD` | Yes (admin mode) | Admin user password |

### JWT Settings

| Variable                          | Required | Description                                          |
| --------------------------------- | -------- | ---------------------------------------------------- |
| `AUTH_JWT_ACCESS_TOKEN_LIFETIME`  | No       | Access token lifetime in seconds (default: 900)      |
| `AUTH_JWT_REFRESH_TOKEN_LIFETIME` | No       | Refresh token lifetime in seconds (default: 2592000) |

### Feature Toggles

| Variable                | Required | Description                                |
| ----------------------- | -------- | ------------------------------------------ |
| `AUTH_DISABLE_PASSWORD` | No       | Set to `true` to disable password login    |
| `AUTH_DISABLE_SIGNUP`   | No       | Set to `true` to disable user registration |

### Google OAuth

| Variable                      | Required         | Description                           |
| ----------------------------- | ---------------- | ------------------------------------- |
| `AUTH_GOOGLE_CLIENT_ID`       | For Google OAuth | Google OAuth client ID                |
| `AUTH_GOOGLE_CLIENT_SECRET`   | For Google OAuth | Google OAuth client secret            |
| `AUTH_GOOGLE_REDIRECT_URI`    | No               | Custom redirect URI                   |
| `AUTH_GOOGLE_ALLOWED_DOMAINS` | No               | Comma-separated allowed email domains |

### GitHub OAuth

| Variable                    | Required         | Description                |
| --------------------------- | ---------------- | -------------------------- |
| `AUTH_GITHUB_CLIENT_ID`     | For GitHub OAuth | GitHub OAuth client ID     |
| `AUTH_GITHUB_CLIENT_SECRET` | For GitHub OAuth | GitHub OAuth client secret |
| `AUTH_GITHUB_REDIRECT_URI`  | No               | Custom redirect URI        |

## Common Tasks

### Generate JWT Secret

```bash
# Using OpenSSL
openssl rand -hex 32


# Using Python
python3 -c "import secrets; print(secrets.token_hex(32))"
```

### Verify Authentication is Working

```bash
# Check auth config endpoint
curl http://localhost:9300/api/v1/auth/config


# Should return:
# {"mode":"none","password_auth_enabled":false,"oauth_providers":[],"signup_enabled":false}


# For admin mode:
curl -X POST http://localhost:9300/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"your-password"}'
```

### Create Personal Access Token

```bash
# Login first to get access token
TOKEN=$(curl -s -X POST http://localhost:9300/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"password"}' | jq -r '.access_token')


# Create personal access token
curl -X POST http://localhost:9300/api/v1/auth/personal-access-tokens \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-token"}'


# Response includes the full token - save it, it's shown only once
```

### Revoke Personal Access Token

```bash
curl -X DELETE http://localhost:9300/api/v1/auth/personal-access-tokens/{token_id} \
  -H "Authorization: Bearer $TOKEN"
```

### Force Logout User

Delete their refresh tokens from database:

```sql
DELETE FROM refresh_tokens WHERE user_id = 'user-uuid-here';
```

## Troubleshooting

### “Authentication required” when AUTH\_MODE=none

* Verify `AUTH_MODE` environment variable is set correctly
* Restart the server after changing environment variables

### JWT Validation Fails

* Ensure `AUTH_JWT_SECRET` hasn’t changed
* Check token hasn’t expired
* Verify the token is for the correct environment

### OAuth Redirect Fails

* Verify `AUTH_BASE_URL` matches the OAuth app configuration, or that `PUBLIC_APP_URL` derives the expected `{PUBLIC_APP_URL}/api` base
* Check that redirect URI in provider matches `{AUTH_BASE_URL}/v1/auth/callback/{provider}` or `{PUBLIC_APP_URL}/api/v1/auth/callback/{provider}`
* If you set `AUTH_BASE_URL`, ensure it already includes your REST API prefix (default: `/api`)
* Ensure client ID and secret are correct

### Password Login Returns Unauthorized

* In admin mode: check `AUTH_ADMIN_EMAIL` and `AUTH_ADMIN_PASSWORD` match
* In full mode with password disabled: check `AUTH_DISABLE_PASSWORD` isn’t set
* Verify user exists and password is correct

## Database Migration

Authentication tables are included in the base schema and **auto-applied on server startup**.

No manual migration step is required. To check migration status:

```bash
# Via admin container
docker run --rm -e DATABASE_URL="$DATABASE_URL" everruns-admin migrate-info
```

## Health Check

The `/health` endpoint shows current auth mode:

```bash
curl http://localhost:9300/health
# {"status":"ok","version":"0.2.0","auth_mode":"None"}
```

## Agent Discovery Endpoints

The server publishes public documents so an AI agent that only knows the origin can find the MCP endpoint and work out how to authenticate before it has any credentials:

| Path                                    | Contents                                                                                                                                |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `/llms.txt`                             | What the server offers and where to start: MCP endpoint, auth.md, REST API, OpenAPI, docs                                               |
| `/auth.md`                              | How to obtain credentials: OAuth 2.1 with dynamic client registration for the MCP endpoint, and personal access tokens for the REST API |
| `/.well-known/mcp/server-card.json`     | MCP Server Card (SEP-1649): server identity, transport, capabilities, and where its OAuth metadata lives                                |
| `/.well-known/api-catalog`              | API catalog (RFC 9727): the REST API with its OpenAPI document, and the MCP endpoint                                                    |
| `/.well-known/oauth-protected-resource` | Root copy of the MCP endpoint’s protected resource metadata (RFC 9728), for clients that probe the bare path                            |
| `/robots.txt`, `/sitemap.xml`           | Keep crawlers out of the console, allow the documents above, declare Content Signals                                                    |

All are generated from the running configuration rather than hardcoded, so a self-hosted deployment describes itself:

* URLs come from `AUTH_BASE_URL` / `BASE_URL` (or `PUBLIC_APP_URL` plus `API_PREFIX`), the same value used for OAuth callbacks and the MCP resource binding. Set them to the public origin, not an internal container address, or the documents will advertise URLs an agent cannot reach.
* Content follows `AUTH_MODE`. Under `AUTH_MODE=none` the documents state that no credentials are required instead of describing an OAuth flow that is not enforced.
* The server card `version` and `serverInfo.version` are the running server’s release version, the same value `/health` reports. A release updates them with the binary.

### Reverse proxy configuration (required)

`/auth.md`, `/llms.txt`, `/robots.txt`, and `/sitemap.xml` sit at the server root, so a deployment that puts the UI on `/` must route these paths to the server explicitly. Without the rule the request falls through to the UI and returns its 404, and the document is unreachable even though the server serves it. The proxy also owns two behaviours of `/` itself: a `Link` header pointing at the discovery documents, and `Accept: text/markdown` answered with `/llms.txt`, since the console renders nothing without JavaScript.

The bundled [local proxy](https://github.com/everruns/everruns/blob/main/local/Caddyfile), [Railway proxy](https://github.com/everruns/everruns/blob/main/infra/railway/caddy/Caddyfile), and [Docker Compose stack](https://github.com/everruns/everruns/blob/main/examples/docker-compose-full.yaml) already include it. For a custom proxy, add these rules next to wherever `/.well-known/*` is routed:

```caddyfile
handle /.well-known/* {
  reverse_proxy server:9000
}
@agent_docs path /auth.md /llms.txt /robots.txt /sitemap.xml
handle @agent_docs {
  reverse_proxy server:9000
}
@agent_markdown {
  path /
  header Accept *text/markdown*
}
handle @agent_markdown {
  header Vary Accept
  rewrite * /llms.txt
  reverse_proxy server:9000
}
handle {
  header / +Link "</.well-known/api-catalog>; rel=\"api-catalog\", </llms.txt>; rel=\"describedby\"; type=\"text/markdown\""
  reverse_proxy ui:9305
}
```

nginx (documents only):

```nginx
location ~ ^/(auth\.md|llms\.txt|robots\.txt|sitemap\.xml)$ {
    proxy_pass http://server:9000;
}
```

Verify after deploying:

```bash
curl -fsS https://your-host/auth.md | head -1        # expect "# auth.md"
curl -fsS -H 'Accept: text/markdown' https://your-host/ | head -1   # expect "# Everruns"
curl -fsSI https://your-host/ | grep -i '^link'      # expect rel="api-catalog"
curl -fsS https://your-host/.well-known/mcp/server-card.json
```

A response of `text/html` rather than `text/markdown` means the UI answered and the proxy rule is missing.

## Security Best Practices

1. **Never commit secrets**: Use environment variables or secret management
2. **Rotate JWT secret**: Change `AUTH_JWT_SECRET` periodically (invalidates all tokens)
3. **Use HTTPS**: Always use HTTPS in production for OAuth callbacks
4. **Limit OAuth domains**: Use `AUTH_GOOGLE_ALLOWED_DOMAINS` to restrict access
5. **Monitor personal access token usage**: Track `last_used_at` for suspicious activity
6. **Set token expiration**: Use shorter `AUTH_JWT_ACCESS_TOKEN_LIFETIME` for higher security