Password and magic-link sign-in work out of the box - no configuration beyond a working email provider. 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.
A provider can reach your instance in two ways:
| Server-wide provider | Org SSO connection | |
|---|---|---|
| Set up by | You, the operator, in PUG_CONFIG_FILE |
An org admin, under Settings > SSO & domains |
| Turned on by | Adding it to the file and restarting | Setting PUG_SSO_SECRET_KEY once |
| On the sign-in page | A button for everyone | Shown after someone enters an email on one of its domains |
| Who it signs in | Anyone the provider authenticates | Only emails on the verified domains it lists |
| Redirect URI | /oauth/callback |
/oauth/callback/<connection id> |
Org admins also verify their domains and choose auto-join, org creation and Require SSO in that tab. SSO & domains is their guide. This page covers what you configure as the operator.
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:
services:
server:
environment:
PUG_CONFIG_FILE: /etc/pug/config.json
volumes:
- ./config.json:/etc/pug/config.json:ro{
"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"],
"emailDomains": ["example.com"]
}
]
}
}| Field | Required? | Description |
|---|---|---|
id |
Yes | Unique lowercase identifier (^[a-z][a-z0-9_-]*$, max 63 chars). Permanent - see below. |
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. |
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. |
emailDomains |
No | Email domains this provider speaks for. Only for a provider your company runs, and never on Google. See Domains a provider speaks for. |
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.
Set up a provider
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:
https://YOUR_PUG_DASHBOARD/oauth/callbackCheck the ID token claims
The provider must expose standard OIDC discovery metadata (/.well-known/openid-configuration) and return these claims:
subemailemail_verified: true
A token without email_verified is still accepted in two cases: it carries Entra ID’s xms_edov: true, or it carries neither claim and its email is on one of the provider’s emailDomains. Entra ID never sends email_verified, so an Entra ID provider needs one of the two. An explicit email_verified: false is always refused.
name and picture are optional. Pug verifies the ID token’s signature, issuer, audience, expiry, nonce, and verified-email claim on the server.
The dashboard reads the discovery document from the browser, so the provider must allow cross-origin requests to it. Okta, Entra ID, Auth0 and Google do.
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.
An org SSO connection uses its own path, /oauth/callback/<connection id>, under the same rules. Pug refuses a connection’s sign-in on any other path, and a server-wide provider’s sign-in on a connection’s path. That way, one provider can’t hand its code to another provider’s callback.
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.
Configure Google like any other OIDC provider: issuer https://accounts.google.com, plus its client ID and secret from the Google Cloud console. Create the client as a “Web application”, and register the same /oauth/callback redirect URI on it.
Name the entry "id": "google". That’s the value existing Google accounts were already stored under, so they resolve directly with no migration.
That one client also covers Google Workspace SSO for every org on your instance. A Workspace account’s ID token carries an hd claim naming its domain, and Pug reads it as proof of that domain. Orgs need no Google setup of their own: an admin verifies the domain and turns on the settings. Pug reads hd only from a provider whose configured issuer is https://accounts.google.com. A personal Google account has no hd and proves nothing.
Google brings three constraints:
- The redirect URI must be on a public domain name, such as
pug.example.com. The name may resolve only inside your network, because the browser does the redirect. - The server needs outbound HTTPS to Google, to exchange the code and fetch Google’s signing keys. An air-gapped server can’t use Google. Use your own identity provider instead.
- Set the OAuth consent screen to Internal if only your own Workspace’s accounts should sign in with Google.
Domains a provider speaks for
A provider your company runs can list the email domains it speaks for in emailDomains. A sign-in through it with an email on a listed domain proves that domain.
Orgs that verified the domain under Settings > SSO & domains act on that proof. With Auto-join on, people who prove the domain join the org when they sign in. With Require SSO on, accounts on the domain can sign in only through a provider that proves it, and passwords and email links stop working for them. See SSO & domains.
Listing a domain tells Pug to trust this provider for every address on it, even without email_verified. Never list a domain on a provider anyone can sign up to: a stranger could register someone else’s address there and sign in to that person’s Pug account.
Pug refuses emailDomains on Google’s issuer, because anyone can create a personal Google account with an address on your domain. Google Workspace accounts need no list: their hd claim proves their domain on its own.
If the provider breaks while Require SSO is on, for example when its client secret expires, nobody on the domain can sign in to turn it off, admins included. pug domains unenforce example.com turns it off in every org.
Org SSO connections
Org admins can connect their own OIDC provider under Settings > SSO & domains. It needs no change to PUG_CONFIG_FILE and no restart. SSO connections covers the admin’s side.
Connections are off until you set a key:
openssl rand -base64 32 # paste the output into PUG_SSO_SECRET_KEY- The key encrypts the connections’ client secrets in Postgres. It must be 32 bytes, base64-encoded, or the server refuses to start.
- With it empty, the tab tells admins that connections are off and names this variable. If connections already exist, the server logs an error at startup, since none of them can sign anyone in. A domain that requires SSO through a connection is then locked out until you set the key again or run
pug domains unenforce. - Keep the key stable. A new key can’t read the stored secrets, so connection sign-ins fail until each admin enters their client secret again.
- Only the server reads it (
pug server, orpug dev).
Each connection has its own redirect URI, https://YOUR_PUG_DASHBOARD/oauth/callback/<connection id>. The tab shows it once the connection is saved, and the admin registers it with their provider. Connections live in Postgres and add to the providers in PUG_CONFIG_FILE. They never replace them.
Issuers on a private network
The server fetches each connection’s discovery document, signing keys and token endpoint itself. By default it refuses private, loopback, link-local and cloud metadata addresses, and ignores proxy settings. So an org admin can’t point a connection at a service inside your network.
Set PUG_SSO_ALLOW_PRIVATE_ISSUERS=true when:
- your identity provider is on an internal network, or
- the server reaches the internet only through a proxy. Connections then use the environment’s proxy settings.
Leave it off if people outside your company can sign up to your instance. Any of them can create an org and add a connection. Providers in PUG_CONFIG_FILE are yours, so they skip this check.
Verified domains and pug domains
Org admins verify a domain with a DNS TXT record, _pug-verification.<domain>. The server looks the record up itself, so it must see your public DNS. On an air-gapped server, or one whose DNS differs from the public one, the check can’t succeed. Verify the domain for them instead:
pug domains verify <org-id> example.com # mark it verified without DNS
pug domains show example.com # every org that added it
pug domains release <org-id> example.com # drop one org's claim, e.g. in a dispute
pug domains unenforce example.com # turn Require SSO off in every orgThe commands talk only to Postgres. They never change an org’s settings, which stay its admins’ choice, except unenforce. It is the way back when SSO breaks and nobody on the domain can sign in, admins included.
An operator-verified domain shows as Verified by your administrator, and setting changes don’t re-check its record. Admins find their org’s ID under Settings > Organization. Org admins can’t skip DNS themselves: on an instance with open sign-up, anyone can create an org and become its admin. See the CLI reference for each command’s output.
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.
Pug refuses to create or link an account from an email with non-ASCII characters. An account already linked to the provider keeps signing in.
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. The account gets no organization or project when:
- the sign-in added the person to an org, through an invite or auto-join, or
- an org that verified the email’s domain turned off Let members create their own organizations.
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. SAML and SCIM aren’t supported; see Not supported yet.
Further reading
- SSO & domains: the org admin’s side - domains, auto-join, Require SSO and connections
- Configuration:
PUG_CONFIG_FILE,PUG_SSO_SECRET_KEYand every other backend variable - CLI reference:
pug domains - Dashboard: build and serve the UI these buttons appear on