feat: JWT authentication with auto-provisioning and read-only enforcement#1084
Open
ziomarco wants to merge 5 commits into
Open
feat: JWT authentication with auto-provisioning and read-only enforcement#1084ziomarco wants to merge 5 commits into
ziomarco wants to merge 5 commits into
Conversation
Adds JSON Web Token (JWT) authentication as a new auth_type option. When enabled, clients present a JWT as their password; pgDog validates the signature (PEM file or JWKS endpoint), extracts the configured username claim (default: sub), and optionally auto-provisions a connection pool for that user. Key changes: - pgdog/src/auth/jwt/ — new module: JwtValidator, Claims, PEM cache, tests (validation, expiry, audience, algorithm, memory-leak, suffix) - pgdog-config/src/auth.rs — add Jwt variant to AuthType - pgdog-config/src/general.rs — add 10 JWT config fields with env var support and tests - pgdog/src/config/mod.rs — JWT_VALIDATOR static + jwt_validator() - pgdog/src/config/convert.rs — user_from_jwt() helper - pgdog/src/frontend/client/mod.rs — JWT auth flow in login(); effective_user derived from token claim used for all downstream ops - pgdog/src/backend/pool/address.rs — client_user field (drives SET ROLE) - pgdog/src/backend/server.rs — execute SET ROLE after connect when client_user differs from server user - pgdog/src/backend/pool/cluster.rs — user_read_only flag; forces ExcludePrimary rw_split for read-only users - pgdog/src/backend/pool/request.rs — replica_only field - pgdog/src/backend/pool/lb/mod.rs — respect replica_only in routing - pgdog/src/backend/pool/monitor.rs — shut down pool on auth error; replenish() propagates errors - pgdog/src/backend/pool/error.rs — Auth variant + is_auth() - pgdog/src/backend/databases.rs — recreate offline pools in add() - pgdog/src/backend/error.rs — extend is_auth() for codes 22023/42704 - pgdog/src/frontend/router/parser/query/mod.rs — enforce read-only routing when user_read_only; also apply to StartTransaction routes - example.pgdog.toml — document all JWT configuration options Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds integration/jwt/ with:
- docker-compose.yml — postgres + pgdog (JWT mode) + python test runner
- Dockerfile.pgdog — builds pgdog from source
- Dockerfile.test — python test image
- pgdog.toml / users.toml — JWT-mode config pointing at postgres service
- generate_keys.sh — generates RSA key pair for local testing
- conftest.py — pytest fixtures: key loading, token_factory, DSN helpers
- test_jwt.py — E2E tests covering:
- valid JWT connects and queries succeed
- auto-provisioned pools per unique sub claim
- simultaneous multi-user sessions
- expired / tampered / plain-string tokens rejected
- transactions (commit + rollback) through proxy
- custom claim passthrough
Run locally:
cd integration/jwt
bash generate_keys.sh
docker-compose up --build --abort-on-container-exit
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Escape embedded double quotes in the JWT-derived role name before interpolating it into SET ROLE, preventing identifier injection. - Collapse nested if-let blocks and use is_none_or to satisfy clippy.
Running the integration suite (integration/jwt) surfaced several bugs in the JWT auth path; all 12 tests now pass. pgdog fixes: - Launch backend pools for users that only have server-side credentials (server_password / external identity) even when no client-facing password is configured. Auto-provisioned JWT users previously had their pools disabled with "password not set" and reported "pool is down". - Reuse an existing online pool when the JWT subject matches a user that is already configured or previously provisioned, instead of trying to re-provision it (which failed under passthrough auth with a password change error). - Enforce strict JWT expiry by setting validation leeway to 0; jsonwebtoken's default 60s leeway accepted tokens that had just expired. - Treat a failed backend SET ROLE (e.g. role does not exist) as non-fatal: log a warning and continue as the configured server user rather than dropping the connection. Integration suite fixes: - Dockerfile.pgdog: build with rust 1.96 (edition 2024) and run on debian:trixie-slim to match the builder's glibc. - pgdog.toml: use the renamed 'database_name' field. - test_jwt.py: create the table in a committed transaction before the rollback test so the verification query has a table to read.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds JWT-based client authentication to PgDog. Clients present a signed JWT as their password (cleartext auth flow); PgDog validates it against either a local RSA/EC public-key PEM file or a remote JWKS endpoint (with cached keys), maps a configurable claim (
subby default) to a Postgres role, and optionally auto-provisions a connection pool for that user. Auto-provisioned users can be marked read-only, which is enforced at the query-router level (writes are forced to replicas and write-escalation viaSET pgdog.role/ comments is blocked).What's included
AuthType::Jwtand aJwtValidator(RS256/ES256, JWKS caching with double-checked locking, audience + expiry validation).[general]:jwt_public_key_file,jwt_jwks_url,jwt_jwks_cache_ttl,jwt_audience,jwt_username_claim,jwt_server_user,jwt_server_password,jwt_user_suffix,jwt_user_auto_provision,jwt_user_auto_provision_read_only.SET ROLEto the JWT-derived role when it differs from the connection user (identifier is escaped defensively).integration/jwt/(docker-compose + pytest) and unit tests.Motivation
At my company we run on Cloud SQL and rely heavily on Cloud SQL IAM authentication — we have internal tooling that automates provisioning, and our developers' local tooling authenticates against it automatically. We're now evaluating Postgres on Kubernetes with CloudNativePG, and JWT-based auth is the natural fit for that environment. We discovered PgDog, started using it, and wanted to contribute this capability back.
Note on authorship/affiliation: This is entirely my own personal work. It is not owned by, sponsored by, or otherwise affiliated with my employer — I'm only mentioning the company to give context on the production use case that motivated it.
Testing
cargo fmt --check,cargo clippy(clean), JWT/auth/backend unit tests pass viacargo nextest.integration/jwt/.Happy to iterate on naming, defaults, or the read-only enforcement approach.
Of course I'm more than happy to contribute to the project documentation as well, if you may explain me how to do it :)