---
title: 'Authentication & Security'
description: '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` column (`private` / `public`, with `group` reserved for a future user→group plumbing) that controls which non-admin users see the server in `tools/list` and the dashboard. Admins always see every server regardless of this setting; the field only affects the filter applied by `dataService.filterData` to non-admin requests.

- `'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'`: reserved. Stored if set but treated as `'private'` by the filter until user→group membership ships.

The visibility is editable per server from the dashboard (Server form → Visibility). 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
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
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.**

The first admin account is created at startup by `initializeDefaultUser()`. If no users exist, MCPHub creates `admin` and uses `ADMIN_PASSWORD` if set, otherwise it generates a random password and prints 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_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.

## 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.
