Skip to content
This repository was archived by the owner on Feb 3, 2026. It is now read-only.

Commit bc20183

Browse files
committed
feat(keyserver): implement sidecar to cache and publish JWK set
1 parent 2042d16 commit bc20183

16 files changed

Lines changed: 318 additions & 62 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.idea
22
*.tgz
3+
/charts/postgrest/keyserver/test.*

charts/postgrest/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
apiVersion: v2
22
name: postgrest
33
icon: https://docs.postgrest.org/en/v14/_images/postgrest.png
4-
version: 0.2.5
4+
version: 0.3.0
55
maintainers:
66
- name: jared-prime
77
email: jared.davis@pelo.tech
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
test.env
2+
test.jwks.json
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM denoland/deno:alpine
2+
3+
LABEL authors="Jared Davis <jared.davis@pelo.tech>"
4+
5+
WORKDIR /keyserver
6+
7+
COPY deno.json deno.json
8+
COPY deno.lock deno.lock
9+
COPY main.ts main.ts
10+
11+
RUN deno install --entrypoint main.ts
12+
13+
CMD [ "deno", "run", "--allow-net", "--allow-env", "--allow-read", "--allow-write", "main.ts" ]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"tasks": {
3+
"dev": "deno run --env-file=test.env --watch main.ts"
4+
},
5+
"imports": {
6+
"@std/assert": "jsr:@std/assert@1",
7+
"hono": "npm:hono@^4.11.5",
8+
"jose": "npm:jose@^6.1.3"
9+
}
10+
}

charts/postgrest/keyserver/deno.lock

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/postgrest/keyserver/main.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Hono } from 'hono';
2+
import { generateKeyPair, JSONWebKeySet, JWK, calculateJwkThumbprintUri } from 'jose';
3+
import { GenerateKeyPairResult } from 'jose/key/generate/keypair';
4+
import { exportJWK } from "jose/key/export";
5+
import { SignJWT } from "jose/jwt/sign";
6+
import { cors } from 'hono/cors';
7+
8+
const path = Deno.env.get('PGRST_JWT_SECRET')?.replace('@', '') ?? '/tmp/jwks.json';
9+
const claims = JSON.parse(Deno.env.get('PGRST_JWT_CLAIMS') ?? '{}');
10+
const origin = Deno.env.get('PGRST_CLIENT_ORIGIN')?.split(',') ?? [];
11+
const trusted = Deno.env.get('PGRST_JWK_TRUST')?.split(',') ?? [];
12+
const cert = Deno.env.get('PGRST_JWK_CERT') ?? 'cert.pem';
13+
const alg = Deno.env.get('PGRST_JWT_ALG') ?? 'RS256';
14+
const iss = Deno.env.get('PGRST_JWT_ISS') ?? 'http://localhost:8000/jwks'
15+
const aud = Deno.env.get('PGRST_JWT_AUD') ?? 'postgrest';
16+
const exp = Deno.env.get('PGRST_JWT_EXP') ?? '5 minutes';
17+
const sub = Deno.env.get('PGRST_JWT_SUB') ?? 'anon';
18+
const api = Deno.env.get('PGRST_CLIENT_KEY') ?? '';
19+
20+
let keypair: GenerateKeyPairResult;
21+
const initialize = async () => {
22+
keypair = await generateKeyPair(alg, { extractable: true });
23+
const keysets = await jwk(keypair.publicKey, ...await upstream());
24+
25+
keysets.keys[0].kid = await calculateJwkThumbprintUri(keysets.keys[0])
26+
27+
await write(keysets);
28+
localStorage.setItem('jwk:kid', keysets.keys[0].kid);
29+
localStorage.setItem('jwk:val', JSON.stringify(keysets.keys[0]))
30+
}
31+
32+
const upstream = async (): Promise<Array<JWK>> => {
33+
const certificate = await Deno.readTextFile(cert).catch((error) => {
34+
console.warn(error)
35+
return undefined
36+
})
37+
const caCerts = certificate ? [certificate] : [];
38+
39+
const client = Deno.createHttpClient({ caCerts })
40+
41+
const keyset = new Array<JWK>();
42+
43+
for await (const address of trusted) {
44+
try {
45+
const response = await fetch(address, { client })
46+
console.log(response.status)
47+
const data = await response.json() as JSONWebKeySet;
48+
keyset.push(...data.keys)
49+
} catch (error) {
50+
console.warn(error)
51+
}
52+
}
53+
console.debug(keyset);
54+
55+
return keyset;
56+
}
57+
58+
const jwk = async (key: CryptoKey, ...jwks: Array<JWK>): Promise<JSONWebKeySet> => {
59+
const jwk = await exportJWK(key);
60+
61+
const keys = [ jwk, ...jwks].filter(value => !!value);
62+
63+
return { keys }
64+
}
65+
66+
const jwt = async (key: CryptoKey, kid?: string): Promise<string> => await new SignJWT(claims)
67+
.setProtectedHeader({ alg, kid })
68+
.setAudience(aud)
69+
.setIssuedAt()
70+
.setExpirationTime(exp)
71+
.setSubject(sub)
72+
.setIssuer(iss)
73+
.sign(key);
74+
75+
76+
const write = async (keys: JSONWebKeySet) => {
77+
const file = await Deno.create(path);
78+
const encoder = new TextEncoder();
79+
const data = encoder.encode(JSON.stringify(keys))
80+
81+
await file.write(data)
82+
}
83+
84+
const app = new Hono();
85+
86+
app.use('/jwks/.well-known/openid-configuration', cors({ origin }))
87+
app.get('/jwks/.well-known/openid-configuration', (context) => {
88+
const keyset = localStorage.getItem('jwk:val');
89+
90+
if (keyset) return context.json(JSON.parse(keyset));
91+
92+
return context.json({message:'not found'}, 404);
93+
});
94+
95+
app.get('/', (context) => context.redirect('/jwks/.well-known/openid-configuration', 301));
96+
app.get('/jwks', (context) => context.redirect('/jwks/.well-known/openid-configuration', 301));
97+
98+
app.get('/anon', async (context) => {
99+
const auth = context.req.header('Authorization')?.replace('Bearer ', '');
100+
101+
if (api && api != auth) return context.json({message:'not authorized'}, 401)
102+
103+
const kid = localStorage.getItem('jwk:kid') ?? '';
104+
105+
return context.json({ access_token: await jwt(keypair.privateKey, kid) })
106+
});
107+
108+
initialize().finally(() => console.log(`initialized application with kid ${localStorage.getItem('kid')}`));
109+
110+
Deno.serve(app.fetch);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
PGRST_JWT_SECRET="test.jwks.json"
2+
PGRST_JWT_CLAIMS="{\"mode\":\"test\"}"
3+
PGRST_CLIENT_ORIGIN="https://jwt.io"
4+
PGRST_JWK_TRUST="https://sso.localhost/auth/realms/od360-kind/protocol/openid-connect/certs"
5+
PGRST_JWT_ALG="RS256"
6+
PGRST_JWT_ISS="http://localhost:8000/jwks"
7+
PGRST_JWT_AUD="postgrest"
8+
PGRST_JWT_EXP="5 minutes"
9+
PGRST_JWT_SUB="test"
10+
PGRST_CLIENT_KEY="a-string-secret-at-least-256-bits-long"

charts/postgrest/templates/_helpers.tpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@
1313
{{- $hostname := .Values.database.migrations.hostname }}
1414
{{- printf "user=%s password=%s host=%s dbname=%s sslmode=disable" $username $password $hostname $database }}
1515
{{- end -}}
16+
17+
{{- define "postgrest.jwt.claims" }}
18+
{{- printf "{%s:%s}" (.Values.application.jwt.claim | quote ) (.Values.application.anon | quote )}}
19+
{{- end }}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: goose
5+
data:
6+
GOOSE_DRIVER: "postgres"
7+
8+
---
9+
10+
apiVersion: v1
11+
kind: ConfigMap
12+
metadata:
13+
name: keyserver
14+
data:
15+
PGRST_JWT_ALG: {{ .Values.keyserver.jwt.alg | quote }}
16+
PGRST_JWT_ISS: {{ .Values.keyserver.jwt.iss | quote }}
17+
PGRST_JWT_EXP: {{ .Values.keyserver.jwt.exp | quote }}
18+
PGRST_JWT_SUB: {{ .Values.keyserver.jwt.sub | quote }}
19+
PGRST_CLIENT_ORIGIN: {{ .Values.keyserver.jwt.origin | quote }}
20+
PGRST_JWK_TRUST: {{ .Values.keyserver.jwt.trust | quote }}
21+
PGRST_JWT_CLAIMS: {{ .Values.keyserver.jwt.claims | toJson | quote }}
22+
23+
---
24+
25+
apiVersion: v1
26+
kind: ConfigMap
27+
metadata:
28+
name: postgrest
29+
data:
30+
PGRST_DB_ANON_ROLE: {{ .Values.application.anon | quote }}
31+
PGRST_DB_SCHEMAS: {{ .Values.application.schemas | quote }}
32+
PGRST_JWT_ROLE_CLAIM_KEY: {{ .Values.application.jwt.claim.selector | quote }}
33+
PGRST_JWT_SECRET: "@/etc/opt/postgrest/certificates/jwks.json"
34+
35+
---

0 commit comments

Comments
 (0)