Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions docs/authentication/oidc.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: OIDC
---

Allow users to sign in with any OpenID Connect provider, such as Authentik, Auth0, Authelia, Zitadel, or VoidAuth.

Use this when your identity provider does not have a dedicated page. For Google, Okta, and Keycloak, use their dedicated provider types instead.

## Prerequisites

- An OpenID Connect provider that supports [discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) (publishes `/.well-known/openid-configuration`)
- Admin access to create a client (application)
- [external_url](/configuration/config#general)

## Setup

### 1. Create a Client in Your Provider

The exact steps vary by provider. Create a confidential OAuth 2.0 / OpenID Connect client with these settings:

- **Grant type**: Authorization Code
- **Client authentication**: enabled (client secret)
- **Redirect URI**:
```
<<external_url>>/api/auth/oidc/callback
```
- **Scopes**: `openid`, `email`, `profile`

pgconsole requires an `email` claim. Make sure the client is allowed to release the user's email.

### 2. Get Client Credentials

Copy the **Client ID** and **Client Secret** from the client you created.

### 3. Get Issuer URL

The issuer URL is the base URL your provider publishes its discovery document under. pgconsole fetches `<<issuer_url>>/.well-known/openid-configuration` to find the authorization, token, and userinfo endpoints.

| Provider | Issuer URL pattern |
|----------|--------------------|
| Authentik | `https://auth.example.com/application/o/pgconsole/` |
| Auth0 | `https://your-tenant.us.auth0.com` |
| Authelia | `https://auth.example.com` |
| Zitadel | `https://your-org.zitadel.cloud` |

To verify, open the issuer URL with `/.well-known/openid-configuration` appended in a browser. It should return a JSON document whose `issuer` field matches the URL you configured.

### 4. Configure pgconsole

Add the OIDC provider to your `pgconsole.toml`:

```toml pgconsole.toml
[general]
external_url = "https://pgconsole.example.com"

[auth]
# openssl rand -hex 32
jwt_secret = "your-secret-key-at-least-32-characters-long"
signin_expiry = "7d"

[[auth.providers]]
type = "oidc"
name = "Authentik" # optional, shown on the sign-in button
client_id = "pgconsole"
client_secret = "your-client-secret"
issuer_url = "https://auth.example.com/application/o/pgconsole/"

# Allow each user to sign in. Emails must match those returned by your provider.
[[users]]
email = "alice@example.com"

[[users]]
email = "bob@example.com"

[[users]]
email = "carol@example.com"

# Groups and IAM rules grant access; they do not create users.
[[groups]]
id = "dev-team"
name = "Development Team"
members = ["alice@example.com", "bob@example.com"]

[[iam]]
connection = "production"
permissions = ["read"]
members = ["user:carol@example.com", "group:dev-team"]
```

See [OAuth Providers](/configuration/config#oauth-providers) in the configuration reference for all provider fields.

pgconsole authenticates to the token endpoint with HTTP Basic (`client_secret_basic`) when your provider advertises it, and falls back to form-body credentials (`client_secret_post`) otherwise. If your client is registered for a specific method, set `token_auth_method` explicitly:

```toml pgconsole.toml
[[auth.providers]]
type = "oidc"
client_id = "pgconsole"
client_secret = "your-client-secret"
issuer_url = "https://auth.example.com"
token_auth_method = "client_secret_post"
```

Only one `oidc` provider can be configured. To offer several identity providers side by side, combine `oidc` with the dedicated `google`, `okta`, or `keycloak` types.

### 5. Restart pgconsole

Restart the application for changes to take effect. A "Continue with Authentik" button (or "Continue with SSO" if `name` is omitted) will appear on the login page.
2 changes: 1 addition & 1 deletion docs/authentication/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pgconsole supports multiple authentication methods to secure access to your data
| Method | Description | Best For |
|--------|-------------|----------|
| Email/Password | Built-in local authentication | Simple setups, small teams |
| SSO ([Google](/authentication/google), [Okta](/authentication/okta), [Keycloak](/authentication/keycloak)) | OAuth providers | Organizations with existing identity providers |
| SSO ([Google](/authentication/google), [Okta](/authentication/okta), [Keycloak](/authentication/keycloak), [OIDC](/authentication/oidc)) | OAuth providers | Organizations with existing identity providers |

## Configuration

Expand Down
25 changes: 22 additions & 3 deletions docs/configuration/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ client_id = "0oaXXXXXXXXXXXXXX"
client_secret = "your-okta-client-secret"
issuer_url = "https://your-org.okta.com/oauth2/default"

[[auth.providers]]
type = "oidc"
name = "Authentik"
client_id = "pgconsole"
client_secret = "your-client-secret"
issuer_url = "https://auth.example.com/application/o/pgconsole/"

[[users]]
email = "admin@example.com"
password = "your-password"
Expand Down Expand Up @@ -281,14 +288,18 @@ signin_expiry = "7d"

### OAuth Providers

OAuth providers are configured as an array of `[[auth.providers]]` entries. Each entry requires a `type` field. Repeat for multiple providers.
OAuth providers are configured as an array of `[[auth.providers]]` entries. Each entry requires a `type` field. Repeat for multiple providers, at most one per type.

Use `type = "oidc"` for any provider that supports OpenID Connect discovery (Authentik, Auth0, Authelia, Zitadel, and most self-hosted identity providers). pgconsole resolves the authorization, token, and userinfo endpoints from `<issuer_url>/.well-known/openid-configuration`.

| Field | Description | Required |
|-------|-------------|----------|
| `type` | Provider type: `google`, `keycloak`, or `okta` | Yes |
| `type` | Provider type: `google`, `keycloak`, `okta`, or `oidc` | Yes |
| `client_id` | OAuth client ID | Yes |
| `client_secret` | OAuth client secret | Yes |
| `issuer_url` | Issuer URL (required for `keycloak` and `okta`) | |
| `issuer_url` | Issuer URL (required for `keycloak`, `okta`, and `oidc`) | |
| `name` | Display name on the sign-in button (`oidc` only, defaults to "SSO") | |
| `token_auth_method` | How the client secret is sent to the token endpoint: `client_secret_basic` or `client_secret_post` (`oidc` only, inferred from discovery when omitted) | |

```toml pgconsole.toml
# Google
Expand All @@ -310,6 +321,14 @@ type = "okta"
client_id = "0oaXXXXXXXXXXXXXX"
client_secret = "your-okta-client-secret"
issuer_url = "https://your-org.okta.com/oauth2/default"

# Generic OIDC (Authentik, Auth0, Authelia, Zitadel, ...)
[[auth.providers]]
type = "oidc"
name = "Authentik"
client_id = "pgconsole"
client_secret = "your-client-secret"
issuer_url = "https://auth.example.com/application/o/pgconsole/"
```

## Users
Expand Down
3 changes: 2 additions & 1 deletion docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"pages": [
"authentication/google",
"authentication/okta",
"authentication/keycloak"
"authentication/keycloak",
"authentication/oidc"
]
}
]
Expand Down
2 changes: 1 addition & 1 deletion docs/features/audit-log.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ All audit events are JSON objects with a common structure:

| Field | Description |
|-------|-------------|
| `provider` | Auth provider (`basic`, `google`, `keycloak`) |
| `provider` | Auth provider (`basic`, `google`, `okta`, `keycloak`, `oidc`) |
| `ip` | Client IP address |
| `success` | Whether login succeeded |
| `error` | Error message (if failed) |
Expand Down
6 changes: 4 additions & 2 deletions server/auth-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { auditLogin, auditLogout } from './lib/audit'
import { registerGoogleOAuth } from './lib/oauth/google'
import { registerKeycloakOAuth } from './lib/oauth/keycloak'
import { registerOktaOAuth } from './lib/oauth/okta'
import { registerOidcOAuth } from './lib/oauth/oidc'

const router = Router()

Expand Down Expand Up @@ -118,6 +119,7 @@ const oauthOpts = {
registerGoogleOAuth(router, oauthOpts)
registerKeycloakOAuth(router, oauthOpts)
registerOktaOAuth(router, oauthOpts)
registerOidcOAuth(router, oauthOpts)

// GET /api/auth/providers
router.get('/providers', (_req: Request, res: Response) => {
Expand All @@ -126,10 +128,10 @@ router.get('/providers', (_req: Request, res: Response) => {
}

const config = getAuthConfig()
const providers: Array<{ name: string }> = []
const providers: Array<{ name: string; label?: string }> = []
if (getUsers().some(u => u.password)) providers.push({ name: 'basic' })
for (const provider of config?.providers ?? []) {
providers.push({ name: provider.type })
providers.push(provider.name ? { name: provider.type, label: provider.name } : { name: provider.type })
}

return res.json({ providers })
Expand Down
4 changes: 2 additions & 2 deletions server/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { getAuthConfig, getUserByEmail } from './config'
export interface TokenPayload extends JWTPayload {
sub: string // email
name?: string // display name (only if different from sub)
idp?: 'google' | 'keycloak' | 'okta' // identity provider (omitted for basic auth)
idp?: 'google' | 'keycloak' | 'okta' | 'oidc' // identity provider (omitted for basic auth)
avatar?: string // avatar URL (Google profile picture)
}

export interface User {
email: string
name: string
idp?: 'google' | 'keycloak' | 'okta' // omitted for basic auth
idp?: 'google' | 'keycloak' | 'okta' | 'oidc' // omitted for basic auth
avatar?: string // avatar URL (Google profile picture)
}

Expand Down
18 changes: 13 additions & 5 deletions server/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ export interface UserConfig {
}

export interface AuthProviderConfig {
type: 'google' | 'keycloak' | 'okta'
type: 'google' | 'keycloak' | 'okta' | 'oidc'
client_id: string
client_secret: string
issuer_url?: string // required for keycloak and okta
issuer_url?: string // required for keycloak, okta and oidc
name?: string // display name on the sign-in button (oidc only)
token_auth_method?: 'client_secret_basic' | 'client_secret_post' // oidc only; inferred from discovery when omitted
}

export interface AuthConfig {
Expand Down Expand Up @@ -511,7 +513,7 @@ export async function loadConfigFromString(content: string): Promise<void> {
// Parse [[auth.providers]] array
const providers: AuthProviderConfig[] = []
const rawProviders = a.providers as unknown[] | undefined
const validProviderTypes = ['google', 'keycloak', 'okta']
const validProviderTypes = ['google', 'keycloak', 'okta', 'oidc']
if (rawProviders && Array.isArray(rawProviders)) {
for (const entry of rawProviders) {
const raw = entry as Record<string, unknown>
Expand All @@ -529,16 +531,22 @@ export async function loadConfigFromString(content: string): Promise<void> {
throw new Error(`${raw.type} provider missing required field: client_secret`)
}

// issuer_url required for keycloak and okta
if ((raw.type === 'keycloak' || raw.type === 'okta') && (!raw.issuer_url || typeof raw.issuer_url !== 'string')) {
// issuer_url required for keycloak, okta and oidc
if ((raw.type === 'keycloak' || raw.type === 'okta' || raw.type === 'oidc') && (!raw.issuer_url || typeof raw.issuer_url !== 'string')) {
throw new Error(`${raw.type} provider missing required field: issuer_url`)
}

if (raw.token_auth_method !== undefined && raw.token_auth_method !== 'client_secret_basic' && raw.token_auth_method !== 'client_secret_post') {
throw new Error(`${raw.type} provider has invalid token_auth_method: ${raw.token_auth_method}. Must be one of: client_secret_basic, client_secret_post`)
}

providers.push({
type: raw.type as AuthProviderConfig['type'],
client_id: raw.client_id,
client_secret: raw.client_secret,
issuer_url: typeof raw.issuer_url === 'string' ? raw.issuer_url.replace(/\/+$/, '') : undefined,
name: typeof raw.name === 'string' ? raw.name : undefined,
token_auth_method: raw.token_auth_method as AuthProviderConfig['token_auth_method'],
})
}
}
Expand Down
Loading
Loading