https://github.com/vishvish/pgjwt
Modern JWT (JSON Web Tokens) implementation for Postgres that can handle Ed25519
https://github.com/vishvish/pgjwt
jwt jwt-validation postgresql rust
Last synced: 1 day ago
JSON representation
Modern JWT (JSON Web Tokens) implementation for Postgres that can handle Ed25519
- Host: GitHub
- URL: https://github.com/vishvish/pgjwt
- Owner: vishvish
- License: mit
- Created: 2025-10-25T20:09:28.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2026-05-30T15:15:00.000Z (3 days ago)
- Last Synced: 2026-05-30T17:08:19.529Z (3 days ago)
- Topics: jwt, jwt-validation, postgresql, rust
- Language: Rust
- Homepage:
- Size: 94.7 KB
- Stars: 8
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# pgjwt
[](https://github.com/vishvish/pgjwt_rs/actions/workflows/tests.yml)
PostgreSQL extension for JWT verification with RS256 and Ed25519 support.
## Overview
This extension provides cryptographically secure JWT validation at the database layer using the Rust `jsonwebtoken` crate. Unlike the pure-SQL `pgjwt` extension which only supports HMAC algorithms, `pgjwt_rs` supports asymmetric key algorithms:
- **RS256** (RSA with SHA-256)
- **Ed25519** (EdDSA)
## Why This Extension?
### The Problem
The popular `pgjwt` extension only supports HMAC-based JWT validation (HS256/384/512), which requires sharing secret keys between services. This is insecure for multi-service architectures because:
- Any service with the shared secret can forge tokens for other services
- Compromising one service compromises all services
- No way to cryptographically prove token origin
### The Solution
Asymmetric key cryptography with `pgjwt_rs`:
- Each service has a unique private key (never shared)
- Database stores only public keys
- Services sign JWTs with their private key
- Database verifies signatures with public keys
- **Mathematically impossible** to forge tokens without the private key
## Functions
### `jwt_decode_payload(token TEXT) -> JSONB`
Extract the payload from a JWT without verification. Useful for getting the issuer to look up the correct public key.
```sql
SELECT jwt_decode_payload('eyJhbGc...');
-- Returns: {"iss": "auth_guard", "sub": "...", ...}
```
### `jwt_verify_rs256(token TEXT, public_key TEXT) -> TABLE(header JSONB, payload JSONB, valid BOOLEAN)`
Verify a JWT token using RS256 algorithm.
```sql
SELECT * FROM jwt_verify_rs256(
'eyJhbGciOiJSUzI1NiI...',
'-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----'
);
```
### `jwt_verify_ed25519(token TEXT, public_key TEXT) -> TABLE(header JSONB, payload JSONB, valid BOOLEAN)`
Verify a JWT token using Ed25519 algorithm.
```sql
SELECT * FROM jwt_verify_ed25519(
'eyJhbGciOiJFZERTQSI...',
'-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=
-----END PUBLIC KEY-----'
);
```
### `jwt_verify(token TEXT, public_key TEXT, algorithm TEXT) -> TABLE(header JSONB, payload JSONB, valid BOOLEAN)`
Verify a JWT token with the specified algorithm ('RS256' or 'EdDSA').
```sql
SELECT * FROM jwt_verify(
'eyJhbGc...',
'-----BEGIN PUBLIC KEY-----...',
'RS256'
);
```
## Installation
### Prerequisites
- Rust toolchain (1.70+)
- PostgreSQL 18 (or 13-17 with appropriate feature flags)
- PostgreSQL development headers (`postgresql-server-dev` on Debian/Ubuntu, `postgresql-devel` on RHEL)
- `cargo-pgrx` version 0.16.1
### Build and Install
There are three supported ways to build + package the extension:
1. **Docker-based build (recommended)** — produces a fully packaged `pkg/` directory without needing local Postgres dev packages.
2. **Local `package.sh` build** — requires Postgres dev headers installed locally.
3. **Manual Rust build** — for advanced users who want to run `cargo build` directly.
#### 1) Docker build (recommended)
This repository includes a helper script that builds the extension in a container and extracts the resulting `pkg/` output.
```bash
# Build and extract artifacts into ./out/pkg
./build.sh
# If you want to customize the image tag or output directory:
# ./build.sh mytag my-output-dir
```
The resulting files will be in `out/pkg/usr/lib/postgresql/` and `out/pkg/usr/share/postgresql/extension/`.
#### 2) Local package.sh build (requires Postgres dev headers)
```bash
# Install cargo-pgrx (matching pgrx version)
cargo install --locked cargo-pgrx --version 0.17.0
# Initialize pgrx for PostgreSQL 18 (one-time setup)
cargo pgrx init --pg18 $(which pg_config)
# Build and package the extension
chmod +x package.sh
./package.sh
```
#### 3) Install into a running PostgreSQL instance
```bash
sudo cp pkg/usr/lib/postgresql/pgjwt_rs.so $(pg_config --pkglibdir)/
sudo cp pkg/usr/share/postgresql/extension/* $(pg_config --sharedir)/extension/
```
On macOS, the library extension will be `.dylib` instead of `.so`.
### Cutting a Release
This repository publishes release artifacts when you push a version tag. Steps:
1. Bump the version in `Cargo.toml`.
2. Commit and push.
3. Create and push a tag matching the version, e.g. `v0.1.0`.
The CI release workflow will build the extension and attach a tarball to the GitHub Release.
### Enable in Database
```sql
CREATE EXTENSION pgjwt_rs;
```
## Usage Example
```sql
-- Create a function to validate service JWTs
CREATE OR REPLACE FUNCTION validate_service_jwt(token TEXT)
RETURNS TABLE(
service_name TEXT,
tenant_id UUID,
scopes TEXT[],
valid BOOLEAN
) AS $$
DECLARE
payload JSONB;
jwt_result RECORD;
public_key_pem TEXT;
algorithm TEXT;
BEGIN
-- Get unverified payload to find issuer
payload := jwt_decode_payload(token);
-- Look up public key for the claimed service
SELECT pk.public_key, pk.algorithm
INTO public_key_pem, algorithm
FROM auth_schema.jwt_keys pk
WHERE pk.service_name = payload->>'iss'
AND pk.is_active = true;
IF public_key_pem IS NULL THEN
RETURN QUERY SELECT NULL::TEXT, NULL::UUID, NULL::TEXT[], FALSE;
RETURN;
END IF;
-- Verify JWT with correct algorithm
SELECT * INTO jwt_result
FROM jwt_verify(token, public_key_pem, algorithm);
IF jwt_result.valid THEN
payload := jwt_result.payload;
RETURN QUERY SELECT
(payload->>'iss')::TEXT,
(payload->>'tenant_id')::UUID,
ARRAY(SELECT jsonb_array_elements_text(payload->'scopes')),
TRUE;
ELSE
RETURN QUERY SELECT NULL::TEXT, NULL::UUID, NULL::TEXT[], FALSE;
END IF;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```
## Security Features
- **Signature Verification**: Always validates cryptographic signatures
- **Algorithm Enforcement**: Explicitly specify which algorithm to use
- **Public Key Validation**: Validates PEM format before use
- **Error Handling**: Safe error messages, no key material leaks
- **Memory Safety**: Built with Rust's memory safety guarantees
## Security posture
- **No unsafe code**: The crate avoids `unsafe` blocks unless explicitly justified and reviewed.
- **Secret handling**: JWT keys and tokens are never logged or exposed in error messages.
- **CI gating**: GitHub Actions validate formatting, linting, tests, dependency advisories, and license policy.
- **Supply-chain checks**: Run `cargo audit` and `cargo deny check advisories licenses bans sources` locally before merging.
### Validation behavior
This extension focuses on cryptographic verification and intentionally defers claim validation to SQL so you can express policy close to data:
- Signature validation is always enforced.
- The default required JWT spec claims (like `exp`) are not enforced by the extension. Tokens without `exp` will be accepted at the cryptographic level.
- `exp`, `nbf`, and `aud` checks are disabled in Rust and can be applied in SQL, e.g. by checking `now() < to_timestamp((payload->>'exp')::bigint)` if present.
Rationale: application-specific TTLs, audiences, and clock tolerance are often best handled in database logic or service code rather than hard-coded in the extension.
## Performance
Built with Rust and compiled to native code, `pgjwt_rs` provides:
- Fast signature verification (native crypto operations)
- Minimal overhead compared to SQL-only solutions
- Efficient JSONB handling
## Development
### Running Tests (Docker — recommended)
No local PostgreSQL installation needed:
```bash
./docker-test.sh # fmt + clippy + unit tests
./docker-test.sh build # … then package the extension
```
### Running Tests (locally)
Requires a local PostgreSQL installation and `cargo-pgrx` initialised.
First, generate PKCS#8 test key pairs:
```bash
# Generate RS256 test keys (PKCS#8 for ring)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out test_private.pem
openssl pkey -in test_private.pem -pubout -out test_public.pem
# Generate Ed25519 test keys
openssl genpkey -algorithm ed25519 -out test_ed25519_private.pem
openssl pkey -in test_ed25519_private.pem -pubout -out test_ed25519_public.pem
```
Then run tests:
```bash
# Run Rust unit tests (with coverage)
cargo install cargo-llvm-cov
cargo llvm-cov --html
# Run PostgreSQL integration tests
cargo pgrx test pg18
```
### Building for Development
```bash
# Run with test database (opens psql with extension loaded)
cargo pgrx run pg18
# Build for specific PostgreSQL version
cargo build --release --features pg18 --no-default-features
```
## License
MIT - see LICENSE file for details.
## Credits
Built with:
- [pgrx](https://github.com/pgcentralfoundation/pgrx) - Rust framework for PostgreSQL extensions
- [jsonwebtoken](https://github.com/Keats/jsonwebtoken) - JWT encoding/decoding for Rust