diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a4d6c0f32..481cb36b7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -28,7 +28,7 @@ The following is a set of guidelines that we ask you to follow when you contribu # Please Be Nice -[Main source; if wording differ, the main source supersedes this copy](https://github.com/lestrrat-go/contributions/blob/main/Contributions.md) +[Main source; if wordings differ, the main source supersedes this copy](https://github.com/lestrrat-go/contributions/blob/main/Contributions.md) Please be nice when you contact us. @@ -46,6 +46,18 @@ But we do not owe you anything. Please do not order us to work for you. We are not your support staff, and we are not here to do your research. We are willing to help, but only as long as you are being nice to us. +# Please Read The Examples First + +[Main source; if wordings differ, the main source supersedes this copy](https://github.com/lestrrat-go/contributions/blob/main/Contributions.md) + +On most of the projects that we provide, we have example test code available, +most likely in the [`examples/`](../examples) directory. Before asking questions or filing issues, please make sure to take a look at the examples. + +Specifically for Go projects, please first look for files with names `*_example_test.go`, which contain the runnable example code. + +If the examples do not solve your problems, feel free to proceed with your report. If there are missing examples or inaccuracies, please do not hesitate to contact us. + + # Please Use Correct Medium (GitHub Issues / Discussions) [Main source; this is a specialized version copied from the main source](https://github.com/lestrrat-go/contributions/blob/main/Contributions.md) @@ -58,7 +70,7 @@ questions/discussions should be posted to [GitHub Discussions](https://github.co # Please Include (Pseudo)code for Any Technical Issues -[Main source; if wording differ, the main source supersedes this copy](https://github.com/lestrrat-go/contributions/blob/main/Contributions.md) +[Main source; if wordings differ, the main source supersedes this copy](https://github.com/lestrrat-go/contributions/blob/main/Contributions.md) Your report should contain clear, concise description of the issue that you are facing. However, at the same time please always include (pseudo)code in report. diff --git a/.github/workflows/assign-issue.yml b/.github/workflows/assign-issue.yml index 31b8d3af9..c0f1a5534 100644 --- a/.github/workflows/assign-issue.yml +++ b/.github/workflows/assign-issue.yml @@ -8,6 +8,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Auto-assign issue' - uses: pozil/auto-assign-issue@v1 + uses: pozil/auto-assign-issue@v2 with: assignees: lestrrat diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ec9e9896e..02a30407d 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.19', '1.18', '1.17' ] + go: [ '1.21', '1.20' ] name: "Test [ Go ${{ matrix.go }} / JSON Backend ${{ matrix.json_backend }} ]" steps: - name: Checkout repository diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a38daa1b8..c4034b598 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go_tags: [ 'stdlib', 'goccy', 'es256k', 'asmbase64', 'alltags'] - go: [ '1.20', '1.19', '1.18'] + go_tags: [ 'stdlib', 'goccy', 'es256k', 'secp256k1-pem', 'asmbase64', 'alltags'] + go: [ '1.21', '1.20', '1.19' ] name: "Test [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]" steps: - name: Checkout repository diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index eb17563b5..12b44f305 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -10,6 +10,8 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install tparse + run: go install github.com/mfridman/tparse@v0.12.2 - run: | make tidy - run: | @@ -23,7 +25,7 @@ jobs: git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' git add . git commit -m "Run tidy / bazel+gazelle" - git push origin ${{ github.ref_name }} + git push gh pr review --approve "$PR_URL" gh pr merge --auto --merge "$PR_URL" env: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 11ba5ba52..702a4c5d7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: with: go-version: 1.19 check-latest: true - - uses: golangci/golangci-lint-action@v4 + - uses: golangci/golangci-lint-action@v6 with: version: v1.54.2 - name: Run go vet diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 4bd6130d5..624ed102d 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: go_tags: [ 'stdlib', 'goccy', 'es256k', 'alltags' ] - go: [ '1.20', '1.19', '1.18' ] + go: [ '1.21', '1.20', '1.19' ] name: "Smoke [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]" steps: - name: Checkout repository diff --git a/Changes b/Changes index 33a5cb702..0c7be2dea 100644 --- a/Changes +++ b/Changes @@ -4,6 +4,85 @@ Changes v2 has many incompatibilities with v1. To see the full list of differences between v1 and v2, please read the Changes-v2.md file (https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes-v2.md) +v2.1.0 18 Jun 2024 +[New Features] + * [jwt] Added `jwt.ParseCookie()` function + * [jwt] `jwt.ParseRequest()` can now accept a new option, jwt.WithCookieKey() to + specify a cookie name to extract the token from. + * [jwt] `jwt.ParseRequest()` and `jwt.ParseCookie()` can accept the `jwt.WithCookie()` option, + which will, upon successful token parsing, make the functions assign the *http.Cookie + used to parse the token. This allows users to further inspect the cookie where the + token came from, should the need arise. + * [jwt] (BREAKING CHANGE) `jwt.ParseRequest()` no longer automatically looks for "Authorization" header when + only `jwt.WithFormKey()` is used. This behavior is the same for `jwt.WithCookieKey()` and + any similar options that may be implemented in the future. + + # previously + jwt.ParseRequest(req) // looks under Authorization + jwt.ParseReuqest(req, jwt.WithFormKey("foo")) // looks under foo AND Authorization + jwt.ParseReuqest(req, jwt.WithHeaderKey("Authorization"), jwt.WithFormKey("foo")) // looks under foo AND Authorization + + # since this release + jwt.ParseRequest(req) // same as before + jwt.ParseRequest(req, jwt.WithFormKey("foo")) // looks under foo + jwt.ParseReuqest(req, jwt.WithHeaderKey("Authorization"), jwt.WithFormKey("foo")) // looks under foo AND Authorization + + * [jwt] Add `jwt.WithResetValidators()` option to `jwt.Validate()`. This option + will allow you to tell `jwt.Validate()` to NOT automatically check the + default validators (`iat`, `exp`, and `nbf`), so that you can completely customize + the validation with the validators you specify using `jwt.WithValidator()`. + + This sort of behavior is useful for special cases such as + https://openid.net/specs/openid-connect-rpinitiated-1_0.html. However, you SHOULD NOT + use this option unless you know exactly what you are doing, as this will pose + significant security issues when used incorrectly. + + * [jwk] Provide a _stop-gap_ measure to work with PEM format ASN.1 DER encoded secp256k1 keys. + + In order to enable this feature, you must compile jwx with TWO build tags: + `jwx_es256k` to enable ES256K/secp256k1, and `jwx_secp256k1_pem` to enable PEM handling. + Not one, but BOTH tags need to be present. + + With this change, by suppliying the `WithPEM(true)` option, `jwk.Parse()` is now + able to read sep256k1 keys. Also, `jwk.Pem()` should be able to handle `jwk.Key` objects + that represent a secp256k1 key. + + Please do note that the implementation of this feature is dodgy at best. Currently + Go's crypto/x509 does not allow handling additional EC curves, and thus in order to + accomodate secp256k1 keys in PEM/ASN.1 DER format we need to "patch" the stdlib. + We do this by copy-and-pasting relevant parts of go 1.22.2's crypto/x509 code and + adding the minimum required code to make secp256k1 keys work. + + Because of the above, there are several important caveats for this feature: + + 1. This feature is provided solely as a stop-gap measure until such time Go's stdlib + provides a way to handle non-standard EC curves, or another external module + is able to solve this issue. + + 2. This feature should be considered unstable and not guaranteed by semantic versioning + backward compatibility. At any given point we may drop or modify this feature. It may be + because we can no longer maintain the code, or perhaps a security issue is found in the + version of the code that we ship with, etc. + + 3. Please always remember that we are now bundling a static set of code for handling + x509 formats. You are taking a possible security risk by code that could be outdated. + Please always do your own research, and if possible, please notify us if the bundled + code needs to be updated. Unless you know what you are doing, it is not recommended + that you enable this feature. + + 4. Please note that because we imported the code from go 1.22's src/crypto/x509, + it has some go1.20-isms in its code. Therefore you will not be able to use the + `jwx_secp256k1_pem` tag to enable secp256k1 key PEM handling against codebases + that are built using go 1.19 and below (the build will succeed, but the feature + will be unavailable). + + 5. We have no plans to include more curves this way. One is already one too many. + + * [jwe] Fixed a bug when using encryption algorithms involving PBES2 along with the + jwx.WithUseNumber() global option. Enabling this option would turn all values + stored in the JSON content to be of type `json.Number`, but we did not account for + it when checking for the value of `p2c` header, resulting in a conversion error. + v2.0.21 07 Mar 2024 [Security] * [jwe] Added `jwe.Settings(jwe.WithMaxDecompressBufferSize(int64))` to specify the diff --git a/Makefile b/Makefile index 40add854f..672c007b2 100644 --- a/Makefile +++ b/Makefile @@ -25,11 +25,14 @@ test-goccy: test-es256k: $(MAKE) test-cmd TESTOPTS="-tags jwx_es256k" +test-secp256k1-pem: + $(MAKE) test-cmd TESTOPTS="-tags jwx_es256k,jwx_secp256k1_pem" + test-asmbase64: $(MAKE) test-cmd TESTOPTS="-tags jwx_asmbase64" test-alltags: - $(MAKE) test-cmd TESTOPTS="-tags jwx_asmbase64,jwx_goccy,jwx_es256k" + $(MAKE) test-cmd TESTOPTS="-tags jwx_asmbase64,jwx_goccy,jwx_es256k,jwx_secp256k1_pem" cover-cmd: env MODE=cover ./tools/test.sh @@ -46,11 +49,14 @@ cover-goccy: cover-es256k: $(MAKE) cover-cmd TESTOPTS="-tags jwx_es256k" +cover-secp256k1-pem: + $(MAKE) cover-cmd TESTOPTS="-tags jwx_es256k,jwx_secp256k1" + cover-asmbase64: $(MAKE) cover-cmd TESTOPTS="-tags jwx_asmbase64" cover-alltags: - $(MAKE) cover-cmd TESTOPTS="-tags jwx_asmbase64,jwx_goccy,jwx_es256k" + $(MAKE) cover-cmd TESTOPTS="-tags jwx_asmbase64,jwx_goccy,jwx_es256k,jwx_secp256k1_pem" smoke-cmd: env MODE=short ./tools/test.sh @@ -67,8 +73,11 @@ smoke-goccy: smoke-es256k: $(MAKE) smoke-cmd TESTOPTS="-tags jwx_es256k" +smoke-secp256k1-pem: + $(MAKE) smoke-cmd TESTOPTS="-tags jwx_es256k,jwx_secp256k1_pem" + smoke-alltags: - $(MAKE) smoke-cmd TESTOPTS="-tags jwx_goccy,jwx_es256k" + $(MAKE) smoke-cmd TESTOPTS="-tags jwx_goccy,jwx_es256k,jwx_secp256k1_pem" viewcover: go tool cover -html=coverage.out diff --git a/README.md b/README.md index 2bfb71fbd..448e32b94 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# github.com/lestrrat-go/jwx/v2 ![](https://github.com/lestrrat-go/jwx/workflows/CI/badge.svg?branch=v2) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/jwx/v2.svg)](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2) [![codecov.io](https://codecov.io/github/lestrrat-go/jwx/coverage.svg?branch=v2)](https://codecov.io/github/lestrrat-go/jwx?branch=v2) +# github.com/lestrrat-go/jwx/v2 ![](https://github.com/lestrrat-go/jwx/workflows/CI/badge.svg?branch=develop/v2) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/jwx/v2.svg)](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2) [![codecov.io](https://codecov.io/github/lestrrat-go/jwx/coverage.svg?branch=v2)](https://codecov.io/github/lestrrat-go/jwx?branch=v2) Go module implementing various JWx (JWA/JWE/JWK/JWS/JWT, otherwise known as JOSE) technologies. @@ -15,7 +15,7 @@ If you are using this module in your product or your company, please add your p * Opinionated, but very uniform API. Everything is symmetric, and follows a standard convention * jws.Parse/Verify/Sign * jwe.Parse/Encrypt/Decrypt - * Arguments are organized as explicit required paramters and optional WithXXXX() style options. + * Arguments are organized as explicit required parameters and optional WithXXXX() style options. * Extra utilities * `jwk.Cache` to always keep a JWKS up-to-date * [bazel](https://bazel.build)-ready @@ -226,7 +226,7 @@ For either bug reports or feature requests, failing tests are even better. ## Pull Requests -Please make sure to include tests that excercise the changes you made. +Please make sure to include tests that exercise the changes you made. If you are editing auto-generated files (those files with the `_gen.go` suffix, please make sure that you do the following: diff --git a/bench/performance/go.mod b/bench/performance/go.mod index d4696adc7..3da9a7890 100644 --- a/bench/performance/go.mod +++ b/bench/performance/go.mod @@ -2,4 +2,4 @@ module github.com/lestrrat-go/jwx/v2/bench/performance go 1.16 -require github.com/lestrrat-go/jwx/v2 v2.0.19 +require github.com/lestrrat-go/jwx/v2 v2.0.21 diff --git a/bench/performance/go.sum b/bench/performance/go.sum index f908327d7..b28325957 100644 --- a/bench/performance/go.sum +++ b/bench/performance/go.sum @@ -10,12 +10,12 @@ github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N 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.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= -github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +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.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= -github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= +github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0= +github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 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= @@ -26,16 +26,19 @@ github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +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/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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -43,6 +46,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -54,13 +58,15 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/cmd/jwx/go.mod b/cmd/jwx/go.mod index 480939214..51d4c9594 100644 --- a/cmd/jwx/go.mod +++ b/cmd/jwx/go.mod @@ -3,9 +3,9 @@ module github.com/lestrrat-go/jwx/v2/cmd/jwx go 1.17 require ( - github.com/lestrrat-go/jwx/v2 v2.0.19 + github.com/lestrrat-go/jwx/v2 v2.0.21 github.com/urfave/cli/v2 v2.26.0 - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.21.0 ) require ( @@ -14,11 +14,11 @@ require ( github.com/goccy/go-json v0.10.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.4 // 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/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect ) diff --git a/cmd/jwx/go.sum b/cmd/jwx/go.sum index 1420bf90d..6c802ef31 100644 --- a/cmd/jwx/go.sum +++ b/cmd/jwx/go.sum @@ -14,12 +14,12 @@ github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N 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.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= -github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +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.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= -github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= +github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0= +github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 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= @@ -32,11 +32,13 @@ github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI= github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= @@ -45,8 +47,9 @@ github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsr github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +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/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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -54,6 +57,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -65,13 +69,15 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/deps.bzl b/deps.bzl index df224b0b7..5b6754095 100644 --- a/deps.bzl +++ b/deps.bzl @@ -19,15 +19,15 @@ def go_dependencies(): name = "com_github_decred_dcrd_dcrec_secp256k1_v4", build_file_proto_mode = "disable_global", importpath = "github.com/decred/dcrd/dcrec/secp256k1/v4", - sum = "h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=", - version = "v4.2.0", + sum = "h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=", + version = "v4.3.0", ) go_repository( name = "com_github_goccy_go_json", build_file_proto_mode = "disable_global", importpath = "github.com/goccy/go-json", - sum = "h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=", - version = "v0.10.2", + sum = "h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=", + version = "v0.10.3", ) go_repository( name = "com_github_lestrrat_go_blackmagic", @@ -115,8 +115,8 @@ def go_dependencies(): name = "org_golang_x_crypto", build_file_proto_mode = "disable_global", importpath = "golang.org/x/crypto", - sum = "h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=", - version = "v0.21.0", + sum = "h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=", + version = "v0.24.0", ) go_repository( @@ -131,21 +131,21 @@ def go_dependencies(): name = "org_golang_x_sys", build_file_proto_mode = "disable_global", importpath = "golang.org/x/sys", - sum = "h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=", - version = "v0.18.0", + sum = "h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=", + version = "v0.21.0", ) go_repository( name = "org_golang_x_term", build_file_proto_mode = "disable_global", importpath = "golang.org/x/term", - sum = "h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=", - version = "v0.18.0", + sum = "h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=", + version = "v0.21.0", ) go_repository( name = "org_golang_x_text", build_file_proto_mode = "disable_global", importpath = "golang.org/x/text", - sum = "h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=", - version = "v0.14.0", + sum = "h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=", + version = "v0.16.0", ) diff --git a/docs/01-jwt.md b/docs/01-jwt.md index ef03ea0cf..d9de411be 100644 --- a/docs/01-jwt.md +++ b/docs/01-jwt.md @@ -94,9 +94,8 @@ func ExampleJWT_Parse() { source: [examples/jwt_parse_example_test.go](https://github.com/lestrrat-go/jwx/blob/v2/examples/jwt_parse_example_test.go) -Note that the above form does NOT perform any signature verification, or validation of the JWT token itself. -This just reads the contents of src, and maps it into the token, period. -In order to perform verification/validation, please see the methods described elsewhere in this document, and pass the appropriate option(s). +Note that the above form performs only signature verification andno validation of the JWT token itself. +In order to perform validation, please use `Validate()`. ## Parse a JWT from file @@ -151,25 +150,26 @@ import ( "fmt" "net/http" "net/url" - "strings" "github.com/lestrrat-go/jwx/v2/jwt" ) func ExampleJWT_ParseRequest_Authorization() { - values := url.Values{ - `access_token`: []string{exampleJWTSignedHMAC}, - } - - req, err := http.NewRequest(http.MethodGet, `https://github.com/lestrrat-go/jwx`, strings.NewReader(values.Encode())) + req, err := http.NewRequest(http.MethodGet, `https://github.com/lestrrat-go/jwx`, nil) if err != nil { fmt.Printf("failed to create request: %s\n", err) return } + req.Form = url.Values{} + req.Form.Add("access_token", exampleJWTSignedHMAC) req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, exampleJWTSignedECDSA)) req.Header.Set(`X-JWT-Token`, exampleJWTSignedRSA) + req.AddCookie(&http.Cookie{Name: "accessToken", Value: exampleJWTSignedHMAC}) + + var dst *http.Cookie + testcases := []struct { options []jwt.ParseOption }{ @@ -187,17 +187,34 @@ func ExampleJWT_ParseRequest_Authorization() { { options: []jwt.ParseOption{jwt.WithFormKey(`access_token`)}, }, + // Looks under "accessToken" cookie, and assigns the http.Cookie object + // where the token came from to the variable `dst` + { + options: []jwt.ParseOption{jwt.WithCookieKey(`accessToken`), jwt.WithCookie(&dst)}, + }, } for _, tc := range testcases { options := append(tc.options, []jwt.ParseOption{jwt.WithVerify(false), jwt.WithValidate(false)}...) tok, err := jwt.ParseRequest(req, options...) if err != nil { - fmt.Printf("jwt.ParseRequest with options %#v failed: %s\n", tc.options, err) + fmt.Print("jwt.ParseRequest with options [") + for i, option := range tc.options { + if i > 0 { + fmt.Print(", ") + } + fmt.Printf("%s", option) + } + fmt.Printf("]: %s\n", err) return } _ = tok } + + if dst == nil { + fmt.Printf("failed to assign cookie to dst\n") + return + } // OUTPUT: } ``` @@ -488,6 +505,7 @@ import ( "context" "crypto/rand" "crypto/rsa" + "encoding/base64" "fmt" "github.com/lestrrat-go/jwx/v2/jwa" @@ -495,6 +513,75 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" ) +func ExampleJWT_ParseWithKeyProvider_UseToken() { + // This example shows how one might use the information in the JWT to + // load different keys. + + // Setup + tok, err := jwt.NewBuilder(). + Issuer("me"). + Build() + if err != nil { + fmt.Printf("failed to build token: %s\n", err) + return + } + + symmetricKey := []byte("Abracadabra") + alg := jwa.HS256 + signed, err := jwt.Sign(tok, jwt.WithKey(alg, symmetricKey)) + if err != nil { + fmt.Printf("failed to sign token: %s\n", err) + return + } + + // This next example assumes that you want to minimize the number of + // times you parse the JWT JSON + { + _, b64payload, _, err := jws.SplitCompact(signed) + if err != nil { + fmt.Printf("failed to split jws: %s\n", err) + return + } + + enc := base64.RawStdEncoding + payload := make([]byte, enc.DecodedLen(len(b64payload))) + _, err = enc.Decode(payload, b64payload) + if err != nil { + fmt.Printf("failed to decode base64 payload: %s\n", err) + return + } + + parsed, err := jwt.Parse(payload, jwt.WithVerify(false)) + if err != nil { + fmt.Printf("failed to parse JWT: %s\n", err) + return + } + + _, err = jws.Verify(signed, jws.WithKeyProvider(jws.KeyProviderFunc(func(_ context.Context, sink jws.KeySink, sig *jws.Signature, msg *jws.Message) error { + switch parsed.Issuer() { + case "me": + sink.Key(alg, symmetricKey) + return nil + default: + return fmt.Errorf("unknown issuer %q", parsed.Issuer()) + } + }))) + + if err != nil { + fmt.Printf("%s\n", err) + return + } + + if parsed.Issuer() != tok.Issuer() { + fmt.Printf("issuers do not match\n") + return + } + } + + // OUTPUT: + // +} + func ExampleJWT_ParseWithKeyProvider() { // Pretend that this is a storage somewhere (maybe a database) that maps // a signature algorithm to a key diff --git a/docs/99-faq.md b/docs/99-faq.md index b2e685116..d77fd2c7f 100644 --- a/docs/99-faq.md +++ b/docs/99-faq.md @@ -104,3 +104,50 @@ Now you should be able to just pass the `alg` value to most high-level functions There are some functions that accept `jwa.KeyAlgorithm`, while there are others that expect `jwa.SignatureAlgorithm` or `jwa.KeyEncryptionAlgorithm`. So when do we use which? The guideline is as follows: If it's a high-level function/method that the users regularly use, use `jwa.KeyAlgorithm`. For example, almost everybody who use `jwt` will want to verify the JWS signed payload, so `jwt.Sign()`, and `jwt.Verify()` expect `jwa.KeyAlgorithm`. On the other hand, `jwt.Serializer` uses `jwa.SignatureAlgorithm` and such. This is a low-level utility, and users are not really meant to use it for their most basic needs: therefore they use the specific algorithm type. + +## Why are your options objects, and not callbacks? + +We get this one a lot. The short answer is that 1) the options API is not designed to be extended by users, and 2) callbacks introduce tight coupling between the options and the consumers. + +(1) should be obvious. We just never intended it to be extensible for end-users. That's a design choice, and at the point of writing this, we have no intention to change this. + +(2) is subtler: consider a case where you are setting a few instance variables on an object: + +```go +func MyOption(obj *Object) { + obj.FieldA = ... + obj.FieldB = ... +} +``` + +From this design you can see that we need to make a few assumptions. + +First, the callback must have a specific signature. This is not a deal breaker, but you have to be conscious of the fact that you are tying your option to this object type specifically, and you will not be able to change this. + +Second, you are setting values to an object. The library can either provide exported fields, or it can provide setter methods, but either way, it will need to expose those knobs to the end user. This means that internal details of the object _will_ have to be visible, even if this option is the only logical place that detail is to be used. You could maybe use a state (or a config) variable to avoid assigning to the object itself, and localize the effect of the option for the method: + +```go +func (obj *Object) Method(options ...Options) error { + var cfg MethodConfig + for _, option := range options { + option(obj, cfg) + } + ... +} + +func MyOption(obj *Object, cfg *MethodConfig) { + cfg.FieldA = ... + cfg.FieldB = ... +} +``` + +But this means that you will have to have a state/config object for _each_ method call that takes options, and we have a lot of methods. + +And finally, as you have seen above, with a callback based option object you will have to change its signature for each usecase. It's just a lot of hassle to remember which option uses which signature. + +Based on the reasons above, we decided to **decouple the option data** and the **option handling logic**. Our option objects are simply data containers. They have an identity (`Ident()`), and they have a value (`Value()`). The option objects themselves do not know how they are going to be used. The consumers (the methods) are the ones who know how to deal with the data the options carry. + +Yes, we understand that this way we introduce more boilerplate code in each method's starting section. But our design choice is that this way fulfills our goals better, namely that **the overall structure is simpler**, **we do not expose unnecessary internal data to the end-user**, and because the options are all data, it's far **easier to re-use the same options for multiple methods** (as we do in cases such as `jws.WithKey()`, for example). + + + diff --git a/examples/go.sum b/examples/go.sum index 59cd89b12..6be8ddd05 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -5,10 +5,11 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +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/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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= @@ -37,21 +38,31 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +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/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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -60,27 +71,37 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.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/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/examples/jwt_parse_request_example_test.go b/examples/jwt_parse_request_example_test.go index f3eab1551..475239ba3 100644 --- a/examples/jwt_parse_request_example_test.go +++ b/examples/jwt_parse_request_example_test.go @@ -4,25 +4,26 @@ import ( "fmt" "net/http" "net/url" - "strings" "github.com/lestrrat-go/jwx/v2/jwt" ) func ExampleJWT_ParseRequest_Authorization() { - values := url.Values{ - `access_token`: []string{exampleJWTSignedHMAC}, - } - - req, err := http.NewRequest(http.MethodGet, `https://github.com/lestrrat-go/jwx`, strings.NewReader(values.Encode())) + req, err := http.NewRequest(http.MethodGet, `https://github.com/lestrrat-go/jwx`, nil) if err != nil { fmt.Printf("failed to create request: %s\n", err) return } + req.Form = url.Values{} + req.Form.Add("access_token", exampleJWTSignedHMAC) req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, exampleJWTSignedECDSA)) req.Header.Set(`X-JWT-Token`, exampleJWTSignedRSA) + req.AddCookie(&http.Cookie{Name: "accessToken", Value: exampleJWTSignedHMAC}) + + var dst *http.Cookie + testcases := []struct { options []jwt.ParseOption }{ @@ -40,16 +41,33 @@ func ExampleJWT_ParseRequest_Authorization() { { options: []jwt.ParseOption{jwt.WithFormKey(`access_token`)}, }, + // Looks under "accessToken" cookie, and assigns the http.Cookie object + // where the token came from to the variable `dst` + { + options: []jwt.ParseOption{jwt.WithCookieKey(`accessToken`), jwt.WithCookie(&dst)}, + }, } for _, tc := range testcases { options := append(tc.options, []jwt.ParseOption{jwt.WithVerify(false), jwt.WithValidate(false)}...) tok, err := jwt.ParseRequest(req, options...) if err != nil { - fmt.Printf("jwt.ParseRequest with options %#v failed: %s\n", tc.options, err) + fmt.Print("jwt.ParseRequest with options [") + for i, option := range tc.options { + if i > 0 { + fmt.Print(", ") + } + fmt.Printf("%s", option) + } + fmt.Printf("]: %s\n", err) return } _ = tok } + + if dst == nil { + fmt.Printf("failed to assign cookie to dst\n") + return + } // OUTPUT: } diff --git a/examples/jwt_parse_with_key_provider_example_test.go b/examples/jwt_parse_with_key_provider_example_test.go index ae204a094..09bbb8c01 100644 --- a/examples/jwt_parse_with_key_provider_example_test.go +++ b/examples/jwt_parse_with_key_provider_example_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "crypto/rsa" + "encoding/base64" "fmt" "github.com/lestrrat-go/jwx/v2/jwa" @@ -11,6 +12,75 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" ) +func ExampleJWT_ParseWithKeyProvider_UseToken() { + // This example shows how one might use the information in the JWT to + // load different keys. + + // Setup + tok, err := jwt.NewBuilder(). + Issuer("me"). + Build() + if err != nil { + fmt.Printf("failed to build token: %s\n", err) + return + } + + symmetricKey := []byte("Abracadabra") + alg := jwa.HS256 + signed, err := jwt.Sign(tok, jwt.WithKey(alg, symmetricKey)) + if err != nil { + fmt.Printf("failed to sign token: %s\n", err) + return + } + + // This next example assumes that you want to minimize the number of + // times you parse the JWT JSON + { + _, b64payload, _, err := jws.SplitCompact(signed) + if err != nil { + fmt.Printf("failed to split jws: %s\n", err) + return + } + + enc := base64.RawStdEncoding + payload := make([]byte, enc.DecodedLen(len(b64payload))) + _, err = enc.Decode(payload, b64payload) + if err != nil { + fmt.Printf("failed to decode base64 payload: %s\n", err) + return + } + + parsed, err := jwt.Parse(payload, jwt.WithVerify(false)) + if err != nil { + fmt.Printf("failed to parse JWT: %s\n", err) + return + } + + _, err = jws.Verify(signed, jws.WithKeyProvider(jws.KeyProviderFunc(func(_ context.Context, sink jws.KeySink, sig *jws.Signature, msg *jws.Message) error { + switch parsed.Issuer() { + case "me": + sink.Key(alg, symmetricKey) + return nil + default: + return fmt.Errorf("unknown issuer %q", parsed.Issuer()) + } + }))) + + if err != nil { + fmt.Printf("%s\n", err) + return + } + + if parsed.Issuer() != tok.Issuer() { + fmt.Printf("issuers do not match\n") + return + } + } + + // OUTPUT: + // +} + func ExampleJWT_ParseWithKeyProvider() { // Pretend that this is a storage somewhere (maybe a database) that maps // a signature algorithm to a key diff --git a/go.mod b/go.mod index d68e784d7..f572e38cc 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,21 @@ module github.com/lestrrat-go/jwx/v2 go 1.18 require ( - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 - github.com/goccy/go-json v0.10.2 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 + github.com/goccy/go-json v0.10.3 github.com/lestrrat-go/blackmagic v1.0.2 github.com/lestrrat-go/httprc v1.0.5 github.com/lestrrat-go/iter v1.0.2 github.com/lestrrat-go/option v1.0.1 github.com/segmentio/asm v1.2.0 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.24.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sys v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fe7dc05e3..948ab8262 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,10 @@ 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.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +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/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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= @@ -24,10 +24,10 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -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/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/json/goccy.go b/internal/json/goccy.go index 59682104b..b63c015a2 100644 --- a/internal/json/goccy.go +++ b/internal/json/goccy.go @@ -26,11 +26,9 @@ func Engine() string { func NewDecoder(r io.Reader) *json.Decoder { dec := json.NewDecoder(r) - muGlobalConfig.RLock() - if useNumber { + if UseNumber() { dec.UseNumber() } - muGlobalConfig.RUnlock() return dec } diff --git a/internal/json/json.go b/internal/json/json.go index a4f1026a5..89209721b 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -4,19 +4,24 @@ import ( "bytes" "fmt" "os" - "sync" + "sync/atomic" "github.com/lestrrat-go/jwx/v2/internal/base64" ) -var muGlobalConfig sync.RWMutex -var useNumber bool +var useNumber uint32 // TODO: at some point, change to atomic.Bool + +func UseNumber() bool { + return atomic.LoadUint32(&useNumber) == 1 +} // Sets the global configuration for json decoding func DecoderSettings(inUseNumber bool) { - muGlobalConfig.Lock() - useNumber = inUseNumber - muGlobalConfig.Unlock() + var val uint32 + if inUseNumber { + val = 1 + } + atomic.StoreUint32(&useNumber, val) } // Unmarshal respects the values specified in DecoderSettings, diff --git a/internal/json/stdlib.go b/internal/json/stdlib.go index 62b1a5ff5..759306260 100644 --- a/internal/json/stdlib.go +++ b/internal/json/stdlib.go @@ -25,11 +25,9 @@ func Engine() string { func NewDecoder(r io.Reader) *json.Decoder { dec := json.NewDecoder(r) - muGlobalConfig.RLock() - if useNumber { + if UseNumber() { dec.UseNumber() } - muGlobalConfig.RUnlock() return dec } diff --git a/jwe/internal/aescbc/aescbc.go b/jwe/internal/aescbc/aescbc.go index 749277b9d..057953e77 100644 --- a/jwe/internal/aescbc/aescbc.go +++ b/jwe/internal/aescbc/aescbc.go @@ -42,11 +42,12 @@ func pad(buf []byte, n int) []byte { return buf } + bufsiz := len(buf) + rem mbs := atomic.LoadInt64(&maxBufSize) - if int64(len(buf)+rem) > mbs { + if int64(bufsiz) > mbs { panic(fmt.Errorf("failed to allocate buffer")) } - newbuf := make([]byte, len(buf)+rem) + newbuf := make([]byte, bufsiz) copy(newbuf, buf) for i := len(buf); i < len(newbuf); i++ { @@ -203,7 +204,7 @@ func (c Hmac) Seal(dst, nonce, plaintext, data []byte) []byte { if int64(bufsiz) > mbs { panic(fmt.Errorf("failed to allocate buffer")) } - ciphertext := make([]byte, ctlen+c.Overhead())[:ctlen] + ciphertext := make([]byte, bufsiz)[:ctlen] copy(ciphertext, plaintext) ciphertext = pad(ciphertext, c.blockCipher.BlockSize()) diff --git a/jwe/jwe.go b/jwe/jwe.go index 44909a266..1e6ecda5a 100644 --- a/jwe/jwe.go +++ b/jwe/jwe.go @@ -745,10 +745,28 @@ func (dctx *decryptCtx) decryptContent(ctx context.Context, alg jwa.KeyEncryptio if !ok { return nil, fmt.Errorf(`failed to get 'p2c' field`) } - countFlt, ok := count.(float64) - if !ok { - return nil, fmt.Errorf("unexpected type for 'p2c': %T", count) + + // check if WithUseNumber is effective, because it will change the + // type of the underlying value (#1140) + var countFlt float64 + if json.UseNumber() { + num, ok := count.(json.Number) + if !ok { + return nil, fmt.Errorf("unexpected type for 'p2c': %T", count) + } + v, err := num.Float64() + if err != nil { + return nil, fmt.Errorf("failed to convert 'p2c' to float64: %w", err) + } + countFlt = v + } else { + v, ok := count.(float64) + if !ok { + return nil, fmt.Errorf("unexpected type for 'p2c': %T", count) + } + countFlt = v } + muSettings.RLock() maxCount := maxPBES2Count muSettings.RUnlock() diff --git a/jwk/BUILD.bazel b/jwk/BUILD.bazel index a61a919f5..170b1fb15 100644 --- a/jwk/BUILD.bazel +++ b/jwk/BUILD.bazel @@ -35,6 +35,7 @@ go_library( "//internal/pool", "//jwa", "//x25519", + "//jwk/internal/x509", "@com_github_lestrrat_go_blackmagic//:go_default_library", "@com_github_lestrrat_go_httprc//:go_default_library", "@com_github_lestrrat_go_iter//arrayiter:go_default_library", @@ -66,6 +67,7 @@ go_test( "//jwa", "//jws", "//x25519", + "//jwk/internal/x509", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", ], diff --git a/jwk/es256k_go1.20_test.go b/jwk/es256k_go1.20_test.go new file mode 100644 index 000000000..44db4cbdc --- /dev/null +++ b/jwk/es256k_go1.20_test.go @@ -0,0 +1,44 @@ +//go:build jwx_es256k && jwx_secp256k1_pem && go1.20 + +package jwk_test + +import ( + "fmt" + "testing" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/stretchr/testify/require" +) + +func TestES256KPem(t *testing.T) { + raw, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err, `GeneratePrivateKey should succeed`) + + testcases := []interface{}{raw.ToECDSA(), raw.PubKey().ToECDSA()} + for _, tc := range testcases { + t.Run(fmt.Sprintf("Marshal %T", tc), func(t *testing.T) { + key, err := jwk.FromRaw(tc) + require.NoError(t, err, `FromRaw should succeed`) + + pem, err := jwk.Pem(key) + require.NoError(t, err, `Pem should succeed`) + require.NotEmpty(t, pem, `Pem should not be empty`) + + parsed, err := jwk.Parse(pem, jwk.WithPEM(true)) + require.NoError(t, err, `Parse should succeed`) + _ = parsed + }) + } + + t.Run("ParsePKCS8PrivateKey", func(t *testing.T) { + const src = `-----BEGIN PRIVATE KEY----- +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQggS9t6iYyj9JSL+btkMEq +pMYitWV4X+/Jg9zu3L8Ob5ShRANCAAT/YrxWHfw3e8lfDncJLLkPRbdby0L4qT95 +vyWU5lPpSwRbEAfSFR1E5RD9irkN1mCY8D1ko1PAlmHVB78pNzq4 +-----END PRIVATE KEY-----` + key, err := jwk.Parse([]byte(src), jwk.WithPEM(true)) + require.NoError(t, err, `Parse should succeed`) + require.NotNil(t, key, `key should not be nil`) + }) +} diff --git a/jwk/internal/x509/BUILD.bazel b/jwk/internal/x509/BUILD.bazel new file mode 100644 index 000000000..222c91088 --- /dev/null +++ b/jwk/internal/x509/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "x509", + srcs = [ "x509.go", "x509_nosecp256k1.go", "x509_sepc256k1.go" ], + importpath = "github.com/lestrrat-go/jwx/v2/jwk/internal/x509", + visibility = ["//:__subpackages__"], +) + +alias( + name = "go_default_library", + actual = ":x509", + visibility = ["//jwe:__subpackages__"], +) diff --git a/jwk/internal/x509/x509.go b/jwk/internal/x509/x509.go new file mode 100644 index 000000000..383203d76 --- /dev/null +++ b/jwk/internal/x509/x509.go @@ -0,0 +1,31 @@ +package x509 + +import ( + "crypto/rsa" + "crypto/x509" +) + +// In this x509 package we provide a proxy for crypto/x509 methods, +// so that we can easily swap out the ParseECPrivateKey method with +// our version of it that recognizes the secp256k1 curve... +// _IF_ the jwx_es256k build tag is set. + +func MarshalPKCS1PrivateKey(priv *rsa.PrivateKey) []byte { + return x509.MarshalPKCS1PrivateKey(priv) +} + +func MarshalPKCS8PrivateKey(priv interface{}) ([]byte, error) { + return x509.MarshalPKCS8PrivateKey(priv) +} + +func ParsePKCS1PrivateKey(der []byte) (*rsa.PrivateKey, error) { + return x509.ParsePKCS1PrivateKey(der) +} + +func ParsePKCS1PublicKey(der []byte) (*rsa.PublicKey, error) { + return x509.ParsePKCS1PublicKey(der) +} + +func ParseCertificate(der []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(der) +} diff --git a/jwk/internal/x509/x509_nosecp256k1.go b/jwk/internal/x509/x509_nosecp256k1.go new file mode 100644 index 000000000..efcd6cfe1 --- /dev/null +++ b/jwk/internal/x509/x509_nosecp256k1.go @@ -0,0 +1,28 @@ +//go:build !jwx_es256k || !jwx_secp256k1_pem || !go1.20 + +package x509 + +import ( + "crypto/ecdsa" + "crypto/x509" +) + +func MarshalECPrivateKey(priv *ecdsa.PrivateKey) ([]byte, error) { + return x509.MarshalECPrivateKey(priv) +} + +func ParseECPrivateKey(der []byte) (*ecdsa.PrivateKey, error) { + return x509.ParseECPrivateKey(der) +} + +func MarshalPKIXPublicKey(pub any) ([]byte, error) { + return x509.MarshalPKIXPublicKey(pub) +} + +func ParsePKIXPublicKey(der []byte) (any, error) { + return x509.ParsePKIXPublicKey(der) +} + +func ParsePKCS8PrivateKey(der []byte) (interface{}, error) { + return x509.ParsePKCS8PrivateKey(der) +} diff --git a/jwk/internal/x509/x509_sepc256k1.go b/jwk/internal/x509/x509_sepc256k1.go new file mode 100644 index 000000000..248ab558e --- /dev/null +++ b/jwk/internal/x509/x509_sepc256k1.go @@ -0,0 +1,479 @@ +//go:build jwx_es256k && jwx_secp256k1_pem && go1.20 + +package x509 + +import ( + "bytes" + "crypto/dsa" + "crypto/ecdh" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rsa" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + "math/big" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "golang.org/x/crypto/cryptobyte" + cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" +) + +// See src/crypto/x509/sec1.go for original code +const ecPrivKeyVersion = 1 + +// See src/crypto/x509/x509.go for original code +type publicKeyInfo struct { + Raw asn1.RawContent + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString +} + +// See src/crypto/x509/pkcs1.go for original code +type pkcs1PrivateKey struct { + Version int + N *big.Int + E int + D *big.Int + P *big.Int + Q *big.Int + // We ignore these values, if present, because rsa will calculate them. + Dp *big.Int `asn1:"optional"` + Dq *big.Int `asn1:"optional"` + Qinv *big.Int `asn1:"optional"` + + AdditionalPrimes []pkcs1AdditionalRSAPrime `asn1:"optional,omitempty"` +} + +// See src/crypto/x509/pkcs1.go for original code +type pkcs1AdditionalRSAPrime struct { + Prime *big.Int + + // We ignore these values because rsa will calculate them. + Exp *big.Int + Coeff *big.Int +} + +// See src/crypto/x509/pkcs1.go for original code +type pkcs1PublicKey struct { + N *big.Int + E int +} + +// See src/crypto/x509/pkcs8.go for original code +type pkcs8 struct { + Version int + Algo pkix.AlgorithmIdentifier + PrivateKey []byte + // optional attributes omitted. +} + +// See src/crypto/x509/x509.go for original code +type pkixPublicKey struct { + Algo pkix.AlgorithmIdentifier + BitString asn1.BitString +} + +type ecPrivateKey struct { + Version int + PrivateKey []byte + NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` + PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` +} + +var ( + // See src/crypto/x509/x509.go for original code + oidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33} + oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7} + oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34} + oidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35} + oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} + oidPublicKeyDSA = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 1} + oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} + oidPublicKeyX25519 = asn1.ObjectIdentifier{1, 3, 101, 110} + oidPublicKeyEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112} + // Added for this package + oidNamedCurveSecp256K1 = asn1.ObjectIdentifier{1, 3, 132, 0, 10} +) + +// See src/crypto/x509/x509.go for original code +func namedCurveFromOID(oid asn1.ObjectIdentifier) elliptic.Curve { + switch { + case oid.Equal(oidNamedCurveP224): + return elliptic.P224() + case oid.Equal(oidNamedCurveP256): + return elliptic.P256() + case oid.Equal(oidNamedCurveP384): + return elliptic.P384() + case oid.Equal(oidNamedCurveP521): + return elliptic.P521() + case oid.Equal(oidNamedCurveSecp256K1): + // Added for this package + return secp256k1.S256() + } + return nil +} + +// See src/crypto/x509/x509.go for original code +func oidFromNamedCurve(curve elliptic.Curve) (asn1.ObjectIdentifier, bool) { + switch curve { + case elliptic.P224(): + return oidNamedCurveP224, true + case elliptic.P256(): + return oidNamedCurveP256, true + case elliptic.P384(): + return oidNamedCurveP384, true + case elliptic.P521(): + return oidNamedCurveP521, true + case secp256k1.S256(): + return oidNamedCurveSecp256K1, true + } + + return nil, false +} + +// See crypto/x509/x509.go for original code +func oidFromECDHCurve(curve ecdh.Curve) (asn1.ObjectIdentifier, bool) { + switch curve { + case ecdh.X25519(): + return oidPublicKeyX25519, true + case ecdh.P256(): + return oidNamedCurveP256, true + case ecdh.P384(): + return oidNamedCurveP384, true + case ecdh.P521(): + return oidNamedCurveP521, true + } + + return nil, false +} + +func MarshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) { + // See src/crypto/x509/sec1.go for original code + oid, ok := oidFromNamedCurve(key.Curve) + if !ok { + return nil, errors.New("x509: unknown elliptic curve") + } + privateKey := make([]byte, (key.Curve.Params().N.BitLen()+7)/8) + return asn1.Marshal(ecPrivateKey{ + Version: 1, + PrivateKey: key.D.FillBytes(privateKey), + NamedCurveOID: oid, + PublicKey: asn1.BitString{Bytes: elliptic.Marshal(key.Curve, key.X, key.Y)}, + }) +} + +func ParseECPrivateKey(der []byte) (*ecdsa.PrivateKey, error) { + return parseECPrivateKey(nil, der) +} + +func parseECPrivateKey(namedCurveOID *asn1.ObjectIdentifier, der []byte) (*ecdsa.PrivateKey, error) { + var privKey ecPrivateKey + if _, err := asn1.Unmarshal(der, &privKey); err != nil { + if _, err := asn1.Unmarshal(der, &pkcs8{}); err == nil { + return nil, errors.New("x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)") + } + if _, err := asn1.Unmarshal(der, &pkcs1PrivateKey{}); err == nil { + return nil, errors.New("x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)") + } + return nil, errors.New("x509: failed to parse EC private key: " + err.Error()) + } + if privKey.Version != ecPrivKeyVersion { + return nil, fmt.Errorf("x509: unknown EC private key version %d", privKey.Version) + } + + var curve elliptic.Curve + if namedCurveOID != nil { + curve = namedCurveFromOID(*namedCurveOID) + } else { + curve = namedCurveFromOID(privKey.NamedCurveOID) + } + if curve == nil { + return nil, errors.New("x509: unknown elliptic curve") + } + + k := new(big.Int).SetBytes(privKey.PrivateKey) + curveOrder := curve.Params().N + if k.Cmp(curveOrder) >= 0 { + return nil, errors.New("x509: invalid elliptic curve private key value") + } + priv := new(ecdsa.PrivateKey) + priv.Curve = curve + priv.D = k + + privateKey := make([]byte, (curveOrder.BitLen()+7)/8) + + // Some private keys have leading zero padding. This is invalid + // according to [SEC1], but this code will ignore it. + for len(privKey.PrivateKey) > len(privateKey) { + if privKey.PrivateKey[0] != 0 { + return nil, errors.New("x509: invalid private key length") + } + privKey.PrivateKey = privKey.PrivateKey[1:] + } + + // Some private keys remove all leading zeros, this is also invalid + // according to [SEC1] but since OpenSSL used to do this, we ignore + // this too. + copy(privateKey[len(privateKey)-len(privKey.PrivateKey):], privKey.PrivateKey) + priv.X, priv.Y = curve.ScalarBaseMult(privateKey) + + return priv, nil +} + +// See src/crypto/x509/x509.go for original code +func marshalPublicKey(pub any) (publicKeyBytes []byte, publicKeyAlgorithm pkix.AlgorithmIdentifier, err error) { + switch pub := pub.(type) { + case *rsa.PublicKey: + publicKeyBytes, err = asn1.Marshal(pkcs1PublicKey{ + N: pub.N, + E: pub.E, + }) + if err != nil { + return nil, pkix.AlgorithmIdentifier{}, err + } + publicKeyAlgorithm.Algorithm = oidPublicKeyRSA + // This is a NULL parameters value which is required by + // RFC 3279, Section 2.3.1. + publicKeyAlgorithm.Parameters = asn1.NullRawValue + case *ecdsa.PublicKey: + oid, ok := oidFromNamedCurve(pub.Curve) + if !ok { + return nil, pkix.AlgorithmIdentifier{}, errors.New("x509: unsupported elliptic curve") + } + if !pub.Curve.IsOnCurve(pub.X, pub.Y) { + return nil, pkix.AlgorithmIdentifier{}, errors.New("x509: invalid elliptic curve public key") + } + publicKeyBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y) + publicKeyAlgorithm.Algorithm = oidPublicKeyECDSA + var paramBytes []byte + paramBytes, err = asn1.Marshal(oid) + if err != nil { + return + } + publicKeyAlgorithm.Parameters.FullBytes = paramBytes + case ed25519.PublicKey: + publicKeyBytes = pub + publicKeyAlgorithm.Algorithm = oidPublicKeyEd25519 + case *ecdh.PublicKey: + publicKeyBytes = pub.Bytes() + if pub.Curve() == ecdh.X25519() { + publicKeyAlgorithm.Algorithm = oidPublicKeyX25519 + } else { + oid, ok := oidFromECDHCurve(pub.Curve()) + if !ok { + return nil, pkix.AlgorithmIdentifier{}, errors.New("x509: unsupported elliptic curve") + } + publicKeyAlgorithm.Algorithm = oidPublicKeyECDSA + var paramBytes []byte + paramBytes, err = asn1.Marshal(oid) + if err != nil { + return + } + publicKeyAlgorithm.Parameters.FullBytes = paramBytes + } + default: + return nil, pkix.AlgorithmIdentifier{}, fmt.Errorf("x509: unsupported public key type: %T", pub) + } + + return publicKeyBytes, publicKeyAlgorithm, nil +} + +// See src/crypto/x509/x509.go for original code +func MarshalPKIXPublicKey(pub any) ([]byte, error) { + var publicKeyBytes []byte + var publicKeyAlgorithm pkix.AlgorithmIdentifier + var err error + + if publicKeyBytes, publicKeyAlgorithm, err = marshalPublicKey(pub); err != nil { + return nil, err + } + + pkix := pkixPublicKey{ + Algo: publicKeyAlgorithm, + BitString: asn1.BitString{ + Bytes: publicKeyBytes, + BitLength: 8 * len(publicKeyBytes), + }, + } + + ret, _ := asn1.Marshal(pkix) + return ret, nil +} + +func ParsePKIXPublicKey(derBytes []byte) (pub any, err error) { + var pki publicKeyInfo + if rest, err := asn1.Unmarshal(derBytes, &pki); err != nil { + if _, err := asn1.Unmarshal(derBytes, &pkcs1PublicKey{}); err == nil { + return nil, errors.New("x509: failed to parse public key (use ParsePKCS1PublicKey instead for this key format)") + } + return nil, err + } else if len(rest) != 0 { + return nil, errors.New("x509: trailing data after ASN.1 of public-key") + } + return parsePublicKey(&pki) +} + +func parsePublicKey(keyData *publicKeyInfo) (any, error) { + oid := keyData.Algorithm.Algorithm + params := keyData.Algorithm.Parameters + der := cryptobyte.String(keyData.PublicKey.RightAlign()) + switch { + case oid.Equal(oidPublicKeyRSA): + // RSA public keys must have a NULL in the parameters. + // See RFC 3279, Section 2.3.1. + if !bytes.Equal(params.FullBytes, asn1.NullBytes) { + return nil, errors.New("x509: RSA key missing NULL parameters") + } + + p := &pkcs1PublicKey{N: new(big.Int)} + if !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) { + return nil, errors.New("x509: invalid RSA public key") + } + if !der.ReadASN1Integer(p.N) { + return nil, errors.New("x509: invalid RSA modulus") + } + if !der.ReadASN1Integer(&p.E) { + return nil, errors.New("x509: invalid RSA public exponent") + } + + if p.N.Sign() <= 0 { + return nil, errors.New("x509: RSA modulus is not a positive number") + } + if p.E <= 0 { + return nil, errors.New("x509: RSA public exponent is not a positive number") + } + + pub := &rsa.PublicKey{ + E: p.E, + N: p.N, + } + return pub, nil + case oid.Equal(oidPublicKeyECDSA): + paramsDer := cryptobyte.String(params.FullBytes) + namedCurveOID := new(asn1.ObjectIdentifier) + if !paramsDer.ReadASN1ObjectIdentifier(namedCurveOID) { + return nil, errors.New("x509: invalid ECDSA parameters") + } + namedCurve := namedCurveFromOID(*namedCurveOID) + if namedCurve == nil { + return nil, errors.New("x509: unsupported elliptic curve") + } + x, y := elliptic.Unmarshal(namedCurve, der) + if x == nil { + return nil, errors.New("x509: failed to unmarshal elliptic curve point") + } + pub := &ecdsa.PublicKey{ + Curve: namedCurve, + X: x, + Y: y, + } + return pub, nil + case oid.Equal(oidPublicKeyEd25519): + // RFC 8410, Section 3 + // > For all of the OIDs, the parameters MUST be absent. + if len(params.FullBytes) != 0 { + return nil, errors.New("x509: Ed25519 key encoded with illegal parameters") + } + if len(der) != ed25519.PublicKeySize { + return nil, errors.New("x509: wrong Ed25519 public key size") + } + return ed25519.PublicKey(der), nil + case oid.Equal(oidPublicKeyX25519): + // RFC 8410, Section 3 + // > For all of the OIDs, the parameters MUST be absent. + if len(params.FullBytes) != 0 { + return nil, errors.New("x509: X25519 key encoded with illegal parameters") + } + return ecdh.X25519().NewPublicKey(der) + case oid.Equal(oidPublicKeyDSA): + y := new(big.Int) + if !der.ReadASN1Integer(y) { + return nil, errors.New("x509: invalid DSA public key") + } + pub := &dsa.PublicKey{ + Y: y, + Parameters: dsa.Parameters{ + P: new(big.Int), + Q: new(big.Int), + G: new(big.Int), + }, + } + paramsDer := cryptobyte.String(params.FullBytes) + if !paramsDer.ReadASN1(¶msDer, cryptobyte_asn1.SEQUENCE) || + !paramsDer.ReadASN1Integer(pub.Parameters.P) || + !paramsDer.ReadASN1Integer(pub.Parameters.Q) || + !paramsDer.ReadASN1Integer(pub.Parameters.G) { + return nil, errors.New("x509: invalid DSA parameters") + } + if pub.Y.Sign() <= 0 || pub.Parameters.P.Sign() <= 0 || + pub.Parameters.Q.Sign() <= 0 || pub.Parameters.G.Sign() <= 0 { + return nil, errors.New("x509: zero or negative DSA parameter") + } + return pub, nil + default: + return nil, errors.New("x509: unknown public key algorithm") + } +} + +// See src/crypto/x509/pkcs8.go for original code +func ParsePKCS8PrivateKey(der []byte) (key any, err error) { + var privKey pkcs8 + if _, err := asn1.Unmarshal(der, &privKey); err != nil { + if _, err := asn1.Unmarshal(der, &ecPrivateKey{}); err == nil { + return nil, errors.New("x509: failed to parse private key (use ParseECPrivateKey instead for this key format)") + } + if _, err := asn1.Unmarshal(der, &pkcs1PrivateKey{}); err == nil { + return nil, errors.New("x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)") + } + return nil, err + } + switch { + case privKey.Algo.Algorithm.Equal(oidPublicKeyRSA): + key, err = ParsePKCS1PrivateKey(privKey.PrivateKey) + if err != nil { + return nil, errors.New("x509: failed to parse RSA private key embedded in PKCS#8: " + err.Error()) + } + return key, nil + + case privKey.Algo.Algorithm.Equal(oidPublicKeyECDSA): + bytes := privKey.Algo.Parameters.FullBytes + namedCurveOID := new(asn1.ObjectIdentifier) + if _, err := asn1.Unmarshal(bytes, namedCurveOID); err != nil { + namedCurveOID = nil + } + key, err = parseECPrivateKey(namedCurveOID, privKey.PrivateKey) + if err != nil { + return nil, errors.New("x509: failed to parse EC private key embedded in PKCS#8: " + err.Error()) + } + return key, nil + + case privKey.Algo.Algorithm.Equal(oidPublicKeyEd25519): + if l := len(privKey.Algo.Parameters.FullBytes); l != 0 { + return nil, errors.New("x509: invalid Ed25519 private key parameters") + } + var curvePrivateKey []byte + if _, err := asn1.Unmarshal(privKey.PrivateKey, &curvePrivateKey); err != nil { + return nil, fmt.Errorf("x509: invalid Ed25519 private key: %v", err) + } + if l := len(curvePrivateKey); l != ed25519.SeedSize { + return nil, fmt.Errorf("x509: invalid Ed25519 private key length: %d", l) + } + return ed25519.NewKeyFromSeed(curvePrivateKey), nil + + case privKey.Algo.Algorithm.Equal(oidPublicKeyX25519): + if l := len(privKey.Algo.Parameters.FullBytes); l != 0 { + return nil, errors.New("x509: invalid X25519 private key parameters") + } + var curvePrivateKey []byte + if _, err := asn1.Unmarshal(privKey.PrivateKey, &curvePrivateKey); err != nil { + return nil, fmt.Errorf("x509: invalid X25519 private key: %v", err) + } + return ecdh.X25519().NewPrivateKey(curvePrivateKey) + + default: + return nil, fmt.Errorf("x509: PKCS#8 wrapping contained private key with unknown algorithm: %v", privKey.Algo.Algorithm) + } +} diff --git a/jwk/jwk.go b/jwk/jwk.go index bf129e8c6..bc14bf7e1 100644 --- a/jwk/jwk.go +++ b/jwk/jwk.go @@ -10,7 +10,6 @@ import ( "crypto/ed25519" "crypto/elliptic" "crypto/rsa" - "crypto/x509" "encoding/pem" "errors" "fmt" @@ -21,6 +20,7 @@ import ( "github.com/lestrrat-go/jwx/v2/internal/ecutil" "github.com/lestrrat-go/jwx/v2/internal/json" "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk/internal/x509" "github.com/lestrrat-go/jwx/v2/x25519" ) @@ -675,7 +675,17 @@ func Pem(v interface{}) ([]byte, error) { func asnEncode(key Key) (string, []byte, error) { switch key := key.(type) { - case RSAPrivateKey, ECDSAPrivateKey, OKPPrivateKey: + case ECDSAPrivateKey: + var rawkey ecdsa.PrivateKey + if err := key.Raw(&rawkey); err != nil { + return "", nil, fmt.Errorf(`failed to get raw key from jwk.Key: %w`, err) + } + buf, err := x509.MarshalECPrivateKey(&rawkey) + if err != nil { + return "", nil, fmt.Errorf(`failed to marshal PKCS8: %w`, err) + } + return pmECPrivateKey, buf, nil + case RSAPrivateKey, OKPPrivateKey: var rawkey interface{} if err := key.Raw(&rawkey); err != nil { return "", nil, fmt.Errorf(`failed to get raw key from jwk.Key: %w`, err) diff --git a/jwt/http.go b/jwt/http.go index a8edc6036..706d8350d 100644 --- a/jwt/http.go +++ b/jwt/http.go @@ -10,6 +10,33 @@ import ( "github.com/lestrrat-go/jwx/v2/internal/pool" ) +// ParseCookie parses a JWT stored in a http.Cookie with the given name. +// If the specified cookie is not found, http.ErrNoCookie is returned. +func ParseCookie(req *http.Request, name string, options ...ParseOption) (Token, error) { + var dst **http.Cookie + //nolint:forcetypeassert + for _, option := range options { + switch option.Ident() { + case identCookie{}: + dst = option.Value().(**http.Cookie) + } + } + + cookie, err := req.Cookie(name) + if err != nil { + return nil, err + } + tok, err := ParseString(cookie.Value, options...) + if err != nil { + return nil, fmt.Errorf(`failed to parse token stored in cookie: %w`, err) + } + + if dst != nil { + *dst = cookie + } + return tok, nil +} + // ParseHeader parses a JWT stored in a http.Header. // // For the header "Authorization", it will strip the prefix "Bearer " and will @@ -46,9 +73,10 @@ func ParseForm(values url.Values, name string, options ...ParseOption) (Token, e // header key. Specifying WithFormKey() will tell it to search under // a specific form field. // -// By default, "Authorization" header will be searched. -// -// If WithHeaderKey() is used, you must explicitly re-enable searching for "Authorization" header. +// If none of jwt.WithHeaderKey()/jwt.WithCookieKey()/jwt.WithFormKey() is +// used, "Authorization" header will be searched. If any of these options +// are specified, you must explicitly re-enable searching for "Authorization" header +// if you also want to search for it. // // # searches for "Authorization" // jwt.ParseRequest(req) @@ -58,9 +86,15 @@ func ParseForm(values url.Values, name string, options ...ParseOption) (Token, e // // # searches for "Authorization" AND "x-my-token" // jwt.ParseRequest(req, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey("x-my-token")) +// +// Cookies are searched using (http.Request).Cookie(). If you have multiple +// cookies with the same name, and you want to search for a specific one that +// (http.Request).Cookie() would not return, you will need to implement your +// own logic to extract the cookie and use jwt.ParseString(). func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { var hdrkeys []string var formkeys []string + var cookiekeys []string var parseOptions []ParseOption for _, option := range options { //nolint:forcetypeassert @@ -69,11 +103,14 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { hdrkeys = append(hdrkeys, option.Value().(string)) case identFormKey{}: formkeys = append(formkeys, option.Value().(string)) + case identCookieKey{}: + cookiekeys = append(cookiekeys, option.Value().(string)) default: parseOptions = append(parseOptions, option) } } - if len(hdrkeys) == 0 { + + if len(hdrkeys) == 0 && len(formkeys) == 0 && len(cookiekeys) == 0 { hdrkeys = append(hdrkeys, "Authorization") } @@ -81,6 +118,8 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { defer pool.ReleaseKeyToErrorMap(mhdrs) mfrms := pool.GetKeyToErrorMap() defer pool.ReleaseKeyToErrorMap(mfrms) + mcookies := pool.GetKeyToErrorMap() + defer pool.ReleaseKeyToErrorMap(mcookies) for _, hdrkey := range hdrkeys { // Check presence via a direct map lookup @@ -97,6 +136,18 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { return tok, nil } + for _, name := range cookiekeys { + tok, err := ParseCookie(req, name, parseOptions...) + if err != nil { + if err == http.ErrNoCookie { + // not fatal + mcookies[name] = err + } + continue + } + return tok, nil + } + if cl := req.ContentLength; cl > 0 { if err := req.ParseForm(); err != nil { return nil, fmt.Errorf(`failed to parse form: %w`, err) @@ -135,20 +186,45 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { triedForms.WriteString(strconv.Quote(formkey)) } + var triedCookies strings.Builder + for i, cookiekey := range cookiekeys { + if i > 0 { + triedCookies.WriteString(", ") + } + triedCookies.WriteString(strconv.Quote(cookiekey)) + } + var b strings.Builder - b.WriteString(`failed to find a valid token in any location of the request (tried: [header keys: `) - b.WriteString(triedHdrs.String()) - b.WriteByte(']') + b.WriteString(`failed to find a valid token in any location of the request (tried: `) + olen := b.Len() + if triedHdrs.Len() > 0 { + b.WriteString(`header keys: [`) + b.WriteString(triedHdrs.String()) + b.WriteByte(']') + } if triedForms.Len() > 0 { - b.WriteString(", form keys: [") + if b.Len() > olen { + b.WriteString(", ") + } + b.WriteString("form keys: [") b.WriteString(triedForms.String()) b.WriteByte(']') } + + if triedCookies.Len() > 0 { + if b.Len() > olen { + b.WriteString(", ") + } + b.WriteString("cookie keys: [") + b.WriteString(triedCookies.String()) + b.WriteByte(']') + } b.WriteByte(')') lmhdrs := len(mhdrs) lmfrms := len(mfrms) - if lmhdrs > 0 || lmfrms > 0 { + lmcookies := len(mcookies) + if lmhdrs > 0 || lmfrms > 0 || lmcookies > 0 { b.WriteString(". Additionally, errors were encountered during attempts to parse") if lmhdrs > 0 { @@ -168,6 +244,22 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { b.WriteString(")") } + if lmcookies > 0 { + count := 0 + b.WriteString(" cookies: (") + for cookiekey, err := range mcookies { + if count > 0 { + b.WriteString(", ") + } + b.WriteString("[cookie key: ") + b.WriteString(strconv.Quote(cookiekey)) + b.WriteString(", error: ") + b.WriteString(strconv.Quote(err.Error())) + b.WriteString("]") + count++ + } + } + if lmfrms > 0 { count := 0 b.WriteString(" forms: (") diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index 86f323d72..04379c306 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -466,17 +466,31 @@ func TestValidateClaims(t *testing.T) { t.Parallel() // GitHub issue #37: tokens are invalid in the second they are created (because Now() is not after IssuedAt()) t.Run("Empty fields", func(t *testing.T) { + t.Parallel() token := jwt.New() + require.Error(t, jwt.Validate(token, jwt.WithIssuer("foo")), `token.Validate should fail`) + require.Error(t, jwt.Validate(token, jwt.WithJwtID("foo")), `token.Validate should fail`) + require.Error(t, jwt.Validate(token, jwt.WithSubject("foo")), `token.Validate should fail`) + }) + t.Run("Reset Validator, No validator", func(t *testing.T) { + t.Parallel() + token := jwt.New() + now := time.Now().UTC() + token.Set(jwt.IssuedAtKey, now) - if !assert.Error(t, jwt.Validate(token, jwt.WithIssuer("foo")), `token.Validate should fail`) { - return - } - if !assert.Error(t, jwt.Validate(token, jwt.WithJwtID("foo")), `token.Validate should fail`) { - return - } - if !assert.Error(t, jwt.Validate(token, jwt.WithSubject("foo")), `token.Validate should fail`) { - return - } + err := jwt.Validate(token, jwt.WithResetValidators(true)) + require.Error(t, err, `token.Validate should fail`) + require.Contains(t, err.Error(), "no validators specified", `error message should contain "no validators specified"`) + }) + t.Run("Reset Validator, Check iss only", func(t *testing.T) { + t.Parallel() + token := jwt.New() + iat := time.Now().UTC().Add(time.Hour * 24) + token.Set(jwt.IssuedAtKey, iat) + token.Set(jwt.IssuerKey, "github.com/lestrrat-go") + + err := jwt.Validate(token, jwt.WithResetValidators(true), jwt.WithIssuer("github.com/lestrrat-go")) + require.NoError(t, err, `token.Validate should succeed`) }) t.Run(jwt.IssuedAtKey+"+skew", func(t *testing.T) { t.Parallel() @@ -824,6 +838,7 @@ func TestParseRequest(t *testing.T) { jwt.WithHeaderKey("x-authorization"), jwt.WithFormKey("access_token"), jwt.WithFormKey("token"), + jwt.WithCookieKey("cookie"), jwt.WithKey(jwa.ES256, pubkey)) }, Error: true, @@ -911,6 +926,29 @@ func TestParseRequest(t *testing.T) { return jwt.ParseRequest(req, jwt.WithFormKey("access_token"), jwt.WithKey(jwa.ES256, pubkey)) }, }, + { + Name: "Token in cookie (w/ option)", + Request: func() *http.Request { + req := httptest.NewRequest(http.MethodGet, u, nil) + req.AddCookie(&http.Cookie{Name: "cookie", Value: string(signed)}) + return req + }, + Parse: func(req *http.Request) (jwt.Token, error) { + return jwt.ParseRequest(req, jwt.WithCookieKey("cookie"), jwt.WithKey(jwa.ES256, pubkey)) + }, + }, + { + Name: "Invalid token in cookie", + Request: func() *http.Request { + req := httptest.NewRequest(http.MethodGet, u, nil) + req.AddCookie(&http.Cookie{Name: "cookie", Value: string(signed) + "foobarbaz"}) + return req + }, + Parse: func(req *http.Request) (jwt.Token, error) { + return jwt.ParseRequest(req, jwt.WithCookieKey("cookie"), jwt.WithKey(jwa.ES256, pubkey)) + }, + Error: true, + }, { Name: "Token in access_token form field (w/o option)", Request: func() *http.Request { @@ -970,6 +1008,17 @@ func TestParseRequest(t *testing.T) { } }) } + + // One extra test. Make sure we can extract the cookie object that we used + // when parsing from cookies + t.Run("jwt.WithCookie", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, u, nil) + req.AddCookie(&http.Cookie{Name: "cookie", Value: string(signed)}) + var dst *http.Cookie + _, err := jwt.ParseRequest(req, jwt.WithCookieKey("cookie"), jwt.WithCookie(&dst), jwt.WithKey(jwa.ES256, pubkey)) + require.NoError(t, err, `jwt.ParseRequest should succeed`) + require.NotNil(t, dst, `cookie should be extracted`) + }) } func TestGHIssue368(t *testing.T) { diff --git a/jwt/openid/birthdate.go b/jwt/openid/birthdate.go index a193b4034..929f925c6 100644 --- a/jwt/openid/birthdate.go +++ b/jwt/openid/birthdate.go @@ -61,18 +61,17 @@ func (b *BirthdateClaim) UnmarshalJSON(data []byte) error { var intSize int func init() { - switch math.MaxInt { - case math.MaxInt16: - intSize = 16 - case math.MaxInt32: + intSize = 64 + if math.MaxInt == math.MaxInt32 { intSize = 32 - case math.MaxInt64: - intSize = 64 } } func parseBirthdayInt(s string) int { - i, _ := strconv.ParseInt(s, 10, intSize) + i, err := strconv.ParseInt(s, 10, intSize) + if err != nil { + return 0 + } return int(i) } diff --git a/jwt/options.yaml b/jwt/options.yaml index 5e257801e..7b2241031 100644 --- a/jwt/options.yaml +++ b/jwt/options.yaml @@ -72,6 +72,28 @@ options: Please be aware that in the next major release of this library, `jwt.Validate()`'s signature will change to include an explicit `context.Context` object. + - ident: ResetValidators + interface: ValidateOption + argument_type: bool + comment: | + WithResetValidators specifies that the default validators should be + reset before applying the custom validators. By default `jwt.Validate()` + checks for the validity of JWT by checking `exp`, `nbf`, and `iat`, even + when you specify more validators through other options. + + You SHOULD NOT use this option unless you know exactly what you are doing, + as this will pose significant security issues when used incorrectly. + + Using this option with the value `true` will remove all default checks, + and will expect you to specify validators as options. This is useful when you + want to skip the default validators and only use specific validators, such as + for https://openid.net/specs/openid-connect-rpinitiated-1_0.html, where + the token could be accepted even if the token is expired. + + If you set this option to true and you do not specify any validators, + `jwt.Validate()` will return an error. + + The default value is `false` (`iat`, `exp`, and `nbf` are automatically checked). - ident: FlattenAudience interface: GlobalOption argument_type: bool @@ -106,6 +128,24 @@ options: comment: | WithHeaderKey is used to specify header keys to search for tokens. + While the type system allows this option to be passed to `jwt.Parse()` directly, + doing so will have no effect. Only use it for HTTP request parsing functions + - ident: Cookie + interface: ParseOption + argument_type: '**http.Cookie' + comment: | + WithCookie is used to specify a variable to store the cookie used when `jwt.ParseCookie()` + is called. This allows you to inspect the cookie for additional information after a successful + parsing of the JWT token stored in the cookie. + + While the type system allows this option to be passed to `jwt.Parse()` directly, + doing so will have no effect. Only use it for HTTP request parsing functions + - ident: CookieKey + interface: ParseOption + argument_type: string + comment: | + WithCookieKey is used to specify cookie keys to search for tokens. + While the type system allows this option to be passed to `jwt.Parse()` directly, doing so will have no effect. Only use it for HTTP request parsing functions - ident: Token diff --git a/jwt/options_gen.go b/jwt/options_gen.go index 375d704e1..9a545f866 100644 --- a/jwt/options_gen.go +++ b/jwt/options_gen.go @@ -5,6 +5,7 @@ package jwt import ( "context" "io/fs" + "net/http" "time" "github.com/lestrrat-go/jwx/v2/jwe" @@ -126,6 +127,8 @@ type identAcceptableSkew struct{} type identClock struct{} type identCompactOnly struct{} type identContext struct{} +type identCookie struct{} +type identCookieKey struct{} type identEncryptOption struct{} type identFS struct{} type identFlattenAudience struct{} @@ -136,6 +139,7 @@ type identNumericDateFormatPrecision struct{} type identNumericDateParsePedantic struct{} type identNumericDateParsePrecision struct{} type identPedantic struct{} +type identResetValidators struct{} type identSignOption struct{} type identToken struct{} type identTruncation struct{} @@ -159,6 +163,14 @@ func (identContext) String() string { return "WithContext" } +func (identCookie) String() string { + return "WithCookie" +} + +func (identCookieKey) String() string { + return "WithCookieKey" +} + func (identEncryptOption) String() string { return "WithEncryptOption" } @@ -199,6 +211,10 @@ func (identPedantic) String() string { return "WithPedantic" } +func (identResetValidators) String() string { + return "WithResetValidators" +} + func (identSignOption) String() string { return "WithSignOption" } @@ -255,6 +271,24 @@ func WithContext(v context.Context) ValidateOption { return &validateOption{option.New(identContext{}, v)} } +// WithCookie is used to specify a variable to store the cookie used when `jwt.ParseCookie()` +// is called. This allows you to inspect the cookie for additional information after a successful +// parsing of the JWT token stored in the cookie. +// +// While the type system allows this option to be passed to `jwt.Parse()` directly, +// doing so will have no effect. Only use it for HTTP request parsing functions +func WithCookie(v **http.Cookie) ParseOption { + return &parseOption{option.New(identCookie{}, v)} +} + +// WithCookieKey is used to specify cookie keys to search for tokens. +// +// While the type system allows this option to be passed to `jwt.Parse()` directly, +// doing so will have no effect. Only use it for HTTP request parsing functions +func WithCookieKey(v string) ParseOption { + return &parseOption{option.New(identCookieKey{}, v)} +} + // WithEncryptOption provides an escape hatch for cases where extra options to // `(jws.Serializer).Encrypt()` must be specified when usng `jwt.Sign()`. Normally you do not // need to use this. @@ -333,6 +367,28 @@ func WithPedantic(v bool) ParseOption { return &parseOption{option.New(identPedantic{}, v)} } +// WithResetValidators specifies that the default validators should be +// reset before applying the custom validators. By default `jwt.Validate()` +// checks for the validity of JWT by checking `exp`, `nbf`, and `iat`, even +// when you specify more validators through other options. +// +// You SHOULD NOT use this option unless you know exactly what you are doing, +// as this will pose significant security issues when used incorrectly. +// +// Using this option with the value `true` will remove all default checks, +// and will expect you to specify validators as options. This is useful when you +// want to skip the default validators and only use specific validators, such as +// for https://openid.net/specs/openid-connect-rpinitiated-1_0.html, where +// the token could be accepted even if the token is expired. +// +// If you set this option to true and you do not specify any validators, +// `jwt.Validate()` will return an error. +// +// The default value is `false` (`iat`, `exp`, and `nbf` are automatically checked). +func WithResetValidators(v bool) ValidateOption { + return &validateOption{option.New(identResetValidators{}, v)} +} + // WithSignOption provides an escape hatch for cases where extra options to // `jws.Sign()` must be specified when usng `jwt.Sign()`. Normally you do not // need to use this. diff --git a/jwt/options_gen_test.go b/jwt/options_gen_test.go index 1ad69c2b7..a72cf4af1 100644 --- a/jwt/options_gen_test.go +++ b/jwt/options_gen_test.go @@ -13,6 +13,8 @@ func TestOptionIdent(t *testing.T) { require.Equal(t, "WithClock", identClock{}.String()) require.Equal(t, "WithCompactOnly", identCompactOnly{}.String()) require.Equal(t, "WithContext", identContext{}.String()) + require.Equal(t, "WithCookie", identCookie{}.String()) + require.Equal(t, "WithCookieKey", identCookieKey{}.String()) require.Equal(t, "WithEncryptOption", identEncryptOption{}.String()) require.Equal(t, "WithFS", identFS{}.String()) require.Equal(t, "WithFlattenAudience", identFlattenAudience{}.String()) @@ -23,6 +25,7 @@ func TestOptionIdent(t *testing.T) { require.Equal(t, "WithNumericDateParsePedantic", identNumericDateParsePedantic{}.String()) require.Equal(t, "WithNumericDateParsePrecision", identNumericDateParsePrecision{}.String()) require.Equal(t, "WithPedantic", identPedantic{}.String()) + require.Equal(t, "WithResetValidators", identResetValidators{}.String()) require.Equal(t, "WithSignOption", identSignOption{}.String()) require.Equal(t, "WithToken", identToken{}.String()) require.Equal(t, "WithTruncation", identTruncation{}.String()) diff --git a/jwt/validate.go b/jwt/validate.go index db2a65959..3638b5f33 100644 --- a/jwt/validate.go +++ b/jwt/validate.go @@ -48,11 +48,13 @@ func Validate(t Token, options ...ValidateOption) error { var clock Clock = ClockFunc(time.Now) var skew time.Duration - var validators = []Validator{ + var baseValidators = []Validator{ IsIssuedAtValid(), IsExpirationValid(), IsNbfValid(), } + var extraValidators []Validator + var resetValidators bool for _, o := range options { //nolint:forcetypeassert switch o.Ident() { @@ -64,6 +66,8 @@ func Validate(t Token, options ...ValidateOption) error { trunc = o.Value().(time.Duration) case identContext{}: ctx = o.Value().(context.Context) + case identResetValidators{}: + resetValidators = o.Value().(bool) case identValidator{}: v := o.Value().(Validator) switch v := v.(type) { @@ -72,22 +76,33 @@ func Validate(t Token, options ...ValidateOption) error { if err := isSupportedTimeClaim(v.c1); err != nil { return err } - validators = append(validators, IsRequired(v.c1)) + extraValidators = append(extraValidators, IsRequired(v.c1)) } if v.c2 != "" { if err := isSupportedTimeClaim(v.c2); err != nil { return err } - validators = append(validators, IsRequired(v.c2)) + extraValidators = append(extraValidators, IsRequired(v.c2)) } } - validators = append(validators, v) + extraValidators = append(extraValidators, v) } } ctx = SetValidationCtxSkew(ctx, skew) ctx = SetValidationCtxClock(ctx, clock) ctx = SetValidationCtxTruncation(ctx, trunc) + + var validators []Validator + if !resetValidators { + validators = append(baseValidators, extraValidators...) + } else { + if len(extraValidators) == 0 { + return fmt.Errorf(`no validators specified: jwt.WithResetValidators(true) and no jwt.WithValidator() specified`) + } + validators = extraValidators + } + for _, v := range validators { if err := v.Validate(ctx, t); err != nil { return err diff --git a/jwx_test.go b/jwx_test.go index 40011ff7b..b21e15186 100644 --- a/jwx_test.go +++ b/jwx_test.go @@ -635,3 +635,24 @@ func TestGH996(t *testing.T) { }) } } + +func TestGH1140(t *testing.T) { + // Using WithUseNumber changes the type of value obtained from the + // source JSON, which may cause issues + jwx.DecoderSettings(jwx.WithUseNumber(true)) + t.Cleanup(func() { + jwx.DecoderSettings(jwx.WithUseNumber(false)) + }) + key, err := jwk.FromRaw([]byte("secure-key")) + require.NoError(t, err, `jwk.FromRaw should succeed`) + + var encrypted []byte + encrypted, err = jwe.Encrypt( + []byte("test-encryption-payload"), + jwe.WithKey(jwa.PBES2_HS256_A128KW, key), + ) + require.NoError(t, err, `jwe.Encrypt should succeed`) + + _, err = jwe.Decrypt(encrypted, jwe.WithKey(jwa.PBES2_HS256_A128KW, key)) + require.NoError(t, err, `jwe.Decrypt should succeed`) +} diff --git a/tools/autodoc.pl b/tools/autodoc.pl index 60d1f4b6e..1074af83a 100644 --- a/tools/autodoc.pl +++ b/tools/autodoc.pl @@ -73,6 +73,6 @@ system("git", "switch", "-c", "autodoc-pr-$ENV{GITHUB_HEAD_REF}") == 0 or die $!; system("git", "commit", "-F", $commit_message_file->filename, @files) == 0 or die $!; system("git", "push", "origin", "HEAD:autodoc-pr-$ENV{GITHUB_HEAD_REF}") == 0 or die $!; - system("gh", "pr", "create", "--fill") == 0 or die $!; + system("gh", "pr", "create", "--base", "develop/$link_ref", "--fill") == 0 or die $!; } } diff --git a/tools/test.sh b/tools/test.sh index 905a6fe0a..80897a071 100755 --- a/tools/test.sh +++ b/tools/test.sh @@ -1,5 +1,6 @@ #!/bin/bash +export PATH="$(go env GOPATH)/bin:$PATH" ROOT=$(cd $(dirname $0)/..; pwd -P) DST="$ROOT/coverage.out" if [[ -e "$DST" ]]; then