Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWKS: load public keys from a well known endpoint #1130

Open
purpleKarrot opened this issue Jun 13, 2018 · 30 comments
Open

JWKS: load public keys from a well known endpoint #1130

purpleKarrot opened this issue Jun 13, 2018 · 30 comments
Labels
config related to the configuration options

Comments

@purpleKarrot
Copy link
Contributor

Please see https://auth0.com/docs/jwks

Auth0 exposes a JWKS endpoint for each tenant, which is found at https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json. This endpoint will contain the JWK used to sign all Auth0 issued JWTs for this tenant.

It would be great of PostgREST could load the public key from such an endpoint.

@ruslantalpa
Copy link
Contributor

Or ... you install postgrest as a system service (https://docs.subzero.cloud/production-infrastructure/ubuntu-server/) and use systemd ExecStartPre directive to run one curl call that places that key in the file postgrest is configured to read...

@photz
Copy link

photz commented Jun 14, 2018

@purpleKarrot Did you get PostgREST to work with the JWK from Auth0 in general? I copied the first key in mytenant.auth0.com/.well-known/jwks.json into a file and set jwt-secret = "@thefile" in the config file as describe in the docs here.
Unfortunately PostgREST still always responds with

{
    "message": "JWSError JWSInvalidSignature"
}

Edit: I just found the solution: frasertweedale/hs-jose#56 (comment)

@jcozain
Copy link

jcozain commented Jun 15, 2018

i was just about to ask something similar. does the jwk accepts the x5u parameter. i tried authenticating with firebase ID token (jwt signed with asymetrical RSA) and only could make it work with a node js library to transform the cert into a "standard" jwk [jwk]. wich is not very maintainable because google has 2 public keys wich are going to change periodically.
here are the keys (x5u compliant )
google public key

@jcozain
Copy link

jcozain commented Jun 22, 2018

i made a nodejs script to fectch the certs and mint a jwks, the individual jwk work but when i put them both in a jwks it stops validating. im guessing this is not suported. i can get by because luckyly all of the jwt provided by google firebase had been of the same "kid" so im using just one key. any info would be welcome.

@photz
Copy link

photz commented Jun 26, 2018

@jcozain Yes, PostgREST seems to only support single JSON Web Keys at the moment, not JSON Web Key Sets.
I am using jq to extract the first JWK and remove the properties that hs-jose currently seems to be incompatible with (namely x5c and x5t).

wget --quiet -O - https://yourcompany.eu.auth0.com/.well-known/jwks.json \
    | jq " {
               alg: .keys[0].alg,
               kty: .keys[0].kty,
               use: .keys[0].use,
               n: .keys[0].n,
               kid: .keys[0].kid,
               e: .keys[0].e
               }
               
               " > rsa.jwk.pub

@a-mckinley
Copy link

Or ... you install postgrest as a system service (https://docs.subzero.cloud/production-infrastructure/ubuntu-server/) and use systemd ExecStartPre directive to run one curl call that places that key in the file postgrest is configured to read...

Is there a straightforward way to make this happen with the docker image?

@savv
Copy link

savv commented Dec 13, 2019

Another solution for supporting firebase auth (or auth0), without worrying about JWKS support:

  • put PostgREST behind nginx
  • In nginx, use auth_request to have it reach out to a custom made backend that validates auth tokens (e.g. using firebase auth's admin lib); proxy responds with a custom-proxy-user response header
  • Use auth_request_set $auth_user $upstream_http_custom_proxy_user; and proxy_set_header to pass it on to PostgREST
  • use that instead of the JWT claim

Does this mean that JWT handling is another thing that PostgREST doesn't need to do? :-)

@pauldellinger
Copy link

How would you set a role without a JWT claim?

@savv
Copy link

savv commented Dec 21, 2019

I meant that tongue-in-cheek. You could simply pass the role as another, unsigned, response header. Which, I think, would be fine in an environment where postgrest is behind nginx.

(In my case, I ended up generating a JWT in my auth proxy and passing that to postgrest, because JWTs are well integrated with the role system.)

@steve-chavez steve-chavez added the config related to the configuration options label Jan 24, 2020
@theonewolf
Copy link

It would be really awesome if we could load JWK key sets from URLs instead of local files such as:

https://www.googleapis.com/robot/v1/metadata/jwk/[email protected]

This would prevent operator error in remembering to rotate those key sets once they expire as well---PostgREST should be able to reload them from the URL on expiration.

@steve-chavez
Copy link
Member

For the meantime, @rockodragon shared a nodejs solution on gitter chat: https://gitter.im/begriffs/postgrest?at=5ea5106b568e5258e4852dbc

@theonewolf
Copy link

@rockodragon thanks! I made something similar in Python, but don't have a great place to run it right now.

@savv
Copy link

savv commented May 8, 2021

It would be really awesome if we could load JWK key sets from URLs instead of local files such as:

https://www.googleapis.com/robot/v1/metadata/jwk/[email protected]

This would prevent operator error in remembering to rotate those key sets once they expire as well---PostgREST should be able to reload them from the URL on expiration.

By the way, if you loaded the the key sets from that URL, wouldn't you run the the risk of authorizing any firebase user to access your postgrest? You'd need another mechanism to restrict it to your audience?

@bohanyang
Copy link

I'd like to share my solution. I wrote this to integrate PostgREST with ZITADEL (an open-source Auth0 alternative).
It's tricky that ZITADEL rotates the keys for every 6 hours by default, so the keys should be updated constantly.
I wrote a script to do that by updating the PostgREST setting in-database.

https://gist.github.com/bohanyang/aabb562ad12906ff13420a907c87d2ca

@hiteshjoshi
Copy link

I dont like the idea of using database for config.

So I did this

FROM debian:latest

# Install dependencies
RUN apt-get update && apt-get install -y \
    cron \
    supervisor \
    curl \
    jq \
    postgresql-client \
    postgrest

# Create necessary directories
RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d /app

# Copy the Supervisor configuration file
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

# Copy the cron job script
COPY fetch_jwk.sh /app/fetch_jwk.sh
RUN chmod +x /app/fetch_jwk.sh

# Copy the entrypoint script
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh

# Copy the crontab file
COPY crontab /etc/cron.d/fetch_jwk
RUN chmod 0644 /etc/cron.d/fetch_jwk

# Apply the cron job
RUN crontab /etc/cron.d/fetch_jwk

# Create the log file to be able to run tail
RUN touch /var/log/cron.log

# Entrypoint
ENTRYPOINT ["/app/entrypoint.sh"]
[supervisord]
nodaemon=true

[program:postgrest]
command=/usr/local/bin/postgrest /app/postgrest.conf
autostart=true
autorestart=true
stderr_logfile=/var/log/postgrest.err.log
stdout_logfile=/var/log/postgrest.out.log

[program:cron]
command=cron -f
autostart=true
autorestart=true
stderr_logfile=/var/log/cron.err.log
stdout_logfile=/var/log/cron.out.log

(fetch_jwk.sh)

#!/bin/sh

# Fetch the JWK secret from the Supertokens API server
JWK_RESPONSE=$(curl -s http://127.0.0.1:3000/auth/jwt/jwks.json)

# Escape the JWK response for the config file
ESCAPED_JWK_RESPONSE=$(echo "$JWK_RESPONSE" | jq -sRr @json)

# Update postgrest.conf
cat <<EOF > /app/postgrest.conf
db-uri = "$DATABASE_URL"
db-schema = "ploton_api"
db-anon-role = "web_anon"
jwt-secret = $ESCAPED_JWK_RESPONSE
server-port = 9000
server-host = "0.0.0.0"
server-cors-allowed-origins="$ALLOWED_CORS"
EOF

# Reload PostgREST
killall -SIGUSR2 postgrest

crontab

# Run fetch_jwk.sh every 6 days
0 0 */6 * * /app/fetch_jwk.sh >> /var/log/cron.log 2>&1

Basically updating the config file and then notifying postgrest about the config reload.

@wolfgangwalther
Copy link
Member

wolfgangwalther commented Jun 17, 2024

So I did this
[...]
Basically updating the config file and then notifying postgrest about the config reload.

Nice, thanks for sharing. You can avoid rewriting the whole config file and also the escaping the secret by just writing the secret to a separate file and then including it via jwt-secret ="@<path-to-file>".

@hiteshjoshi
Copy link

oh nice. I didn't know about the filepath. Thanks for sharing.

@mike-eh2
Copy link

Thanks @hiteshjoshi. I can share an iteration on your solution that includes some simplifications:

Set PGRST_JWT_SECRET to "@/app/jwk.pub".

Dockerfile

FROM postgrest/postgrest

USER root

RUN apt-get update && apt install -y curl cron psmisc

# Copy the cron job script
COPY update_jwk.sh /app/update_jwk.sh
RUN chmod +x /app/update_jwk.sh

# Copy the crontab file
COPY crontab /etc/cron.d/update_jwk
RUN chmod 0644 /etc/cron.d/update_jwk

# Apply the cron job
RUN crontab /etc/cron.d/update_jwk

# Get the current JWK
RUN ./app/update_jwk.sh

crontab

# Run update_jwk.sh at midnight
0 0 */1 * * /app/update_jwk.sh >> /var/log/cron.log 2>&1

update_jwk.sh

#!/bin/sh

# Fetch the public JWK
curl $JWK_URL > /app/jwk.pub

# Ask PostgREST to reload config
# Fails during Docker build because PostgREST isn't running yet
killall -SIGUSR2 postgrest || true

@atatdotdot
Copy link

atatdotdot commented Jan 9, 2025

@steve-chavez Would it align with the ethos of the project to include dynamic JWKS loading from a well-known endpoint directly in PostgREST? Or is it preferable for such functionality to be handled as an external exercise as implemented here?

@wolfgangwalther
Copy link
Member

Would it align with the ethos of the project to include dynamic JWKS loading from a well-known endpoint directly in PostgREST? Or is it preferable for such functionality to be handled as an external exercise as implemented here?

To be honest, I don't see the value, really. The example above is essentially a single cron line and single curl call. I was thinking back and forth between adding a full solution (which would need to support scheduling + the request) or a more generic solution (just automatic reloading after an interval and then calling a custom command)... but I can't come up with anything that really makes the effort to implement this worth it.

@monneyboi
Copy link

To be honest, I don't see the value, really.

In my case, the added value would be being able to just set some environment vars & run PostgREST. Now i have to add some cruft in my docker-compose.yml file to get it working.

I see less value in automatically refreshing, but the loading part would save everybody that uses PostgREST with JWKS a trip to this here ticket.

@wolfgangwalther
Copy link
Member

I see the value - if we could make sure it works reliably. Since the "well known JWKS" is not standardized anywhere, I think, I feat that we might end up not being able to support all use-cases. Some might need custom headers. Some might need filters like in your example (where you use jq etc.). And suddenly 90% of users are back to writing scripts.

My next idea then was to support something like a jwt-secret-command, where you can specify your own command to run to fetch the keys. This'd give you more flexibility.. but suddenly the savings are not that big anymore, because you need to extend the PostgREST docker image. And that's annoying, because it's a single binary image.

So yeah, I don't know what's the best way to proceed.

@rudetrue
Copy link

Automatically refreshing would allow rotating keys without restarting too. It seems like most things have standardized on fetching the JWK for this reason, from what I've seen.

@wolfgangwalther
Copy link
Member

Automatically refreshing would allow rotating keys without restarting too.

This can be achieved already with the solutions proposed / posted above. You don't need built-in JWKS fetching for it.

@rudetrue
Copy link

Yes but it's not easy. I spent 2-4 hours today trying to figure out the right jq incantation to parse the JWKS endpoint in my curl command. On top of adding a container just to do this task on PostgREST startup.

@wolfgangwalther
Copy link
Member

It seems like most things have standardized on fetching the JWK for this reason

This is kind of the point that I was looking for - but haven't found anything. Is there any kind of standard for this?

If there was, there would be a lot more reason to implement something based on that.

Doesn't even need to be an RFC, just something that writes up what "everyone" is doing. We don't want to build something that only works in some cases, but not others.

@monneyboi
Copy link

monneyboi commented Jan 25, 2025 via email

@wolfgangwalther
Copy link
Member

You can request OpenID configuration to get a jwks_uri

Aha!

Now.. would we add support for querying OpenID configuration and then fetch jwks_uri on top? Or would we only support jwks_uri directly?

It seems like setting the OpenID config URL might make more sense, WDYT?

@monneyboi
Copy link

monneyboi commented Jan 25, 2025

It seems like setting the OpenID config URL might make more sense, WDYT?

Yeah i agree. It seems querying the OpenID connect endpoint is the way others are handling this to. For example: OpenSearch, Minio.

@hmoazzem
Copy link

hmoazzem commented Mar 2, 2025

I've been trying to figure a way out, and came up with https://github.com/edgeflare/edge/blob/main/internal/util/postgrest/rotate-jwt.go.go and calling it like

func main() {
	ctx := context.Background()
	pool, _ := pgxpool.New(ctx, os.Getenv("CONN_STRING"))
	err := postgrest.RotateJwtKey(ctx, pool, os.Getenv("ZITADEL_JWK_URL"))
	if err != nil {
		log.Fatalln(err)
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
config related to the configuration options
Development

No branches or pull requests