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

feat: add asymmetric jwt support #1674

Merged
merged 21 commits into from
Jul 26, 2024
Merged

Conversation

kangmingtay
Copy link
Member

@kangmingtay kangmingtay commented Jul 23, 2024

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)
    • 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):
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:

{
  "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

@kangmingtay kangmingtay requested a review from a team as a code owner July 23, 2024 06:21
@kangmingtay kangmingtay marked this pull request as draft July 23, 2024 06:21
@kangmingtay kangmingtay requested review from hf and J0 July 23, 2024 06:22
@kangmingtay kangmingtay force-pushed the km/feat-asymmetric-jwt-support branch from f828ede to e2751e1 Compare July 23, 2024 06:33
@kangmingtay kangmingtay force-pushed the km/feat-asymmetric-jwt-support branch from e2751e1 to ef47fe0 Compare July 23, 2024 06:36
internal/conf/jwk.go Outdated Show resolved Hide resolved
@coveralls
Copy link

coveralls commented Jul 23, 2024

Pull Request Test Coverage Report for Build 10112912238

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 130 of 182 (71.43%) changed or added relevant lines in 6 files are covered.
  • 15 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.1%) to 58.131%

Changes Missing Coverage Covered Lines Changed/Added Lines %
internal/api/auth.go 10 11 90.91%
internal/api/jwks.go 12 13 92.31%
internal/api/token.go 13 18 72.22%
internal/conf/configuration.go 29 41 70.73%
internal/conf/jwk.go 65 98 66.33%
Files with Coverage Reduction New Missed Lines %
internal/api/token.go 1 72.83%
internal/api/middleware.go 2 80.07%
internal/api/user.go 12 65.41%
Totals Coverage Status
Change from base Build 10035433302: 0.1%
Covered Lines: 8937
Relevant Lines: 15374

💛 - Coveralls

Copy link
Contributor

@hf hf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For GOTURE_JWT_KEYS:

  • Why is the in_use field there? If it's configured, it's in use.
  • I'd prefer if the key is encoded as JWK instead of DER, as it aids readability. You also don't need to specify type for it at all in that case. Example:
GOTRUE_JWT_KEYS = [
{
  // HMAC key
  kid: '<ID>',
  alg: 'HS256',
  kty: 'oct',
  k: '<Base64URL of the HMAC symmetric key>',
},
{
  kty: 'EC',
  x: 'JTeOUQoO2zicwixLzfIHawyvM-wQsMEI1EAtI4NVTdI',
  y: 'YgxS33NnJfFIjLeLoWOsFCyAghGOWBzgWbPrENNiMyg',
  crv: 'secp256k1',
  d: 'jaGPBvQHFIaPlgRMICknHNeZxVzx2gyEFlM5T_Le6SM' // this is the "private key" for x and y which are the public key
},
// etc
]

Then you can easily parse that config string into an array of json.RawMessage that you can directly pass to jwk.ParseKey.

Finally for exposing this in the /.well-known/jwks.json endpoint you can just iterate over those parsed JWK objects and call the Public() method to convert the private keys into public ones and then just marshal those objects to JSON.

In fact, the whole in_use complication can just be omitted completely and you can rely on whether the key in the GOTRUE_JWT_KEYS config is private or public. If it's a private key, then it can be used for signing, otherwise it can only be used for verification.

go.mod Show resolved Hide resolved
internal/conf/configuration.go Outdated Show resolved Hide resolved
internal/conf/jwk.go Outdated Show resolved Hide resolved
@kangmingtay kangmingtay force-pushed the km/feat-asymmetric-jwt-support branch from d771cc4 to a2e1cb1 Compare July 23, 2024 21:09
@kangmingtay kangmingtay force-pushed the km/feat-asymmetric-jwt-support branch from fb88f32 to 6d6092e Compare July 23, 2024 23:18
@kangmingtay kangmingtay force-pushed the km/feat-asymmetric-jwt-support branch from 38881c7 to ad4d32c Compare July 24, 2024 00:17
@kangmingtay kangmingtay marked this pull request as ready for review July 24, 2024 01:19
@kangmingtay kangmingtay requested a review from hf July 24, 2024 01:44
@kangmingtay kangmingtay force-pushed the km/feat-asymmetric-jwt-support branch from d67cf27 to 2b6fd93 Compare July 24, 2024 02:24
internal/api/api.go Show resolved Hide resolved
internal/api/auth.go Outdated Show resolved Hide resolved
internal/api/jwks_test.go Outdated Show resolved Hide resolved
internal/api/jwks.go Show resolved Hide resolved
internal/conf/jwk.go Outdated Show resolved Hide resolved
internal/conf/jwk.go Outdated Show resolved Hide resolved
internal/conf/jwk.go Outdated Show resolved Hide resolved
internal/conf/jwk.go Outdated Show resolved Hide resolved
internal/conf/jwk.go Outdated Show resolved Hide resolved
@kangmingtay kangmingtay force-pushed the km/feat-asymmetric-jwt-support branch from 7654407 to 031796b Compare July 24, 2024 22:59
@kangmingtay kangmingtay force-pushed the km/feat-asymmetric-jwt-support branch from 8b62893 to a36c511 Compare July 25, 2024 02:22
Copy link
Contributor

@J0 J0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work

internal/conf/jwk.go Outdated Show resolved Hide resolved
internal/conf/jwk.go Outdated Show resolved Hide resolved
Copy link
Contributor

@hf hf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice.

@kangmingtay kangmingtay merged commit c7a2be3 into master Jul 26, 2024
2 checks passed
@kangmingtay kangmingtay deleted the km/feat-asymmetric-jwt-support branch July 26, 2024 14:46
kangmingtay pushed a commit that referenced this pull request Jul 26, 2024
🤖 I have created a release *beep* *boop*
---


##
[2.157.0](v2.156.0...v2.157.0)
(2024-07-26)


### Features

* add asymmetric jwt support
([#1674](#1674))
([c7a2be3](c7a2be3))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
uxodb pushed a commit to uxodb/auth that referenced this pull request Nov 13, 2024
## 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
uxodb pushed a commit to uxodb/auth that referenced this pull request Nov 13, 2024
🤖 I have created a release *beep* *boop*
---


##
[2.157.0](supabase/auth@v2.156.0...v2.157.0)
(2024-07-26)


### Features

* add asymmetric jwt support
([supabase#1674](supabase#1674))
([c7a2be3](supabase@c7a2be3))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants