Skip to content

Commit

Permalink
feat: add asymmetric jwt support (#1674)
Browse files Browse the repository at this point in the history
## What kind of change does this PR introduce?
* Adds asymmetric JWT support to auth, with zero downtime key rotation

## What is the current behavior?
* Auth only supports symmetric JWTs which involves some downtime if the
key needs to be rolled

## What is the new behavior?

### Config changes
* Accepts a new env var `GOTRUE_JWT_KEYS` which takes in an array of JWK
* The private key is encoded as a JWK that contains the kid, use and alg
claims
* Defaults to use `GOTRUE_JWT_SECRET` and `GOTRUE_JWT_KEY_ID` if
`GOTRUE_JWT_KEYS` is missing, which is just the JWK representation of
the symmetric secret
* On config initialisation, `GOTRUE_JWT_KEYS` is transformed and stored
as JWKs in-memory.
* We use the `key_ops` claim in the JWK and to detect if they should be
used to sign or verify a JWT (see
[RFC](https://datatracker.ietf.org/doc/html/rfc7517#section-4.2))
* All JWKs represented as public keys will have the `key_ops` claim set
to `["verify"]`, while the JWK represented as private keys will have the
`key_ops` claim set to `["sign", "verify"]` if it is used for signing

### Endpoint
* `GET /.well-known/jwks.json`: returns the JWKs for the auth service.
Given the following config (generated from [this
script](https://gist.github.com/kangmingtay/a1c83d9e1ea1f398d9388e2188deab2b)):
```bash
GOTRUE_JWT_KEYS='[{"kty":"oct","k":"KtxwCvCPABNiOmUBij2_uzlO8FM477lO1zpe_E6nQhE","kid":"81763ee4-803e-4420-bed2-6849ef963262","key_ops":["verify"],"alg":"HS256"},{"kty":"RSA","n":"htA_Lzcc3qojwvcrF1JU6yPPRLvxvCp8x3tx_lCO6GyBFktE6HLsIHEpcWfvkiJfwxMZ4npn2CWI4rjjNbT2BHqax7CUOgGFATNZe13kTukx8SUQY3GHCIzPiN39oc55HcMBB_u4sLQBFD3RUCEcLqrlvwYRcTuCY317Xyn3j1YZogZ9gm6fY70v0Sj2hxLxtURr0UQurqhqRqUbXcujI6x3JqKKuk4-1o_K6J8j97hj4AcGMjRgmyi7G_7jM9hZG2SPJiFP7kbCpU1iT0rYYZptxVNUpWe6u5kg6onzXUE_s7Wu64YT7FE7xIFLg9MUrohBqWqrOjmF4IqTaU95Nw","e":"AQAB","d":"CM6rChEeLDfOTUrrgEMLNC9rN5DVupbF_xxD9rrZkzqfdk7lihAT-AycigGhx5jCS9LAIqkfhqHxHuq4QUZ4uhMucHRLQrzdrRXnNyWLqFIYxqnGt9BvY3IbjtP94WfFRtn6A8UArF6eIW3mckcveacFimTBl_Wsz4YfnLh3qV_817F9j8XQCRaaDfNVOF3_EFb61Ewvb4OxM_fa600wL4gJtnwwfQo_57E8bsvGJXYKvDLS1_3T-vrhARjZb3v-mHm7oXbl-0s_7L1orMRBJM1V5Ay5zFYcVr6ko7im4s0AZ-PA5Hkc_qk4rwzG8mrMWXBHi-NarCm-TG3dy6hBhQ","p":"udGq0glcgYqO6XRElohOJgOuQgdKbOodA0rojf1UkiSOHTcTKRqQYVeiYQAQUHfW3eyyNZt3XuZp_--SfjQMctYjD-SmPp2rpxt5Dz7ffFnNB3aKxBgzbiIMT3XHbONLEgRl0ohWzIzHBjZy6rOIqTUaAg2245DQRaZYnua8YdM","q":"ubr-DMx9AamMUcAIv4-aKwMcS_k15NLqulLwm6hjfDh5yezQw_-LWBpg5QxkFAf04lhJa__nAycLzdwpRqJ-u6P6EazGpeqtJtb4n5Zq8kE74ksouDCmymk9Nj5r2aZsfqZdDt5L3IId4AkzTM0yXhEV77dFupidVQZQy18lCI0","dp":"fd_dMnjq9EnTM6vyRnLBVZkKs2nS7eLNkoxs6rqgTnt61amYTjDTe01tDv6HDquPnzgXJJ9TBrNZPOmiN-G0SRpsF_kQ8LvIKuQ-Zqh1pfwDGrofmGS4ejOQWUd0t3tlQChAfZSkD96Rd9DsmbbSraTuIFP__zn7DCN6RvIQzMc","dq":"QuSqY4my7EpYk4kKnZPm_t7b7jEPzB57FCiTKDz5t9_PXX7BohYD5fN6OoS_9sb22B7cMt20IlqJ0dcdtqcH5iUlCACme1OOkZKTcUcHtcDxBIv1WoGLUROeTE8nIPjj0qmwko5V3FGw2OP3ag3tuhuFPxVPM-mLoPfpWZYnDHE","qi":"cyCVFBj5wM1-5syqt0xVuW5U8w6ZaPMM2F4GlkCu6lb-vNkymgmK3uGvr6p5VpJGU4UsEk1yrH3KrzlBHE4j8ssSMx4CLmq6Hpf4c9zkR8fO8-lgBdhyPmlHyIDvloeJqs4503qhFZO30UMTjVEKL6Wk_83CF00JkOhMo1uyv-A","kid":"38285a37-2843-48f9-a69c-6d72f8c4f016","key_ops":["verify"],"alg":"RS256"},{"kty":"EC","x":"8Whe4H2LCoTN4SODA7GIUrFYD-CpoYS7EvUsbOfcjn8","y":"Vs7VB8ozhyUkSy951Sq4clynrgg8URX91f6FjNDy18k","crv":"P-256","d":"UWPQ4T7opsJhdPbTohO0hf0noTkqGlEWOQrP0l_Tteo","kid":"406f824a-a71b-435d-8e0e-12ee8b07f88b","key_ops":["sign","verify"],"alg":"ES256"},{"crv":"Ed25519","d":"6MooMObDBKW1QRe1uHnrzKoWY9iJKcY55kD1rSY9jiI","x":"4yF8m6gflwZntZMc12j4hIUZFuZ5XJlAqbpnlEIgSxk","kty":"OKP","kid":"a678b12f-0f67-4802-ba7a-a3ad0ea5cc17","key_ops":["verify"],"alg":"EdDSA"}]'
```

The response returned is:
```json
{
  "keys": [
    {
      "alg": "ES256",
      "crv": "P-256",
      "kid": "fa6f71ab-03e2-435f-b2f3-9b6143a9c295",
      "kty": "EC",
      "use": "sig",
      "key_ops": ["verify"],
      "x": "3HcQyhPGXE9Tr_7VMIUvh-PJfQ_nXe_d2Ho7HWefJLA",
      "y": "CAdc8gjfA8eIwDjWzEdeRurZFUHs_OZ-SEMWcW_UUaE"
    },
    {
      "alg": "RS256",
      "e": "AQAB",
      "kid": "479be47e-ca6e-44cd-a638-b17754611553",
      "kty": "RSA",
      "n": "16Tia3phliCATvt0VtISvrWazjIw_BN6a7b5p9VerjoaZfx98l6DJfRrxi2RW6aijPPuzT3DYTfHqNkb1SQUGPKQa_OXGDzPPFXT9RTma24I6vi2VjuLmDPQI6vpAiLqCuBHQ_5gMlyN8wa7F1r86Mf1F-14p_78tBBpea1zSPFcgaODjfOens8Om2CV_eKlK-_zPMCacM96X2Gtx00QVS1UEClQaiZWMvwfdprWfg9w8D2C4ze5wNWIVeMINeO-Ajug3nvhw8UJwZS-ZqiIVD34e3gCukc4bAht-F6xO7RGmOry7UpTe-56r9reaRfpN-W2G2si_sb5zikaJuQcEQ",
      "use": "sig",
      "key_ops": ["verify"]
    }
  ]
}
```

### JWT changes
* Now supports encrypted and verifying using EC, RSA, Ed25519 and HMAC 
* Change the `use` claim in the JWK from `sig` to `enc` to specify the
signing/encryption key - this conforms with the JWK spec.
* `GOTRUE_JWT_KEYS` will default to use `GOTRUE_JWT_SECRET` if it's not
set
* A key can continue to be used for verification as long as it's present
in `GOTRUE_JWT_KEYS`
* A revoked key is one that is removed from `GOTRUE_JWT_KEYS` 
* `ValidMethods` are computed on config initialisation so we don't have
to do that on every request
  • Loading branch information
kangmingtay authored Jul 26, 2024
1 parent f0a40c5 commit c7a2be3
Show file tree
Hide file tree
Showing 13 changed files with 583 additions and 49 deletions.
19 changes: 14 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,27 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.21.0
golang.org/x/crypto v0.24.0
golang.org/x/oauth2 v0.17.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)

require (
github.com/bits-and-blooms/bitset v1.10.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/gobuffalo/nulls v0.4.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/jackc/pgx/v4 v4.18.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.5 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/mod v0.17.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
)
Expand Down Expand Up @@ -68,6 +76,7 @@ require (
github.com/go-chi/chi/v5 v5.0.12
github.com/gobuffalo/pop/v6 v6.1.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/lestrrat-go/jwx/v2 v2.1.0
github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20240303152453-e0e82adf1721
github.com/supabase/hibp v0.0.0-20231124125943-d225752ae869
github.com/supabase/mailme v0.2.0
Expand Down Expand Up @@ -134,9 +143,9 @@ require (
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/grpc v1.63.2 // indirect
Expand Down
37 changes: 28 additions & 9 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1n
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP7g5Q2s=
github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas=
github.com/didip/tollbooth/v5 v5.1.1 h1:QpKFg56jsbNuQ6FFj++Z1gn2fbBsvAc1ZPLUaDOYW5k=
Expand Down Expand Up @@ -103,6 +105,8 @@ github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh
github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
Expand Down Expand Up @@ -208,6 +212,18 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.1.0 h1:0zs7Ya6+39qoit7gwAf+cYm1zzgS3fceIdo7RmQ5lkw=
github.com/lestrrat-go/jwx/v2 v2.1.0/go.mod h1:Xpw9QIaUGiIUD1Wx0NcY1sIHwFf8lDuZn/cmxtXYRys=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
Expand Down Expand Up @@ -279,6 +295,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 h1:eajwn6K3weW5cd1ZXLu2sJ4pvwlBiCWY4uDejOr73gM=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
Expand Down Expand Up @@ -388,8 +406,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand All @@ -398,8 +416,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20161007143504-f4b625ec9b21/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand All @@ -422,8 +440,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand All @@ -448,8 +466,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand All @@ -466,8 +484,9 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.0.0-20160926182426-711ca1cb8763/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down
1 change: 1 addition & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
}

r.Get("/health", api.HealthCheck)
r.Get("/.well-known/jwks.json", api.Jwks)

r.Route("/callback", func(r *router) {
r.Use(api.isValidExternalHost)
Expand Down
16 changes: 14 additions & 2 deletions internal/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/gofrs/uuid"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
)
Expand Down Expand Up @@ -75,9 +76,20 @@ func (a *API) parseJWTClaims(bearer string, r *http.Request) (context.Context, e
ctx := r.Context()
config := a.config

p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
p := jwt.NewParser(jwt.WithValidMethods(config.JWT.ValidMethods))
token, err := p.ParseWithClaims(bearer, &AccessTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(config.JWT.Secret), nil
if kid, ok := token.Header["kid"]; ok {
if kidStr, ok := kid.(string); ok {
return conf.FindPublicKeyByKid(kidStr, &config.JWT)
}
}
if alg, ok := token.Header["alg"]; ok {
if alg == jwt.SigningMethodHS256.Name {
// preserve backward compatibility for cases where the kid is not set
return []byte(config.JWT.Secret), nil
}
}
return nil, fmt.Errorf("missing kid")
})
if err != nil {
return nil, forbiddenError(ErrorCodeBadJWT, "invalid JWT: unable to parse or verify signature, %v", err).WithInternalError(err)
Expand Down
110 changes: 99 additions & 11 deletions internal/api/auth_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package api

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/gofrs/uuid"
jwt "github.com/golang-jwt/jwt/v5"
jwk "github.com/lestrrat-go/jwx/v2/jwk"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
Expand Down Expand Up @@ -55,20 +57,106 @@ func (ts *AuthTestSuite) TestExtractBearerToken() {
}

func (ts *AuthTestSuite) TestParseJWTClaims() {
userClaims := &AccessTokenClaims{
Role: "authenticated",
cases := []struct {
desc string
key map[string]interface{}
}{
{
desc: "HMAC key",
key: map[string]interface{}{
"kty": "oct",
"k": "S1LgKUjeqXDEolv9WPtjUpADVMHU_KYu8uRDrM-pDGg",
"kid": "ac50c3cc-9cf7-4fd6-a11f-fe066fd39118",
"key_ops": []string{"sign", "verify"},
"alg": "HS256",
},
},
{
desc: "RSA key",
key: map[string]interface{}{
"kty": "RSA",
"n": "2g0B_hMIx5ZPuTUtLRpRr0k314XniYm3AUFgR5FmTZIjrn7vLwsWij-2egGZeHa-y9ypAgB9Q-lQ3AlT7RMPiCIyLQI6TTC8k10NEnj8c0QZwENx1Qr8aBbuZbOP9Cz30EMWZSbzMbz7r8-3rp5wBRBtIPnLlbfZh_p0iBaJfB77-r_mvhOIFM4xS7ef3nkE96dnvbEN5a-HfjzDJIAt-LniUvzMWW2gQcmHiM4oeijE3PHesapLMt2JpsMhSRo8L7tysags9VMoyZ1GnpCdjtRwb_KpY9QTjV6lL8G5nsKFH7bhABYcpjDOvqkfT5nPXj6C7oCo6MPRirPWUTbq2w",
"e": "AQAB",
"d": "OOTj_DNjOxCRRLYHT5lqbt4f3_BkdZKlWYKBaKsbkmnrPYCJUDEIdJIjPrpkHPZ-2hp9TrRp-upJ2t_kMhujFdY2WWAXbkSlL5475vICjODcBzqR3RC8wzwYgBjWGtQQ5RpcIZCELBovYbRFLR7SA8BBeTU0VaBe9gf3l_qpbOT9QIl268uFdWndTjpehGLQRmAtR1snhvTha0b9nsBZsM_K-EfnoF7Q_lPsjwWDvIGpFXao8Ifaa_sFtQkHjHVBMW2Qgx3ZSrEva_brk7w0MNSYI7Nsmr56xFOpFRwZy0v8ZtgQZ4hXmUInRHIoQ2APeds9YmemojvJKVflt9pLIQ",
"p": "-o2hdQ5Z35cIS5APTVULj_BMoPJpgkuX-PSYC1SeBeff9K04kG5zrFMWJy_-27-ys4q754lpNwJdX2CjN1nb6qyn-uKP8B2oLayKs9ebkiOqvm3S2Xblvi_F8x6sOLba3lTYHK8G7U9aMB9U0mhAzzMFdw15XXusVFDvk-zxL28",
"q": "3sp-7HzZE_elKRmebjivcDhkXO2GrcN3EIqYbbXssHZFXJwVE9oc2CErGWa7QetOCr9C--ZuTmX0X3L--CoYr-hMB0dN8lcAhapr3aau-4i7vE3DWSUdcFSyi0BBDg8pWQWbxNyTXBuWeh1cnRBsLjCxAOVTF0y3_BnVR7mbBVU",
"dp": "DuYHGMfOrk3zz1J0pnuNIXT_iX6AqZ_HHKWmuN3CO8Wq-oimWWhH9pJGOfRPqk9-19BDFiSEniHE3ZwIeI0eV5kGsBNyzatlybl90e3bMVhvmb08EXRRevqqQaesQ_8Tiq7u3t3Fgqz6RuxGBfDvEaMOCyNA-T8WYzkg1eH8AX8",
"dq": "opOCK3CvuDJvA57-TdBvtaRxGJ78OLD6oceBlA29useTthDwEJyJj-4kVVTyMRhUyuLnLoro06zytvRjuxR9D2CkmmseJkn2x5OlQwnvhv4wgSj99H9xDBfCcntg_bFyqtO859tObVh0ZogmnTbuuoYtpEm0aLxDRmRTjxOSXEE",
"qi": "8skVE7BDASHXytKSWYbkxD0B3WpXic2rtnLgiMgasdSxul8XwcB-vjVSZprVrxkcmm6ZhszoxOlq8yylBmMvAnG_gEzTls_xapeuEXGYiGaTcpkCt1r-tBKcQkka2SayaWwAljsX4xSw-zKP2koUkEET_tIcbBOW1R4OWfRGqOI",
"kid": "0d24b26c-b3ec-4c02-acfd-d5a54d50b3a4",
"key_ops": []string{"sign", "verify"},
"alg": "RS256",
},
},
{
desc: "EC key",
key: map[string]interface{}{
"kty": "EC",
"x": "5wsOh-DrNPpm9KkuydtgGs_cv3oNvtR9OdXywt12aS4",
"y": "0y01ZbuH_VQjMEd8fcYaLdiv25EVJ5GOrb79dJJsqrM",
"crv": "P-256",
"d": "EDP4ReMMpAUcf82EF3JYvkm8C5hVAh258Rj6f3HTx7c",
"kid": "10646a77-f470-44a8-8400-2f988d9c9c1a",
"key_ops": []string{"sign", "verify"},
"alg": "ES256",
},
},
{
desc: "Ed25519 key",
key: map[string]interface{}{
"crv": "Ed25519",
"d": "jVpCLvOxatVkKe1MW9nFRn6Q8VVZPq5yziKU_Z0Yu-c",
"x": "YDkGdufJBQEPO6ylvd9IKfZlzvm9tOG5VCDpkJSSkiA",
"kty": "OKP",
"kid": "ec5e7a96-ea66-456c-826c-d8d6cb928c0f",
"key_ops": []string{"sign", "verify"},
"alg": "EdDSA",
},
},
}
userJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaims).SignedString([]byte(ts.Config.JWT.Secret))
require.NoError(ts.T(), err)

req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Set("Authorization", "Bearer "+userJwt)
ctx, err := ts.API.parseJWTClaims(userJwt, req)
require.NoError(ts.T(), err)
for _, c := range cases {
ts.Run(c.desc, func() {
bytes, err := json.Marshal(c.key)
require.NoError(ts.T(), err)
privKey, err := jwk.ParseKey(bytes)
require.NoError(ts.T(), err)
pubKey, err := privKey.PublicKey()
require.NoError(ts.T(), err)
ts.Config.JWT.Keys = conf.JwtKeysDecoder{privKey.KeyID(): conf.JwkInfo{
PublicKey: pubKey,
PrivateKey: privKey,
}}
ts.Config.JWT.ValidMethods = nil
require.NoError(ts.T(), ts.Config.ApplyDefaults())

userClaims := &AccessTokenClaims{
Role: "authenticated",
}

// get signing key and method from config
jwk, err := conf.GetSigningJwk(&ts.Config.JWT)
require.NoError(ts.T(), err)
signingMethod := conf.GetSigningAlg(jwk)
signingKey, err := conf.GetSigningKey(jwk)
require.NoError(ts.T(), err)

// check if token is stored in context
token := getToken(ctx)
require.Equal(ts.T(), userJwt, token.Raw)
userJwtToken := jwt.NewWithClaims(signingMethod, userClaims)
require.NoError(ts.T(), err)
userJwtToken.Header["kid"] = jwk.KeyID()
userJwt, err := userJwtToken.SignedString(signingKey)
require.NoError(ts.T(), err)

req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Set("Authorization", "Bearer "+userJwt)
ctx, err := ts.API.parseJWTClaims(userJwt, req)
require.NoError(ts.T(), err)

// check if token is stored in context
token := getToken(ctx)
require.Equal(ts.T(), userJwt, token.Raw)
})
}
}

func (ts *AuthTestSuite) TestMaybeLoadUserOrSession() {
Expand Down
30 changes: 30 additions & 0 deletions internal/api/jwks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package api

import (
"net/http"

"github.com/lestrrat-go/jwx/v2/jwa"
jwk "github.com/lestrrat-go/jwx/v2/jwk"
)

type JwksResponse struct {
Keys []jwk.Key `json:"keys"`
}

func (a *API) Jwks(w http.ResponseWriter, r *http.Request) error {
config := a.config
resp := JwksResponse{
Keys: []jwk.Key{},
}

for _, key := range config.JWT.Keys {
// don't expose hmac jwk in endpoint
if key.PublicKey == nil || key.PublicKey.KeyType() == jwa.OctetSeq {
continue
}
resp.Keys = append(resp.Keys, key.PublicKey)
}

w.Header().Set("Cache-Control", "public, max-age=600")
return sendJSON(w, http.StatusOK, resp)
}
Loading

0 comments on commit c7a2be3

Please sign in to comment.