Conversation
| In this blog post we follow the same example, but instead of the success story, we explore how OAuth keeps our PostgreSQL servers secure. | ||
|
|
||
| We won't focus on complex attack vectors, like the examples in the [second blog post](/blog/2025/11/17/oidc-in-postgresql-how-it-works-and-staying-secure/) in the OIDC series. | ||
| Instead of social engineering, we'll look at practical errors, misconfigurations, honest mistakes: understanding the error messages and fixing problems. |
There was a problem hiding this comment.
Instead of social engineering, we'll look at practical errors, misconfigurations, and honest mistakes - understanding error messages and how to fix them.
|
|
||
| ### We succeeded with a FATAL error? | ||
|
|
||
| However, before we start using all these additional items, let's go back to the end of the keycloak story, where in the end we succeeded with logging in. |
There was a problem hiding this comment.
Just wordsmithing idea and Keycloak uppercase instead:
let's go back to the end of the Keycloak story, where we succeeded in logging in.
| FATAL: OAuth bearer authentication failed for user "testuser" | ||
| ``` | ||
|
|
||
| But if somebody is observant enough, this message is logged before we even went to the device authentication website of Keycloak, and entered the authentication code. |
There was a problem hiding this comment.
Mixing times/tenses here:
this message is logged before we even go to the device authentication website of Keycloak and enter the authentication code.
| The main reason for this is that developers can trust the backend, controlled by administrators, while they can't trust the frontend, potentially used by malicious users. | ||
|
|
||
| But are we validating user input in this case? | ||
| Why does the user even have to specify the issuer, since the server already knows it, it's in there in the HBA configuration? |
There was a problem hiding this comment.
Would drop the not needed "in there in"
...already knows it, it's in the HBA configuration?
| Since in the above situation the issuer is different, we never get to the point of signature validation. | ||
|
|
||
| To do that, somebody has to tamper with the token. | ||
| For example an attacker realizes that we require a specific scope, and since JWTs contain everything in clear text, decides to edit the `scp` claim and insert `pgscope` into it. |
There was a problem hiding this comment.
Missing comma:
For example, an attacker realizes that we require a specific scope
| Another interesting scenario you might wonder about is token lifetime: | ||
| in OAuth, tokens have a limited period in which they are valid. | ||
|
|
||
| PostgreSQL currently has no facilities to enforce token lifetime when a connection is active -- once somebody is logged in, they stay logged in until they disconnect for some reason --, but validators are expected to validate that tokens are still valid at least during authentication. |
There was a problem hiding this comment.
This -- and especially --, seem awkward and AIish
"PostgreSQL currently has no facilities to enforce token lifetime when a connection is active - once somebody is logged in, they stay logged in until they disconnect for some reason - but validators are expected..."
| However, it doesn't do that for other flows -- the Token Endpoint rejects unknown scopes with an error and doesn't provide an access token. | ||
|
|
||
| On the client side, the error is the same as before -- no details about what's missing. | ||
| Which is clearly fine in this situation, as this is clearly a configuration error, something the administrators have to figure out and fix. |
There was a problem hiding this comment.
clearly once in a single sentence should be enough :D
"Which is fine in this situation, as this is clearly a configuration error..."
| kcmap testuser2@example.com testuser2 | ||
| ``` | ||
|
|
||
| In this new setup, we transformed the configuration problem to a permission issue: |
There was a problem hiding this comment.
The preposition is incorrect, this should be "into a permission issue"
| testuser2 will be able to log in, testuser won't. | ||
|
|
||
| The error message on the client side is unchanged, it still doesn't say "permission denied" or "scope mismatch", or anything like that. | ||
| At this point this can be debatable, but it is still mainly a task for administrators, and not the user: |
There was a problem hiding this comment.
Something is either debatable or not, the "At this point seems redudant to me"
"This is debatable, but it is still..."
| * Instead of one scope, we have two: `pgscope`, `pgscope2` | ||
| * Instead of one realm, we have two - containing exactly the same setup: `pgrealm` and `wrongrealm` | ||
|
|
||
| We also have a role now, simply called `pgrole`. |
There was a problem hiding this comment.
A role named pgrole is also defined.
The pgtest2 client and pgscope2 scope both require the pgrole role. Only testuser2 is assigned this role; testuser does not have it.
| | testuser | OK | denied | OK | denied | | ||
| | testuser2 | OK | OK | OK | OK | | ||
|
|
||
| ### We succeeded with a FATAL error? |
|
|
||
| However, before we start using all these additional items, let's go back to the end of the Keycloak story, where we succeeded in logging in. | ||
| Or did we? | ||
| While `psql` logged us in, if anybody checked the server error log, we could see the following there: |
There was a problem hiding this comment.
If you check the server log, you may notice the following message:
| As for PostgreSQL 18, unfortunately, we have to live with this. | ||
| This also means that we can't rely on simply looking for OAuth authentication errors in the server log, because all OAuth authentication failures will result in exactly the same message, no matter if they are logged because of this harmless situation or because of a real authentication issue. | ||
|
|
||
| A workaround is to rely on the validators instead: since the server is unaware of the exact error situation anyway -- it delegates validation to the validator -- these plugins will print out much more detailed *log messages*. |
There was a problem hiding this comment.
To get meaningful diagnostic information, rely on validator plugins instead. Since the server...
| 1. The client sends an empty connection request to the server. | ||
| 2. The server confirms that we are using OAuth, and sends back its issuer URL. | ||
| 3. The client checks whether the issuer it is planning to use -- or has already used, if it already has a valid token -- matches the one sent by the server. | ||
| * If not, it aborts the login attempt and prints an error. |
There was a problem hiding this comment.
- If it does not match...
- If it does match...
| In our example, wrongrealm and pgrealm are exactly the same -- they have users with the same name, scopes with the same names, clients with the same names. | ||
| If there's a misconfiguration, and the server and the client use different realms in a similar setup, everything would seem to work -- the user trying to log in would be able to log in, get a token, psql would send it to the server... | ||
| and then on the server the validator would reject it -- assuming that it is a good validator, like pg_oidc_validator. | ||
| No harm done -- other than disclosing a token to the server that shouldn't have been sent there --, but figuring out what's the problem could take a while. |
There was a problem hiding this comment.
I think we can rethink this as it's an important part of the blog, something like:
...and then on the server the validator would reject it -- assuming that it is a good validator, like pg_oidc_validator.
Disclosing a token to the wrong server might go unnoticed and take time to diagnose.
|
|
||
| The client-side issuer check prevents this: psql compares the issuer URL from the server against the `oauth_issuer` it was configured with, and aborts if they don't match. | ||
|
|
||
| So while this redundancy, the client having to specify the issuer URL might seem just an annoying extra step, it is there to protect us. |
There was a problem hiding this comment.
While requiring the client to specify the issuer may seem redundant, it's an important safeguard that prevents tokens from being disclosed to malicious servers.
|
|
||
| Similarly to the previous situation, testing this without a custom client isn't possible, as psql always asks for a new token during the connection attempt, there is no way to send an earlier token with it. | ||
|
|
||
| So as with the previous blog post, please accept that pg_oidc_validator will reject this scenario with the following message in the server log: |
There was a problem hiding this comment.
As in the previous example, this scenario is rejected by pg_oidc_validator, which logs the following message on the server:
Feels a little more direct
| DETAIL: Connection matched file "<datadir>/pg_hba.conf" line 119: "host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope email",map=kcmap" | ||
| ``` | ||
|
|
||
| Which should be self explanatory. |
There was a problem hiding this comment.
Probably shouldn't assume, better if this is left out of the blog.
| Similarly to the previous scenarios, this is completely validator specific, we can only showcase our validator. | ||
|
|
||
| This scenario also depends on the OAuth flow used and the identity provider. | ||
| **Note:** Keycloak, for example, permits unknown scopes for the device flow -- it simply ignores them and returns the scopes it can. |
There was a problem hiding this comment.
Keycloak permits unknown scopes in the device flow by ignoring them and returning only the recognized scopes.
| Which is fine in this situation, as this is clearly a configuration error, something the administrators have to figure out and fix. | ||
|
|
||
| Now let's see the error slightly differently: | ||
| the above example worked with the unmodified keycloak setup, described in the previous blog, but we have an improved test setup for this one. |
There was a problem hiding this comment.
an added space between the two paragraphs and starting with "The above example.." flows a bit better.
| kcmap testuser2@example.com testuser2 | ||
| ``` | ||
|
|
||
| In this new setup, we transformed the configuration problem into a permission issue: |
There was a problem hiding this comment.
... into a permission issue where testuser 2 can log in and testuser cannot.
Slightly easier to read
|
|
||
| While this is not an all-inclusive list, as we can't possibly cover every error scenario in a setup involving several components, it covers the most common scenarios, and should address all possible security problems. | ||
|
|
||
| In our next part, we'll focus on a practical, minimal development example: |
Andriciuc
left a comment
There was a problem hiding this comment.
Added a few comments, everything else LGTM!
No description provided.