Skip to content

feat: support OAUTHBEARER SASL authentication - #1378

Open
geoHeil wants to merge 2 commits into
rust-postgres:masterfrom
geoHeil:feature/oauthbearer-authentication
Open

geoHeil wants to merge 2 commits into
rust-postgres:masterfrom
geoHeil:feature/oauthbearer-authentication

Conversation

@geoHeil

@geoHeil geoHeil commented Aug 29, 2026

Copy link
Copy Markdown

Closes #1377.

PostgreSQL 18 added the oauth HBA method, which authenticates a connection with an OAuth 2.0 bearer token over SASL OAUTHBEARER (RFC 7628) instead of a password. authenticate_sasl matched only the SCRAM mechanisms, so such a server was refused with unsupported SASL mechanism.

Scope

Token-first only. The caller supplies a token, the crate performs the SASL exchange. No discovery, no device authorization flow, no conversation with an identity provider — the same line node-postgres draws, and the same thing libpq does when an application installs a custom flow. No token is requested unless the server asks for OAUTHBEARER.

let mut config = Config::new();
config.token_provider(|| async { Ok(fetch_token().await?) });

Verified in Pg18 docker postgres.

FYI: Clippy was failing on some files in CI - I have fixed this (inlined here) to have a green CI

PostgreSQL 18 added the `oauth` HBA method, which authenticates a
connection with an OAuth 2.0 bearer token over the SASL OAUTHBEARER
mechanism (RFC 7628) instead of a password. `authenticate_sasl`
recognised only the SCRAM mechanisms, so such a server was refused with
"unsupported SASL mechanism".

The caller supplies the token through `Config::token_provider`, a closure
called once per connection attempt so that an expiring token can be
refreshed without rebuilding the config. Obtaining, caching and renewing
tokens stays out of scope, as it is in libpq when a custom flow is
installed: there is no discovery and no device authorization flow. No
token is requested unless the server asks for OAUTHBEARER.

A successful exchange is a single round trip. The bearer token travels in
the SASL initial client response and the server replies with
`AuthenticationOk`; OAUTHBEARER carries no additional data on success, so
there is no `AuthenticationSASLFinal` message. On failure the server sends
a challenge holding a JSON error document, which RFC 7628 Sec. 3.2.3
requires the client to answer with a lone kvsep byte - sending it is what
gets the server to report the failure as its own `ErrorResponse` instead
of the client giving up with an unexpected-message error.

OAUTHBEARER defines no channel binding and the server rejects the `p`
specifier, so the gs2 header is always `n,,` and `channel_binding=require`
is refused.

Picks up the approach from rust-postgres#1325 by @JoHaHu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@geoHeil
geoHeil marked this pull request as draft August 29, 2026 09:20
`cargo clippy --all --all-targets` fails on master with `RUSTFLAGS=-Dwarnings`:

    error: this lifetime isn't used in the type
      --> tokio-postgres/tests/test/types/mod.rs:51:24
       |
    51 |     T: PartialEq + for<'a> FromSqlOwned + ToSql + Sync,
       |                        ^^

`FromSqlOwned` is declared as `pub trait FromSqlOwned: for<'a> FromSql<'a> {}`,
so it already carries the higher-ranked bound internally and takes no lifetime
parameter of its own. `for<'a> FromSqlOwned` therefore binds a lifetime nothing
uses, and removing the binder is behaviour-preserving by construction rather than
by inspection.

Not a new defect. `extra_unused_lifetimes` learned to see through an HRTB in a
clippy released after master's last CI run, and the workflow pins
`dtolnay/rust-toolchain@stable` rather than a version, so the next push to touch
CI meets the newer lint. Any PR opened now is red before it changes anything.

Reproduced with CI's own invocation and toolchain, clippy 0.1.98:

    RUSTFLAGS=-Dwarnings cargo clippy --all --all-targets
      without this change: exit=101
      with this change:    exit=0

`cargo fmt --all -- --check` clean, and `cargo test -p tokio-postgres --test
test --no-run` still builds the target.
@geoHeil

geoHeil commented Aug 29, 2026

Copy link
Copy Markdown
Author

CI is green now — one commit, and it is not in the OAUTHBEARER change

clippy was the only red job. It fails on master too, before this branch changes anything:

error: this lifetime isn't used in the type
  --> tokio-postgres/tests/test/types/mod.rs:51:24
   |
51 |     T: PartialEq + for<'a> FromSqlOwned + ToSql + Sync,
   |                        ^^

extra_unused_lifetimes learned to see through a higher-ranked bound in a clippy released after master's last CI run, and the workflow pins dtolnay/rust-toolchain@stable rather than a version — so the next push to touch CI meets the newer lint. Any PR opened right now is red before it does anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support SASL OAUTHBEARER authentication (PostgreSQL 18 oauth HBA method)

1 participant