Re: [PoC] Federated Authn/z with OAUTHBEARER

From: Jacob Champion <jacob(dot)champion(at)enterprisedb(dot)com>
To: Peter Eisentraut <peter(at)eisentraut(dot)org>
Cc: Daniel Gustafsson <daniel(at)yesql(dot)se>, Antonin Houska <ah(at)cybertec(dot)at>, PostgreSQL Hackers <pgsql-hackers(at)postgresql(dot)org>
Subject: Re: [PoC] Federated Authn/z with OAUTHBEARER
Date: 2024-11-14 17:45:32
Message-ID: CAOYmi+kLTJ1wZ6gxRbgtR52E=EyiCpmp6J3mmSvtc1a6i7sZ3Q@mail.gmail.com
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Tue, Nov 12, 2024 at 1:47 PM Jacob Champion
<jacob(dot)champion(at)enterprisedb(dot)com> wrote:
> On Fri, Nov 8, 2024 at 1:21 AM Peter Eisentraut <peter(at)eisentraut(dot)org> wrote:
> > Also, shouldn't [oauth_validator_library] be an hba option instead? What if you want to
> > use different validators for different connections?
>
> Yes. This is again the multiple-issuers problem; I will split that off
> into its own email since this one's getting long. It has security
> implications.

Okay, so, how to use multiple issuers/providers. Here's my current
plan, with justification below:

1. libpq connection strings must specify exactly one issuer
2. the discovery document coming from the server must belong to that
libpq issuer
3. the HBA should allow a choice of discovery document and validator

= Current Bug =

First, I should point out a critical mistake I've made on the client
side: I treat oauth_issuer and oauth_client_id as if they can be
arbitrarily mixed and matched. Some of the providers I've been testing
do allow you to use one registered client across multiple issuers, but
that's the exception rather than the norm. Even if you have multiple
issuers available, you still expect your registered client to be
talking to only the provider you registered it with.

And you don't want the Postgres server to switch providers for you.
Imagine that you've registered a client application for use with a big
provider, and that provider has given you a client secret. You expect
to share that secret only with them, but with the current setup, if a
DBA wants to steal that secret from you, all they have to do is stand
up a provider of their own, and libpq will send the secret straight to
it instead. Great.

There's actually a worse scenario that's pointed out in the spec for
the Device Authorization flow [1]:

Note that if an authorization server used with this flow is
malicious, then it could perform a man-in-the-middle attack on the
backchannel flow to another authorization server. [...] For this to
be possible, the device manufacturer must either be the attacker and
shipping a device intended to perform the man-in-the-middle attack,
or be using an authorization server that is controlled by an
attacker, possibly because the attacker compromised the
authorization server used by the device.

Back when I implemented this, that paragraph seemed pointlessly
obvious: of course you must trust your authorization server. What I
missed was, the Postgres server MUST NOT be able to control the entry
point into the device flow, because that means a malicious DBA can
trivially start a device prompt with a different provider, forward you
all the details through the endpoint they control, and hope you're too
fatigued to notice the difference before clicking through. (This is
easier if that provider is one of the big ones that you're already
used to trusting.) Then they have a token with which to attack you on
a completely different platform.

So in my opinion, my patchset must be changed to require a trusted
issuer in the libpq connection string. The server can tell you which
discovery document to get from that issuer, and it can tell you which
scopes are required (as long as the user hasn't hardcoded those too),
but it shouldn't be able to force the client to talk to an arbitrary
provider or swap out issuers.

= Multiple Issuers =

Okay, with that out of the way, let's talk about multiple issuer support.

First, server-side. If a server wants different groups of
users/databases/etc. to go through different issuers, then it stands
to reason that a validator should be selectable in the HBA settings,
since a validator for Provider A may not have any clue how to validate
Provider B. I don't like the idea of pg_hba being used to load
arbitrary libraries, though; I think the superuser should have to
designate a pool of "blessed" validator libraries to load through a
GUC. As a UX improvement for the common case, maybe we don't require
the HBA to have an explicit validator parameter if the conf contains
exactly one blessed library.

In case someone does want to develop a multi-issuer validator (say, to
deal with the providers that have multiple issuers underneath their
umbrella), we need to make sure that the configured issuer in use is
available to the validator, so that they aren't susceptible to a
mix-up attack of their own.

As for the client side, I think v1 should allow only one expected
issuer per connection. There are OAuth features [2] that help clients
handle more safely, but as far as I can tell they are not widely
deployed yet, and I don't know if any of them apply to the device
flow. (With the device flow, if the client allows multiple providers,
those providers can attack each other as described above.)

If a more complicated client application associates a single end user
with multiple Postgres connections, and each connection needs its own
issuer, then that application needs to be encouraged to use a flow
which has been hardened for that use case. (Setting aside the security
problems with mix-ups, the device flow won't be particularly pleasant
for that anyway. "Here's a bunch of URLs and codes, go to all of them
before they time out, good luck!")

= Discovery Documents =

There are two flavors of discovery document, OAuth and OpenID. And
OIDC Discovery and RFC 8414 disagree on the rules, so for the issuer
"https://example.com/abcd", you have two discovery document locations
using postfix or infix styles for the path:

- OpenID: https://example.com/abcd/.well-known/openid-configuration
- OAuth: https://example.com/.well-known/oauth-authorization-server/abcd

Some providers publish different information at each [3], so the
difference may be important for some deployments. RFC 8414 claims the
OpenID flavor should transition to the infix style at some point (a
transition that is not happening as far as I can see), so now there
are three standards. And Okta uses the construction
"https://example.com/abcd/.well-known/oauth-authorization-server",
which you may notice matches _neither_ of the two options above, so
now there are four standards.

To deal with all of this, I plan to better separate the difference
between the issuer and the discovery URL in the code, as well as allow
DBAs and clients to specify the discovery URL explicitly to override
the default OpenID flavor. For now I plan to support only
"openid-configuration" and "oauth-authorization-server" in both
postfix and infix notation (four options total, as seen in the wild).

How's all that sound?

--Jacob

[1] https://datatracker.ietf.org/doc/html/rfc8628#section-5.3
[2] https://datatracker.ietf.org/doc/html/rfc9207
[3] https://devforum.okta.com/t/is-userinfo-endpoint-available-in-oauth-authorization-server/24284

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Matthias van de Meent 2024-11-14 18:14:18 Re: SQL:2011 application time
Previous Message Paul Jungwirth 2024-11-14 17:31:40 Re: SQL:2011 application time