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

# Authentication & Security

> How clients authenticate against MCPHub

## Overview

MCPHub supports several stacked authentication paths. The same middleware (`src/middlewares/auth.ts`) evaluates them in order on every authenticated request:

1. **Bearer key** — a static token created from the dashboard or `/api/auth/keys`.
2. **MCPHub OAuth access token** — issued by MCPHub's own authorization server (optional).
3. **Better Auth session cookie** — issued by GitHub / Google login (optional).
4. **JWT** — issued by `POST /api/auth/login`, sent via the `x-auth-token` header or `?token=` query.
5. **`skipAuth` guest** — only when `systemConfig.routing.skipAuth` is `true`, the dashboard API treats unauthenticated calls as a synthetic admin `guest` user.

All persistent user records are stored as `{ username, password, isAdmin }`. There are no `manager` or `viewer` roles; permissions are binary (admin or not). Social accounts that log in via Better Auth live in a separate `mcphub_better_auth_*` table but are mapped to the same `username` / `isAdmin` shape at request time.

### Per-server visibility

Each server has a `visibility` setting (`private` / `group` / `public`) that controls which non-admin users can discover and call the server. Admins always have access. The same filter is applied to dashboard listings, direct tool and prompt calls, group routes, and `$smart` routing.

* `'private'` (default): only the server's `owner` (and admins) can see it. Servers seeded from `mcp_settings.json` migrate in with `'private'` to preserve the historical admin-only behaviour.
* `'public'`: every authenticated user can see and call the server. Use this for servers you want to share across all users of the same MCPHub instance — including social-login users created by Better Auth.
* `'group'`: the owner and usernames listed in the server's `sharedWithUsers` array can see and call it. Sharing grants runtime access only; selected users cannot edit or delete the server.

The visibility is editable per server from the dashboard (Server form → Advanced Options → Visibility). Edit an existing server to select local usernames for `'group'` sharing. For deployments where every user should see every server (single-user self-hosted, small trusted team), an operator can set the relevant servers to `'public'` from the dashboard.

## Login (JWT)

The dashboard and any script that does not present a bearer key uses the JWT flow:

```bash theme={null}
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{ "username": "admin", "password": "your-password" }'
```

The response includes a JWT. Send it on subsequent requests using the `x-auth-token` header:

```bash theme={null}
curl http://localhost:3000/api/servers \
  -H "x-auth-token: <jwt>"
```

The JWT is signed with `JWT_SECRET`. If `JWT_SECRET` is not set, MCPHub generates an ephemeral random secret at startup; this is fine for development but means every restart invalidates outstanding tokens. **Set `JWT_SECRET` explicitly in production.**

Issued JWTs expire after a fixed 24 hours (`TOKEN_EXPIRY` in `src/controllers/authController.ts`); the lifetime is not configurable via environment variables.

The first admin account is created at startup by `initializeDefaultUser()`. If no users exist, MCPHub creates `admin` and uses `ADMIN_PASSWORD` if set. When running through `pnpm dev`, it uses `admin123`; other starts generate a random password and print it to the server logs.

## Bearer keys

Bearer keys are long-lived static tokens managed at `/api/auth/keys`. Tokens are generated by the
backend and returned in plaintext only once, in the create response. Later list and update responses
contain a masked value.

| Field | Description |
| - | - |
| `name` | Human-readable label. |
| `token` | The secret value sent in the `Authorization: Bearer ...` header. |
| `kind` | `system` for operator-managed scoped keys, or `user` for keys that inherit an owner's visibility. |
| `owner` | Required for user-level keys. |
| `accessType` | `all` (dashboard + MCP), or a scoped value enforced inside `sseService.ts` for MCP-only access. |
| `enabled` | Disabled keys are ignored without being deleted. |

Admins can create and manage all keys. Regular users can create, rename, enable, disable and delete
their own user-level keys from the settings page. User-level keys are MCP transport credentials only:
they are never accepted by the dashboard / management API. When used on `/mcp`, `/mcp/$smart` or a
group/server endpoint, MCPHub resolves the current owner record and applies that user's live server
visibility. The optional `/:user/mcp/...` routes remain supported, but the URL username must match the
key owner.

System-level keys preserve the original scoped-key behavior. Only system-level keys with
`accessType === 'all'` are accepted by the dashboard / management API middleware. Restricted
system-level keys are enforced at the MCP transport layer.

The header name can be customized via `systemConfig.routing.bearerAuthHeaderName` (default `Authorization`). The legacy `systemConfig.routing.bearerAuthKey` field is preserved only for one-time migration into the bearer key store.

To globally turn off bearer auth on MCP endpoints, set `systemConfig.routing.enableBearerAuth` to `false`.

## MCPHub OAuth authorization server

When `systemConfig.oauthServer.enabled` is `true`, MCPHub exposes an OAuth 2.0 / OIDC-compatible server (`@node-oauth/oauth2-server`). Endpoints:

| Method & Path | Purpose |
| - | - |
| `GET /oauth/authorize`, `POST /oauth/authorize` | Authorization endpoint (PKCE supported). |
| `POST /oauth/token` | Token endpoint. |
| `GET /oauth/userinfo` | OIDC-style userinfo (also validates an access token). |
| `GET /.well-known/oauth-authorization-server` | RFC 8414 metadata. |
| `GET /.well-known/oauth-protected-resource` | Protected resource metadata. |
| `POST /oauth/register`, `GET/PUT/DELETE /oauth/register/:clientId` | RFC 7591 dynamic client registration. |
| `GET/POST/PUT/DELETE /api/oauth/clients[...]` | Admin-only client CRUD. |

OAuth access tokens issued here are accepted by `src/middlewares/auth.ts`.

## Better Auth (GitHub / Google / local OIDC)

Better Auth is optional and initialized once at process startup. That means any change to either `BETTER_AUTH_*` environment variables or stored `systemConfig.auth.betterAuth` values still requires a restart.

Non-secret Better Auth settings can now live either in environment variables or in `systemConfig.auth.betterAuth` (from `mcp_settings.json` or the database-backed system config). The priority order is:

1. `BETTER_AUTH_*` environment variables
2. `systemConfig.auth.betterAuth`
3. Built-in defaults

Provider credentials remain environment-variable only. Required variables (see `.env.example`):

* `BETTER_AUTH_ENABLED` — master switch for Better Auth.
* `BETTER_AUTH_URL` — public base URL of MCPHub (used to build redirect URIs).
* `BETTER_AUTH_BASE_PATH` — optional mount path override for the Better Auth handler.
* `BETTER_AUTH_TRUSTED_ORIGINS` — optional extra trusted origins (comma-separated, whitespace-separated, or JSON array).
* `BETTER_AUTH_DISABLE_PASSWORD_LOGIN` — turn off username/password login while an SSO provider is enabled (see [SSO-only sign-in](#sso-only-sign-in)).
* `BETTER_AUTH_GOOGLE_ENABLED` — enable / disable Google login when credentials are present.
* `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` — for Google login.
* `BETTER_AUTH_GITHUB_ENABLED` — enable / disable GitHub login when credentials are present.
* `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` — for GitHub login.
* `BETTER_AUTH_OIDC_ENABLED` — enable / disable the generic OIDC provider.
* `BETTER_AUTH_OIDC_PROVIDER_ID` — OIDC provider identifier (defaults to `oidc`).
* `BETTER_AUTH_OIDC_DISCOVERY_URL` — discovery URL for a local issuer.
* `BETTER_AUTH_OIDC_SCOPES` — optional requested scopes for local OIDC login.
* `BETTER_AUTH_OIDC_PKCE` — optional PKCE toggle for local OIDC login.
* `BETTER_AUTH_OIDC_PROMPT` — optional `prompt` parameter for local OIDC login.
* `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` — for a local OIDC provider.

For a local issuer (for example Keycloak, Authentik, or Dex), you can either configure the metadata entirely through environment variables, or keep the non-secret values in `systemConfig.auth.betterAuth.providers.oidc` with:

* `enabled`
* `providerId`
* `discoveryUrl`
* optional `scopes`, `pkce`, and `prompt`

The `discoveryUrl` should point at the provider's `/.well-known/openid-configuration` endpoint. `OIDC_DISCOVERY_URL` remains supported as a legacy alias for `BETTER_AUTH_OIDC_DISCOVERY_URL`, and it can still be referenced from config files via `${OIDC_DISCOVERY_URL}`. The login page shows a generic **Continue with OIDC** button when this provider is enabled.

If the dashboard runs behind a different public origin, set `BETTER_AUTH_TRUSTED_ORIGINS` (or `systemConfig.auth.betterAuth.trustedOrigins`) to allow that origin to start the login flow. If no explicit trusted origins are configured, MCPHub automatically trusts the origins from `BETTER_AUTH_URL` and `systemConfig.install.baseUrl` when they are set.

<Note>
  Better Auth in MCPHub currently requires PostgreSQL-backed storage (`DB_URL`). File-only mode does not support Better Auth sessions, including local OIDC login.
</Note>

When enabled, Better Auth mounts at `${BASE_PATH}${betterAuthConfig.basePath}` (default `/api/auth/better`). `BETTER_AUTH_BASE_PATH` overrides the stored `betterAuth.basePath` value at startup. A valid Better Auth session cookie counts as authentication for the dashboard API. The mapped MCPHub user is admin only if a local user with the same username exists and is marked admin.

Local OIDC login follows the same mapping rule as GitHub / Google login: MCPHub resolves a local username from the Better Auth session (`email ?? name ?? id`). If that username does not exist yet, MCPHub auto-creates a **non-admin** local user. OIDC login never auto-promotes a user to admin.

### SSO-only sign-in

To allow sign-in only through the SSO providers, turn on **Disable password login** in Settings → Better Auth, or set `systemConfig.auth.betterAuth.disablePasswordLogin: true`. The login page then shows only the provider buttons, and `POST /api/auth/login` answers `403`. Unlike the other Better Auth settings, it takes effect without a restart.

* **No lockout:** it applies only while at least one provider is actually enabled (Better Auth on, database mode, credentials present) and was enabled when MCPHub started, since a provider's sign-in route is set up only at startup. Otherwise password login stays available: after enabling a new provider, restart MCPHub before the switch applies.
* **Recovery:** `BETTER_AUTH_DISABLE_PASSWORD_LOGIN` overrides the stored setting in both directions. If the identity provider is unavailable, set it to `false` and restart to sign in with a local password again. Keep a strong password on the local admin for that case.
* **Scripts and the CLI** cannot use the password login while it is disabled. Use a system-level bearer key with access type `all`, which authenticates the management API (`mcphub config set-token <key> --bearer` for the CLI).
* Tokens issued by earlier password logins stay valid until they expire. Changing a local password still works, so the recovery password can be rotated.

## Forward auth (JWKS-verified gateway identity)

When MCPHub runs behind an API gateway or identity-aware proxy that already authenticates users, forward auth lets MCPHub trust the gateway's JWT instead of asking users to log in again. MCPHub verifies the token itself against the identity provider's JWKS, so it remains the security boundary. Forward auth is **off by default** and applies to both the dashboard API and the MCP transport routes (`/mcp/*`, `/sse/*`, and their user-scoped variants).

```json theme={null}
"systemConfig": {
  "auth": {
    "forwardAuth": {
      "enabled": true,
      "mode": "jwks",
      "jwks": {
        "uri": "https://idp.example.com/.well-known/jwks.json",
        "issuer": "https://idp.example.com/",
        "audience": "mcphub",
        "algorithms": ["RS256", "ES256"]
      },
      "usernameClaim": "preferred_username",
      "emailClaim": "email",
      "autoCreate": true
    }
  }
}
```

The JWT is read from the configured bearer header (`Authorization: Bearer <jwt>` by default).

**Verification**

* The token is verified against `jwks.uri`. Keys are cached and refetched when a token carries an unknown `kid`, so key rotation at the identity provider needs no restart.
* `iss`, `aud`, and `exp` are always validated; `sub` and `exp` are required. A 30-second clock tolerance applies.
* Only asymmetric algorithms are accepted (`RS*`, `PS*`, `ES*`, `EdDSA`; default `RS256` and `ES256`). Listing `HS*` or `none` disables forward auth.
* `jwks.uri` must use `https` (plain `http` is allowed only for `localhost`). `issuer` and `audience` are required. If the configuration is incomplete or invalid, forward auth stays disabled and MCPHub logs an error.

**Request handling**

* Forward auth is checked before bearer keys, OAuth tokens, Better Auth sessions, and dashboard JWTs.
* A token that is not a JWT, or whose `iss` is not the configured issuer, is ignored and the other methods run as usual.
* A token from the configured issuer that fails verification (bad signature, expired, wrong audience, disallowed algorithm) or cannot be mapped to a user is rejected with `401`. MCPHub **never falls back** to another authentication method for it.

**Identity mapping**

* The identity is the pair (`iss`, `sub`). It is stored in the user's `ssoUserId` under a `forward-auth:` namespace, separate from Better Auth identities. A Better Auth login never adopts a forward-auth user, and forward auth never adopts a Better Auth user.
* On first sight, MCPHub creates a **non-admin** local user. Its username comes from `usernameClaim`, falling back to `emailClaim`, then `sub`.
* Existing accounts are **never linked** by username or email. If the username or email already belongs to another account, including `admin`, the request is rejected with `401`.
* Once bound, the local username does not change when the user is renamed at the identity provider, because servers, groups, and keys reference the username.
* Admin status always comes from the local user record. Role claims in the JWT are ignored.
* With `autoCreate: false`, only identities already bound to a local user are accepted. Because existing accounts are never linked, this effectively rejects every new identity.

Forward-auth settings are read on each request, so changes do not require a restart. Each field can be overridden by an environment variable; see [Environment variables](/configuration/environment-variables#forward-auth).

## Read-only mode

Set `READONLY=true` to prevent any mutating request from succeeding. The middleware permits:

* All `GET` requests.
* All paths starting with `${basePath}/tools/` (so OpenAPI tool execution still works).

Other methods receive `403`.

## Skip-auth mode (local development only)

<Warning>
  **`skipAuth` disables dashboard authentication entirely.** When `systemConfig.routing.skipAuth = true`, every unauthenticated request to `${basePath}/api/*` is automatically treated as a synthetic admin user (`{ username: 'guest', isAdmin: true }`). Anyone who can reach the API — including a misconfigured reverse proxy, a forgotten port-forward, or a public cloud IP — gets full administrative access: they can read every server's secrets, create / delete users, mint bearer keys, and exfiltrate the entire `mcp_settings.json`. **Never enable this in staging, production, demo, or any deployment that is reachable beyond `localhost`.** Bind MCPHub to `127.0.0.1` while developing, and double-check that `skipAuth` is `false` before deploying. MCP transport routes (`/mcp/*`, `/sse/*`) are not affected by `skipAuth` and continue to require their own credentials.
</Warning>

The flag exists purely to make local iteration painless: you can hit the dashboard API from `curl` or a frontend dev server without pasting a JWT. If you need API access from automated tooling instead, prefer a scoped bearer key (`/api/auth/keys`) over `skipAuth`.

## Password policy

Local user passwords are validated by `src/utils/passwordValidation.ts` (length and character-class rules). Failed validations on `POST /api/users` or `PUT /api/users/:username` return `400` with the failed checks under `errors`.

## See also

* [Users API](/api-reference/users) — local user CRUD.
* [OAuth API](/api-reference/auth) — OAuth client management endpoints.
* [MCP Settings](/configuration/mcp-settings) — where `routing`, `oauth`, and `auth.betterAuth` configuration lives.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.