PostgreSQL 18 (RC1 was released on 09/05/2025) introduces a native OAuth2 authentication method based on the SASL OAUTHBEARER mechanism. Instead of passwords, clients present bearer tokens issued by an identity provider (IdP). PostgreSQL validates those tokens through a pluggable validator module, then maps the authenticated identity to a database role before allowing access. This brings Postgres in line with modern SSO practices while keeping role/privilege management under your control.
This blog series contains 3 articles:
- Part-1: (this article) Explore how PostgreSQL 18 OAuth2 authentication works
- Part-2: Write a custom validator with Rust
- Part-3: Teach a PostgreSQL proto 3 client library to speak OAUTHBEARER
What you need (deployment prerequisites)
To enable OAuth2 authentication end-to-end, prepare the following:
- An IdP (OAuth2/OIDC) — practically required for token issuance, though the server ultimately trusts whatever your validator accepts.
- A PostgreSQL v3-protocol client — today, psql (via libpq) is confirmed and supports an integrated device authorization flow.
- PostgreSQL 18, built with
--with-openssl --with-libcurl
, configured with pg_hba.conf and (optionally) pg_ident.conf. - A server-side validator module to verify tokens and return an authorization decision.
How it works
The handshake consists of an SASL exchange where the server advertises OAUTHBEARER. The client either presents a token immediately or requests discovery info to run an OAuth flow (e.g., device code) and then retries with the token. The server validates the token via the configured validator and finalizes authentication.
Key points from this flow:
- Server advertises OAUTHBEARER when
pg_hba.conf
selects oauth. - Client may send a bearer token immediately, or send an empty
auth=""
to fetch discovery info and then obtain a token (libpq implements Device Authorization). - The token-acquisition method is protocol-independent; SASL only transports the result.
Server configuration
1) pg_hba.conf (new oauth method)
A typical rule looks like this:
# TYPE DATABASE USER ADDRESS METHOD OPTIONS...
host all tester ::1/128 oauth scope="openid profile" issuer=https://my-system.my-domain/auth validator=my_system_validator map="my_oauth_map"
method=oauth
enables the feature for matching connections.- issuer and scope express what the server expects the token to contain.
- validator names the validator to use (see below).
- map activates classic
pg_ident.conf
mapping (user ↔ role).
The pg_ident.conf
looks like
# MAPNAME SYSTEM-USERNAME PG-USERNAME
my_oauth_map abcdef_my_user_id tester
2) postgresql.conf (load validators)
# - Authentication -
oauth_validator_libraries = 'my_system_validator'
# comma-separated list of trusted validator modules
Validators listed here become available to HBA rules.
3) Identity mapping modes
You can keep mapping in pg_ident.conf
, like the above example
- pg_ident.conf mapping (default): the validator must return
authn_id
that exactly matches the requested user or the pg_ident-mapped role, otherwise login fails. - Delegated mapping: set
delegate_ident_mapping=1
on the HBA rule. PostgreSQL will ignoreauthn_id
and admit the requested user if the validator returnsauthorized=true
. Use this only if the validator strictly binds token identity to the requested role.
Client behavior
psql/libpq requires two connection parameters and fails fast if they’re missing:
- oauth_issuer
- oauth_client_id
libpq can run the Device Authorization flow in terminal environments by printing a verification_uri and user_code, then polling until the token is issued.
Other clients/drivers (e.g., jackc/pgx, launchbadge/sqlx) can adopt one of two patterns:
- Full SASL OAUTHBEARER: react to the server’s discovery/scope hint, run an OAuth flow, then continue the SASL exchange.
- Token-first: obtain a token out-of-band and send it in the initial SASL message.
GUI tools often prefer (1). SDKs/libraries in services often prefer (2).
Validator module
A validator is the trust anchor between Postgres and the IdP. Its job is to:
- Verify the token (issuer, signature/keys if JWT, or introspection if opaque; scopes; lifetime).
- Return { authorized: true/false, authn_id: "username" }.
- Enforce identity-to-role binding when delegated mapping is enabled.
Design note: offline JWT checks minimize latency; online introspection simplifies revocation but must complete within authentication_timeout. Read this doc for more official explanations.
Token format
SASL OAUTHBEARER does not require JWT. Any bearer token with characters in the allowed set is acceptable at the protocol level; your validator determines how to verify it.
Roles and privileges
OAuth2 support changes the authentication only. Role lifecycle (creation, grants, revocation) stays with your usual DBA processes or automation.
Putting it Together
- Provision roles in Postgres (the identities you intend to map to).
- Choose an IdP and decide JWT (offline) vs. introspection (online).
- Implement a validator and list it in oauth_validator_libraries.
- Add an OAuth HBA rule with issuer, scope, and either map or delegate_ident_mapping=1.
- Update clients: for psql, set oauth_issuer and oauth_client_id; confirm the device flow works where needed.
How does psql work with PostgreSQL 18?
Here is a typical psql command to connect to a PostgreSQL 18 server with the OAuth2 authentication method:
psql "postgres://tester@my_pg.my-domain:5432/upm?oauth_issuer=https://my-system.my-domain/auth&oauth_client_id=my-app-client-id"
What happens underneath can be explained by the following diagram, which highlights the two opportunities a client has to authenticate: send a token upfront or fetch discovery info, run device flow, and then continue.
libpq’s device flow prompts (verification_uri, user_code) are documented in the official libpq OAuth section.