PostgreSQL 18 can authenticate clients with OAuth 2.0 bearer tokens, and it cannot tell a good token from a bad one. The server checks that the token is made of legal characters and stops there. Everything after that (was it signed by the right issuer, has it expired, who is it for, who does it say you are) belongs to a validator module, a shared library that someone other than the PostgreSQL project wrote. oauth_validator_libraries is the list of libraries allowed to do that job.
It is a comma-separated string, empty by default, new in 18. The context is sighup, so it goes in postgresql.conf and a reload is all it needs. (ALTER SYSTEM works too, with a trap: ALTER SYSTEM SET oauth_validator_libraries = '' writes '""', which is a list of one library with an empty name. Use ALTER SYSTEM RESET to empty it.) Ordinary roles cannot read it; SHOW needs superuser or pg_read_all_settings.
The other half of the configuration is a line in pg_hba.conf:
1 host all all 10.0.0.0/8 oauth issuer="https://idp.example.org" scope="openid postgres"
issuer and scope are required. validator= is optional when the list has exactly one entry and mandatory when it has more than one. The match is a plain string comparison: with '$libdir/my_validator' in the list, a line that says validator=my_validator is rejected. Use the bare library name in both places.
What you are listing
At login the backend hands the validator two things, the token and the role the client asked for, and gets two things back: a yes or no, and an identity string. The server compares the identity to the requested role (or runs it through a pg_ident.conf map), and that identity is what system_user reports (oauth:alice) and what log_connections writes down. With delegate_ident_mapping=1 on the HBA line the server skips the comparison as well, and the validator’s yes is the whole decision.
So every name in this parameter is code that can admit anyone, as any role its HBA lines cover. PostgreSQL ships none. There are a few in the wild: Percona’s pg_oidc_validator verifies JWT signatures itself, against keys it fetches from the issuer, and CloudNativePG has a Keycloak validator that labels itself experimental. Read the source of whichever one you pick, the way you would read a PAM module handed to you by a stranger. The documentation warns the people writing these that a broken validator can be worse than no authentication at all. That goes for the people installing them, too.
Emptying the list does not turn OAuth off
The documentation says that with the parameter empty, “OAuth connections will be refused.” Here is an 18.6 server on Linux with one oauth line in pg_hba.conf and a working validator, just after I set the parameter to '' in postgresql.conf and reloaded:
1 LOG: received SIGHUP, reloading configuration files
2 LOG: parameter "oauth_validator_libraries" changed to ""
3 LOG: oauth_validator_libraries must be set for authentication method oauth
4 CONTEXT: line 2 of configuration file "…/pg_hba.conf"
5 LOG: …/pg_hba.conf was not reloaded
SHOW oauth_validator_libraries returns an empty string at this point. The next OAuth login:
1 LOG: connection authenticated: identity="alice" method=oauth (…/pg_hba.conf:2)
2 LOG: connection authorized: user=alice database=postgres
The list is enforced by the pg_hba.conf parser and by nothing else. On a reload the server applies postgresql.conf first and then parses pg_hba.conf again. An oauth line that no longer agrees with the list is a parse error, and a pg_hba.conf with a parse error is discarded whole, which leaves the previous copy in force with the validator’s name already resolved into it. Nothing consults the parameter when a client connects. Removing one library of two while a line still named it went the same way: the library I had removed kept validating logins.
(Windows is the exception, and not a comfortable one. There every new backend parses pg_hba.conf for itself, so the same reload locks out all new connections, superusers included, until the file is fixed.)
The mistake surfaces at the next restart, where the same parse error is fatal and the server does not come up:
1 LOG: oauth_validator_libraries must be set for authentication method oauth
2 CONTEXT: line 2 of configuration file "…/pg_hba.conf"
3 FATAL: could not load …/pg_hba.conf
Adding a library has the same problem from the other direction. Going from one entry to two turns every oauth line without a validator= into an error, the file is rejected, and any unrelated edit you made to pg_hba.conf in the same change stays unapplied. Nothing announces any of this outside the server log.
To turn OAuth off, or to retire a validator, change pg_hba.conf first, reload, and check:
1 SELECT line_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL;
Then edit the parameter. (pg_hba_file_rules parses the file on disk, so it tells you whether the file would load, which is the question.) PostgreSQL 19 beta 4 behaves the same way in every case above, with the messages reworded.
Loaded late, and often
Nothing checks the names when you set them. A misspelled library gets through a reload and a restart, and the first client to try OAuth finds out:
1 FATAL: could not access file "no_such_validator": No such file or directory
That message goes to the client, before authentication, library name included.
One value is worse than a misspelling. On 18.6, setting the parameter to a single space, with an oauth line that has no validator=, segfaults the postmaster on reload and again on every start until the file is fixed. The fix is on the 18 stable branch and in 19 beta 4, and is not in a released 18 yet. With an explicit validator= on the line, the same value is an ordinary parse error.
Each backend loads the library itself at login; the postmaster does not. The validator’s startup and shutdown callbacks therefore run once per connection, and replacing the .so on disk changes authentication for the next connection with no reload and no restart. A package upgrade of your validator is a live change to who can log in. (If the library is also in shared_preload_libraries, the reverse holds: a file replaced by rename, the way a package manager does it, is ignored until a restart. I tested both.)
The validator also runs inside the window that authentication_timeout is supposed to bound, and the timeout only works if the validator cooperates. I wrote one that does a plain blocking recv() against an endpoint that accepts the connection and then says nothing, which is what a careless call to a token introspection endpoint looks like when the identity provider is having a bad day. With authentication_timeout = 5s, three backends sat in authentication for 39 seconds, until the far end hung up, and only then logged canceling authentication due to timeout. pg_terminate_backend() returned true and did nothing. In a second run, a fast shutdown waited 36 seconds for two of them. Every one of those backends holds a connection slot.
Real validators block on the network as well. Both of the ones named above make synchronous libcurl requests and bound them with libcurl’s own timeout: 30 seconds per request in one, 2 seconds by default in the other. That timeout, and not authentication_timeout, is what ends a login stuck behind an unresponsive identity provider. Before you list a validator that calls out to the network, find out what its number is.
One difference between versions will show up in your logs. With libpq’s built-in flow, the client first connects without a token to learn the issuer and scope from the server. PostgreSQL 18 logs that exchange as FATAL: OAuth bearer authentication failed for user "alice", so an interactive OAuth login contributes a failed-authentication line before the successful one; adjust whatever is counting those. PostgreSQL 19 logs nothing for it at the default level. 19 also lets a validator define its own validator.* options on the HBA line and attach a DETAIL to its failures. The parameter itself is unchanged.
Leave it empty until you are deploying OAuth. When you are, list one library by its bare name, put an explicit validator= on every oauth line anyway so that a second library can be added later without invalidating the file, and run that pg_hba_file_rules query after every reload. This parameter decides which libraries pg_hba.conf is allowed to name. It is not the off switch. That is pg_hba.conf, as it is for every other authentication method.