---
title: "Authentication"
description: "Add Google or any OIDC identity provider to your Pug instance with PUG_CONFIG_FILE."
---

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

# Authentication

Password and magic-link sign-in work out of the box - no configuration beyond a working [email provider](/self-hosting/configuration#email). This page is about **external sign-in providers**: Google, Okta, Keycloak, Entra ID, Auth0, or anything else that speaks OpenID Connect.

There is no Google-specific integration. Every external provider, Google included, goes through the same generic OIDC path.

## The config file

Providers are the one setting that can't be a flat environment variable (an instance can have several, each with its own fields), so they live in a versioned JSON file that the server reads at startup. Point `PUG_CONFIG_FILE` at it:

```yaml
services:
  server:
environment:
  PUG_CONFIG_FILE: /etc/pug/config.json
volumes:
  - ./config.json:/etc/pug/config.json:ro
```

```json title="config.json"
{
  "version": 1,
  "auth": {
"providers": [
  {
    "id": "google",
    "type": "oidc",
    "displayName": "Google",
    "issuerUrl": "https://accounts.google.com",
    "clientId": "YOUR_GOOGLE_CLIENT_ID",
    "clientSecret": "YOUR_GOOGLE_CLIENT_SECRET",
    "scopes": ["openid", "profile", "email"]
  },
  {
    "id": "company_sso",
    "type": "oidc",
    "displayName": "Company SSO",
    "issuerUrl": "https://login.example.com/realms/main",
    "clientId": "pug",
    "scopes": ["openid", "profile", "email"]
  }
]
  }
}
```

| Field | Required? | Description |
|-------|-----------|-------------|
| `id` | **Yes** | Unique lowercase identifier (`^[a-z][a-z0-9_-]*$`, max 63 chars). **Permanent** - see [below](#the-id-is-permanent). |
| `type` | **Yes** | `oidc` is the only supported value. |
| `displayName` | **Yes** | 1-64 characters, rendered on the sign-in button. Change this, not `id`, when you want different wording. |
| `clientId` | **Yes** | OAuth client ID from the provider. |
| `clientSecret` | No | Only for confidential clients. Stays server-side - see [Secrets never reach the browser](#secrets-never-reach-the-browser). |
| `issuerUrl` | **Yes** | The OIDC issuer. HTTPS, no credentials, query, or fragment. `http://localhost` is allowed for development. |
| `scopes` | No | Defaults to `["openid", "profile", "email"]`. `openid` and `email` are always required. |

The file is validated strictly at startup, and **any problem stops the server**: an unknown field, an unsupported `version`, a duplicate `id`, a missing required value, or a non-HTTPS remote issuer. That's deliberate - a half-applied config that boots "successfully" with sign-in quietly broken is worse than a crash your orchestrator will retry and surface.

> **The dashboard needs no build-time provider config**
>
> The dashboard fetches the provider list from your backend at runtime and renders a button per provider. Unlike `VITE_API_BASE_URL`, adding or removing a provider needs **no dashboard rebuild** - only a server restart. Client secrets are never part of that response.

## Set up a provider

1. **Create an OIDC client at the identity provider**

   Enable the **Authorization Code flow with PKCE** (`S256`). Register exactly this redirect URI, using your dashboard's origin:

```text
https://YOUR_PUG_DASHBOARD/oauth/callback
```

2. **Check the ID token claims**

   The provider must expose standard OIDC discovery metadata (`/.well-known/openid-configuration`) and return these claims:

   - `sub`
   - `email`
   - `email_verified: true`

   `name` and `picture` are optional. Pug verifies the ID token's signature, issuer, audience, expiry, `nonce`, and verified-email claim on the server.

3. **Add it to config.json and restart**

   Add an entry to `auth.providers` and restart the server. It logs the configured provider IDs at startup - and warns when the list is empty, so a half-finished rollout doesn't look like a healthy boot.

Discovery runs on the **first sign-in**, not at startup. An issuer that's unreachable when the server boots doesn't stop it from starting, and starts working on its own once it recovers; sign-ins against it fail with `OAUTH_PROVIDER_UNAVAILABLE` in the meantime.

### Redirect URI rules

Pug requires the `redirect_uri` to use the path `/oauth/callback` with no query or fragment, over HTTPS except on `localhost` / `127.0.0.1` / `::1`. When the browser sends an `Origin` header it must be same-origin with that URI. The **host** is pinned by the redirect URI you register at the identity provider, not by Pug.

### Secrets never reach the browser

The dashboard reads only the non-secret fields - id, type, display name, client ID, issuer, scopes. `clientSecret` is never returned by the API, and the authorization-code exchange happens server-side, so a confidential client stays confidential even though the flow starts in a browser.

Set `clientSecret` only when your provider requires a confidential client. Public clients (a typical Keycloak SPA client, for instance) omit it and rely on PKCE alone.

> **The id is permanent**
>
> A provider's `id` is stored on every account linked through it. **Renaming it orphans those links.** Affected users still sign in (they fall back to matching on their verified email), but each one picks up a second, stale identity row. Pick the id once (`google`, `okta`, `company_sso`) and change `displayName` instead.

## Google

Configure Google like any other OIDC provider: issuer `https://accounts.google.com`, plus its client ID and secret from the [Google Cloud console](https://console.cloud.google.com/apis/credentials). Register the same `/oauth/callback` redirect URI on the Google OAuth client.

Name the entry `"id": "google"`. That's the value existing Google accounts were already stored under, so they resolve directly with no migration.

> **Breaking change: PUG_OAUTH_GOOGLE_CLIENT_ID was removed**
>
> Google sign-in used to be configured with `PUG_OAUTH_GOOGLE_CLIENT_ID` (backend) and `VITE_GOOGLE_CLIENT_ID` (dashboard). Both are gone. The old variable is now **ignored rather than rejected**, so an instance that upgrades without setting `PUG_CONFIG_FILE` starts cleanly - with no external providers and the Google button absent. The server logs a startup warning naming the ignored variable. Move the client ID into `config.json`, add the client secret Google now needs for the code exchange, and restart.

## Accounts and linking

A verified email from an identity provider links to an existing Pug account with the same address - including one that already has a password or uses magic links. Linking doesn't clear an existing password, so a user can keep signing in either way, and the sign-in marks that account's email verified.

An address that *isn't* already a Pug account gets one. The sign-in provisions a customer, an organization with that user as its **admin**, and a default project.

> **There is no allowlist or invite-only mode**
>
> Pug doesn't restrict sign-up by email domain or require an invitation (that's true of magic links too), so configuring a provider means **everyone your identity provider will authenticate can create an account, and their own organization, on your instance.** Scope it at the provider: assign only the intended users or groups to the Pug OIDC client.

## Not in this release

Pug does not request or retain an external refresh token, map provider groups to Pug roles, or initiate provider-wide logout. Signing out of Pug ends the Pug session only.

## Further reading

- [Configuration](/self-hosting/configuration): `PUG_CONFIG_FILE` and every other backend variable
- [Dashboard](/self-hosting/dashboard): build and serve the UI these buttons appear on

Source: https://docs.pug.sh/self-hosting/authentication/index.mdx
