OAuth in Postgres 19 (and beyond)
It's somehow mid-October already! We're busy working towards the upcoming PG19 release, but it's been just over a year since my last OAuth post, and it's probably time to talk about that again. Sometimes it's even nice to take a break and think about what we'd like to work on after 19 ships...
Earlier this year, at PGConf.dev 2026, I got to discuss and plan the roadmap with interested developers as part of the OAuth Working Group session. This post is a partial recap of some of the discussions we had there.
OAuth in PG19
Compared to the large feature that landed in PG18, OAuth in 19 focuses more on incremental improvement. End users won't notice much change. But DBAs and developers may be happy to hear the following:
- Validator modules may now register configuration options to be used in
pg_hba.conf, so that DBAs can apply different settings to different groups of users in the location they're most familiar with. (Modules could always register GUCs for use inpostgresql.conf, but those settings were more-or-less global. So developers had to contemplate the creation of configuration mini-DSLs if they wanted users to have fine-grained control. No longer!) - The debug mode in libpq, which was previously an all-or-nothing switch, has been split into individual options. So libpq developers can pick and choose rather than worrying about leaking tokens or being MITM'd while they're debugging local behavior. Also, the
PGOAUTHCAFILEenvironment variable, which was hidden underneath debug mode in PG18, has been promoted into its own connection parameter to assist users of private CAs. - The quality of life for both client- and server-side implementers has improved, especially when reporting errors. Custom implementations feel less bolted on when they can reuse the same mechanisms as the core.
Some of the latter changes also helped us solidify the ABI for the libpq-oauth plugin that provides our built-in Device Authorization flow. That, in turn, should make it easier to support third-party flows in the future without needing to recompile client applications.
And speaking of the future...
For PG20 and Beyond
Client-Side Token Caching
We badly need a token cache. No one wants to click through an OAuth web page over and over again (and forcing people to do that increases click fatigue, which is its own security problem). Even when a fully noninteractive flow is in use, requesting a token for every connection is wasteful. But the tricky part here is that we have to figure out how to securely store tokens and decide when they can be safely reused.
Once we take responsibility for caching in the client, we have to defend against session hijacking attacks, just like web browsers have to defend stored cookies against use by unrelated web pages. (libpq is used in a lot of different ways; just because two connections originate from the same process ID or the same OS user account doesn't mean that the people making those connections hold the same credentials or are even mutually trusting.) So I think it's likely that some part of our client software will need to enable and disable credential caching based on certain parameters and properties of the connections themselves. It'll then be up to the application developer to decide which caching strategies are appropriate.
Storage of the secrets might seem like a more mundane concern, but you probably don't want us to roll our own solution here. The question is, which of the infinite possibilities we should support for a v1? There are plenty of kernel-level and userspace keychain services available, but you might be surprised by their security models if you've been spoiled by stricter application isolation.
So all this will take plenty of work and discussion.
Towards Sender Constraints
You can think of an OAuth bearer token as a temporarily-issued password, which also happens to contain tamper-resistant information about the end user. That token can still be stolen and reused until it expires, which is why it's important to use OAuth in combination with strong transport security. For Postgres, this means using a strong sslmode in the client, and configuring the server's HBA to allow only encrypted connections.[1]
This situation is still a step up from the plaintext passwords used by LDAP, since tokens are expected to have a relatively short expiration time and can optionally be revoked. But the ability to steal tokens is a bit of a step down from Kerberos (which can be configured to include replay protections for tickets), and it's a big step down from our password-based default, SCRAM.[2] It'd be nice to shore that aspect up.
Enter sender-constrained tokens. These are designed to be bound to a secret that only the sender knows, but that the resource server can still verify. (Unsurprisingly, this is done using public-key cryptography.) At the moment, there are really only two implementations that could be interesting for our Postgres clients:
Neither is ideal.
mTLS requires X.509 client certificates in some fashion, whether self-signed or centrally issued via PKI. DPoP uses bare keypairs instead of certificates, making it somewhat lighterweight... but DPoP isn't compatible with the OAUTHBEARER SASL mechanism, so we'd need to author a new one. It also contains a number of HTTP-specific pieces, which would have to be ported over to the protocol-agnostic SASL framework as part of the new mechanism.
By contrast, mTLS certificate bindings reuse the Bearer token scheme we have today, and Postgres already supports client certificates, so implementing mTLS would be much easier. In fact, OAuth validators already have the information they need to verify an mTLS-bound token; only the libpq side would really need additional plumbing. But this wouldn't help people who don't already make use of client certificates. (Even if we were to generate a self-signed cert on the fly, we'd have to find a place to securely put it... that sounds familiar.) mTLS is also much less popular among third-party OAuth providers, so it's not clear how many people would be able to use it if we supported it.
For me, one particularly strange aspect of both techniques is that nobody mandated that constrained tokens be incompatible with standard Bearer tokens. That means existing resource servers that don't know about sender constraints will almost certainly accept a "bound" token without its proof. This is even mentioned as a perk in the DPoP specification, because it allows for backwards compatibility during the transition... but in my not-so-humble opinion, it's not really a "binding" if it can be trivially stripped back off. (A mandatory compatibility break might not be completely enforceable in practice, given the existence of opaque tokens — but still, wouldn't you want to try?)
Frankly, I don't like the status quo, and I suspect some non-trivial work is going to need to be done to square sender-constrained tokens with SASL concepts such as channel binding. More research needed.
And More
Our working group discussion covered other important use cases, like server-side credential proxying and connection poolers. Those deserve their own larger conversation, but it seems like they're closely tied to the caching and constraint problems, so we might as well dig into the latter.
And — as always — it'd be lovely to have a token validator included out of the box. If RFC 9068 (at+jwt) support reaches critical mass with popular implementations, then that's probably a decent way to go. (But we may want to let third parties shake out the meaning of "audience" before we do that, so that we can scope our tokens to Postgres-specific resources and separate ourselves from the World Wide Web a bit more.)
Yeah, that seems like enough work for now. Wish us luck!
- ^ As with all other available auth methods, Postgres explicitly does not force the use of transport security with OAuth tokens. That makes me nervous, but there were enough people in the Postgres community using other forms of network encryption (mesh VPN etc.) to convince me that it's an important use case for now. Forcing the engineers who understand what they're doing to jump through that particular hoop for my personal comfort seems likely to have bad side effects; I'd rather spend my energy making it difficult to turn off encryption by mistake.
- ^ SCRAM never places the secret on the wire to begin with, and it additionally binds the authentication process to the TLS session, which makes it very difficult for someone to trick you into relaying usable credentials to them over a separate connection.