This commit is contained in:
GitLab Deploy Bot
2025-10-21 23:45:13 +07:00
parent 6c387b420c
commit bb60e987e5
3548 changed files with 4952576 additions and 116 deletions
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 Micah Parks
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File diff suppressed because one or more lines are too long
+69
View File
@@ -0,0 +1,69 @@
package keyfunc
import (
"crypto/ecdsa"
"crypto/elliptic"
"errors"
"fmt"
"math/big"
)
const (
// ktyEC is the key type (kty) in the JWT header for ECDSA.
ktyEC = "EC"
// p256 represents a 256-bit cryptographic elliptical curve type.
p256 = "P-256"
// p384 represents a 384-bit cryptographic elliptical curve type.
p384 = "P-384"
// p521 represents a 521-bit cryptographic elliptical curve type.
p521 = "P-521"
)
var (
// ErrECDSACurve indicates an error with the ECDSA curve.
ErrECDSACurve = errors.New("invalid ECDSA curve")
)
// ECDSA parses a jsonWebKey and turns it into an ECDSA public key.
func (j *jsonWebKey) ECDSA() (publicKey *ecdsa.PublicKey, err error) {
if j.X == "" || j.Y == "" || j.Curve == "" {
return nil, fmt.Errorf("%w: %s", ErrMissingAssets, ktyEC)
}
// Decode the X coordinate from Base64.
//
// According to RFC 7518, this is a Base64 URL unsigned integer.
// https://tools.ietf.org/html/rfc7518#section-6.3
xCoordinate, err := base64urlTrailingPadding(j.X)
if err != nil {
return nil, err
}
yCoordinate, err := base64urlTrailingPadding(j.Y)
if err != nil {
return nil, err
}
publicKey = &ecdsa.PublicKey{}
switch j.Curve {
case p256:
publicKey.Curve = elliptic.P256()
case p384:
publicKey.Curve = elliptic.P384()
case p521:
publicKey.Curve = elliptic.P521()
default:
return nil, fmt.Errorf("%w: unknown curve: %s", ErrECDSACurve, j.Curve)
}
// Turn the X coordinate into *big.Int.
//
// According to RFC 7517, these numbers are in big-endian format.
// https://tools.ietf.org/html/rfc7517#appendix-A.1
publicKey.X = big.NewInt(0).SetBytes(xCoordinate)
publicKey.Y = big.NewInt(0).SetBytes(yCoordinate)
return publicKey, nil
}
+29
View File
@@ -0,0 +1,29 @@
package keyfunc
import (
"crypto/ed25519"
"fmt"
)
const (
// ktyEC is the key type (kty) in the JWT header for EdDSA.
ktyOKP = "OKP"
)
// EdDSA parses a jsonWebKey and turns it into a EdDSA public key.
func (j *jsonWebKey) EdDSA() (publicKey ed25519.PublicKey, err error) {
if j.X == "" {
return nil, fmt.Errorf("%w: %s", ErrMissingAssets, ktyOKP)
}
// Decode the public key from Base64.
//
// According to RFC 8037, this is from Base64 URL bytes.
// https://datatracker.ietf.org/doc/html/rfc8037#appendix-A.2
publicBytes, err := base64urlTrailingPadding(j.X)
if err != nil {
return nil, err
}
return publicBytes, nil
}
+130
View File
@@ -0,0 +1,130 @@
{
"keys": [
{
"kid": "zXew0UJ1h6Q4CCcd_9wxMzvcp5cEBifH0KWrCz2Kyxc",
"kty": "RSA",
"alg": "PS256",
"use": "sig",
"n": "wqS81x6fItPUdh1OWCT8p3AuLYgFlpmg61WXp6sp1pVijoyF29GOSaD9xE-vLtegX-5h0BnP7va0bwsOAPdh6SdeVslEifNGHCtID0xNFqHNWcXSt4eLfQKAPFUq0TsEO-8P1QHRq6yeG8JAFaxakkaagLFuV8Vd_21PGJFWhvJodJLhX_-Ym9L8XUpIPps_mQriMUOWDe-5DWjHnDtfV7mgaOxbBvVo3wj8V2Lmo5Li4HabT4MEzeJ6e9IdFo2kj_44Yy9osX-PMPtu8BQz_onPgf0wjrVWt349Rj6OkS8RxlNGYeuIxYZr0TOhP5F-yEPhSXDsKdVTwPf7zAAaKQ",
"e": "AQAB",
"x5c": [
"MIICmzCCAYMCBgF4HR7HNDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwMzEwMTcwOTE5WhcNMzEwMzEwMTcxMDU5WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCpLzXHp8i09R2HU5YJPyncC4tiAWWmaDrVZenqynWlWKOjIXb0Y5JoP3ET68u16Bf7mHQGc/u9rRvCw4A92HpJ15WyUSJ80YcK0gPTE0Woc1ZxdK3h4t9AoA8VSrROwQ77w/VAdGrrJ4bwkAVrFqSRpqAsW5XxV3/bU8YkVaG8mh0kuFf/5ib0vxdSkg+mz+ZCuIxQ5YN77kNaMecO19XuaBo7FsG9WjfCPxXYuajkuLgdptPgwTN4np70h0WjaSP/jhjL2ixf48w+27wFDP+ic+B/TCOtVa3fj1GPo6RLxHGU0Zh64jFhmvRM6E/kX7IQ+FJcOwp1VPA9/vMABopAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALILq1Z4oQNJZEUt24VZcvknsWtQtvPxl3JNcBQgDR5/IMgl5VndRZ9OT56KUqrR5xRsWiCvh5Lgv4fUEzAAo9ToiPLub1SKP063zWrvfgi3YZ19bty0iXFm7l2cpQ3ejFV7WpcdLJE0lapFdPLo6QaRdgNu/1p4vbYg7zSK1fQ0OY5b3ajhAx/bhWlrN685owRbO5/r4rUOa6oo9l4Qn7jUxKUx4rcoe7zUM7qrpOPqKvn0DBp3n1/+9pOZXCjIfZGvYwP5NhzBDCkRzaXcJHlOqWzMBzyovVrzVmUilBcj+EsTYJs0gVXKzduX5zO6YWhFs23lu7AijdkxTY65YM0="
],
"x5t": "IYIeevIT57t8ppUejM42Bqx6f3I",
"x5t#S256": "TuOrBy2NcTlFSWuZ8Kh8W8AjQagb4fnfP1SlKMO8-So"
},
{
"kid": "ebJxnm9B3QDBljB5XJWEu72qx6BawDaMAhwz4aKPkQ0",
"kty": "EC",
"alg": "ES512",
"use": "sig",
"crv": "P-521",
"x": "YQ95Xj8MTzcHytbU1h8YkCN2kdEQA7ThuZ1ctB9Ekiw6tlM9RwL62eQvzEt4Rz8qN69uRqgU9RzxQOkSU5xVvyo",
"y": "SMMuP3QnAPHtx7Go2ARsG3NBaySWBLmVvS8s2Ss7Vm_ISWenNbdjKOsY1XvtiQz5scGzWDCEUoZzgV8Ve1mLOV0"
},
{
"kid": "TVAAet63O3xy_KK6_bxVIu7Ra3_z1wlB543Fbwi5VaU",
"kty": "EC",
"alg": "ES384",
"use": "sig",
"crv": "P-384",
"x": "Pik2o5as-evijFABH5p6YLXHnWw8iQ_N1ummPY1c_UgG6NO0za-gNOhTz2-tsd_w",
"y": "e98VSff71k19SY_mHgp3707lgQVrhfVpiGa-sGaKxOWVpxd2jWMhB0Q4RpSRuCp5"
},
{
"kid": "arlUxX4hh56rNO-XdIPhDT7bqBMqcBwNQuP_TnZJNGs",
"kty": "RSA",
"alg": "RS512",
"use": "sig",
"n": "hhtifu8LL3ICE3BAX5l1KZv6Lni0lhlhBusSfepnpxcb4C_z2U71cQTnLY27kt8WB4bNG6e5_KMx9K3xUdd3euj9MCq8vytwEPieeHE1KXQuhJfLv017lhpK_dRMOHyc-9-50YNdgs_8KWRkrzjjuYrCiO9Iu76n5319e-SC8OPvNUglqxp2N0Sp2ltne2ZrpN8T3OEEXT62TSGmLAVopRGw5gllNVrJfmEyZJCRrBM6s5CQcz8un0FjkAAC4DI6QD-eBL0qG3_NR0hQvR1he2o4BLwjOKH45Pk_jj-eArp-DD6Xq6ABQVb5SNOSdaxl5lnmuotRoY3G5d9YSl-K3w",
"e": "AQAB",
"x5c": [
"MIICmzCCAYMCBgF4HSCcDzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwMzEwMTcxMTE5WhcNMzEwMzEwMTcxMjU5WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCGG2J+7wsvcgITcEBfmXUpm/oueLSWGWEG6xJ96menFxvgL/PZTvVxBOctjbuS3xYHhs0bp7n8ozH0rfFR13d66P0wKry/K3AQ+J54cTUpdC6El8u/TXuWGkr91Ew4fJz737nRg12Cz/wpZGSvOOO5isKI70i7vqfnfX175ILw4+81SCWrGnY3RKnaW2d7Zmuk3xPc4QRdPrZNIaYsBWilEbDmCWU1Wsl+YTJkkJGsEzqzkJBzPy6fQWOQAALgMjpAP54EvSobf81HSFC9HWF7ajgEvCM4ofjk+T+OP54Cun4MPperoAFBVvlI05J1rGXmWea6i1Ghjcbl31hKX4rfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAB7bpwPoL02WGCCVhCsbDkq9GeFUwF01opVyFTijZlTUoTf5RcaR2qAH9/irkLjZeFeyozzC5mGvIVruBwnx/6l4PcAMxKK4YiheFVoO/dytpGMCj6ToNmKpjlXzOLAHelieWIUDtAFSYzENjIO01PyXTGYpxebpQCocJBvppj5HqARS9iNPcqBltMhxWrWmMu81tOG3Y7yd2xsIYXk6KjaoefLeN8Was4BPJ0zR6tTSEm6ZOvSRvlppqh84kz7LmWem7gGHAsY2G3tWBUmOdO/SMNMThqV62yLf7sKsuoE1w06lfmrf6D2zGwoEyz+TT6fdSkc34Yeh7+c01X6nFWU="
],
"x5t": "geiCPGtT_10T8xGLUK1LA0_YQEE",
"x5t#S256": "dLp3_QNGwMbYll5VecnR8Q9NSeFVfqJPBTa2_8qf48I"
},
{
"kid": "tW6ae7TomE6_2jooM-sf9N_6lWg7HNtaQXrDsElBzM4",
"kty": "RSA",
"alg": "PS512",
"use": "sig",
"n": "p32N7jqKfMUB6_dKY1uZ3wizzPlBAXg9XrntfUcwNLRPfTBnshpt4uQBf3T8fexkbzhtR18oHvim-YvcWfC5eLGQmWHYiVwACa_C7oGqx51ijK2LRbUg4TKhnZX2X3Ld9xvr3HsosKh2UXn_Ay8nuvdfH-U6S7btT6a-AIFlt3BpqZP0EOl7rY-ie8nXoA13xX6BoyzYiNcugdYCU6czQcmTIJ1JLS0zohi4aTNehRt-1VMRpIMx7q7Ouq3Zhbi7RcDo-_D8FPRhWc2eEKd-h8ebFTIxEOrkguBIomjEFTf3SfYbOB_h-14v9Q2yz-NzyId3-ujRCQGC0hn-cixe2w",
"e": "AQAB",
"x5c": [
"MIICmzCCAYMCBgF4BKAxqzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwMzA1MjMwMDEwWhcNMzEwMzA1MjMwMTUwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCnfY3uOop8xQHr90pjW5nfCLPM+UEBeD1eue19RzA0tE99MGeyGm3i5AF/dPx97GRvOG1HXyge+Kb5i9xZ8Ll4sZCZYdiJXAAJr8LugarHnWKMrYtFtSDhMqGdlfZfct33G+vceyiwqHZRef8DLye6918f5TpLtu1Ppr4AgWW3cGmpk/QQ6Xutj6J7ydegDXfFfoGjLNiI1y6B1gJTpzNByZMgnUktLTOiGLhpM16FG37VUxGkgzHurs66rdmFuLtFwOj78PwU9GFZzZ4Qp36Hx5sVMjEQ6uSC4EiiaMQVN/dJ9hs4H+H7Xi/1DbLP43PIh3f66NEJAYLSGf5yLF7bAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHVWNBTExqlg4LTcyhUXI5U0iNPcMIVdKDoGPDc3EPjXyYNyjURX0oZ6b1Wv5t+XGmpZRqJNYb92xraQatIzLEsRn4IrmzViP+dIyFU8BEDubixTxeqx7LSw2j6LIFnZ05XdmWknlksNTlqi4CT6KL+1c24+QU3CcmU3mkQEIPA2yC4SdAB1oXI0jh49uP6a+JrE7JREZGAdwbIpZ1cqV6acPiJW3tOYfLrHwo7KYn3KwJvIBHXgFBNwx7fl2gYNQ0VEGKub3qVwW5RO5R/6Tcla9uZEfEiamms/Pn4hFA1qbsNHtA9IRGVRSmVeBKDxRvo0fxOUXp+NuZxEnhsoP3I="
],
"x5t": "f1l1fxICz1fe9mI-sSrtc19EDhU",
"x5t#S256": "NUJWRA4ADpLEg_SMkSoE4FKQN0H1Tlz85L-i7puVcqQ"
},
{
"kid": "Lx1FmayP2YBtxaqS1SKJRJGiXRKnw2ov5WmYIMG-BLE",
"kty": "RSA",
"alg": "PS384",
"use": "sig",
"n": "q7WM4SnrdzlFSo_A1DRhc-8Ho-pBsfs49kGRbw3O_OKFIUyZrzHaRuovW_QaEAyiO3HX8CNcGPcpHdmpl4DhTGEBLcd6xXtCaa65ct00Mq7ZHCRRCrKLh6lJ0rY9fP8vCV0RBigpkNoRfrqLQQN4VeVFTbGSrDaS0LzPbap0-q5FKXUR-OQmQEtOupXhKFQtbB73tL83YnG6Swl7nXsx54ulEoDzcCCYt7pjCVVp7L9fzI2_ucTdtQclAJVQZGKpsx7vabOJuiMUwuAIz56lOJyXRMePsW8UogwC4FA2A52STsYlhOPsDEW4iIExFVNqs-CGoDGhYLIavaCkZhXM0w",
"e": "AQAB",
"x5c": [
"MIICmzCCAYMCBgF4HR+9XjANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwMzEwMTcxMDIyWhcNMzEwMzEwMTcxMjAyWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrtYzhKet3OUVKj8DUNGFz7wej6kGx+zj2QZFvDc784oUhTJmvMdpG6i9b9BoQDKI7cdfwI1wY9ykd2amXgOFMYQEtx3rFe0Jprrly3TQyrtkcJFEKsouHqUnStj18/y8JXREGKCmQ2hF+uotBA3hV5UVNsZKsNpLQvM9tqnT6rkUpdRH45CZAS066leEoVC1sHve0vzdicbpLCXudezHni6USgPNwIJi3umMJVWnsv1/Mjb+5xN21ByUAlVBkYqmzHu9ps4m6IxTC4AjPnqU4nJdEx4+xbxSiDALgUDYDnZJOxiWE4+wMRbiIgTEVU2qz4IagMaFgshq9oKRmFczTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADTgP3SrcG3p9XUB7sM4a2IeY0J4bSEtqlZBuHgdgekYJ5DXETJ3hV/82GjitU50NBup0IJyI9KZ0KCwqHIKC2Jn/6biOpM9Ipk4BtNVzx3qKNsDac9qZmyMpm4V9QuWakajknerhwyynG3siGUntbPmLvf5UKvKtbiKlWS4dBPwfedIUnC85mYEnNKSzSI1NiM6TWHB9zQYkARXlb89sh0HBYs08BfRMyBVM+l3OczIyGeQAfhcL+pxPP/0jqPr1ctHUBj2zXkjZxDw1oJFgeD9GDtPcjc3spB20vsRtQUBlzbJElbGflqWGHJK5l5n7gNd3ZXZT0HJ+wUpPE8EUaM="
],
"x5t": "fjRYR1986VCLzbaZaw5r25UKahw",
"x5t#S256": "ZHNHpizlsjD3qSZh7gJQQBu8W9jBL2HR0y7-3u2Wb-g"
},
{
"kid": "gnmAfvmlsi3kKH3VlM1AJ85P2hekQ8ON_XvJqs3xPD8",
"kty": "RSA",
"alg": "RS384",
"use": "sig",
"n": "qUNQewKl3APQcbpACMNJ2XphPpupt395z6OZvj5CW9tiRXY3J7dqi8U0bWoIhtmmc7Js6hjp-A5W_FVStuXlT1hLyjJsHeu9ZVPnfIl2MnYN83zQBKw8E4mFsVv0UXNvkVPBF_k0yXrz-ABleWLOgFGnkNU9csc3Z5aihHcwRmC_oS7PZ9Vc-l0xBCyF3YRHI-al8ppSHwFreOweF3-JP3poNAXd906_tjX2KlHSJmNqcUNiSfEluyCp02ALlRFKXUQ1HlfSupHcHySDlanfUyIzZgM9ysCvC1vfNdAuwZ44oUBMul_XPxxhzlewL2Y8PtSDLUDWGTIou8M8049D8Q",
"e": "AQAB",
"x5c": [
"MIICmzCCAYMCBgF4BJVfaDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwMzA1MjI0ODIxWhcNMzEwMzA1MjI1MDAxWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpQ1B7AqXcA9BxukAIw0nZemE+m6m3f3nPo5m+PkJb22JFdjcnt2qLxTRtagiG2aZzsmzqGOn4Dlb8VVK25eVPWEvKMmwd671lU+d8iXYydg3zfNAErDwTiYWxW/RRc2+RU8EX+TTJevP4AGV5Ys6AUaeQ1T1yxzdnlqKEdzBGYL+hLs9n1Vz6XTEELIXdhEcj5qXymlIfAWt47B4Xf4k/emg0Bd33Tr+2NfYqUdImY2pxQ2JJ8SW7IKnTYAuVEUpdRDUeV9K6kdwfJIOVqd9TIjNmAz3KwK8LW9810C7BnjihQEy6X9c/HGHOV7AvZjw+1IMtQNYZMii7wzzTj0PxAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABoThxhMd7Xiq4x0GJeoJFv2yDKXCL3dJEAEWtOr2+PqdeJl/ZfOxBXynIvrdtYnQdICztN5ydEgDsZ02piDsxZ+s/0SA0iqjw/MEoBYobmr8V+xwUv+WtRLpTBXqWGMuG7NEtrbjKid0iKLLAOAU4dcHQ49iOF9VLnbTkf1EXp4iphJreaubOXMwT6/JDzQPT1dRR34hlhYeKKzMSA0Cz5aYL1tI+eH12rar0MDczXykLChNS/8MlyTzreEf0siUiS9S1kj/lOZKQDg9E/z8fm5vmHEHzAVwf4ON5iO29tDsqLw7BeJqC4AESjliXIqMrdpFynfPnIsGgf3dnph5BM="
],
"x5t": "CmRnQVduZWtEsdOC4mauUUsSWxA",
"x5t#S256": "BvC0LmuM8ZIApN3TQQZWWbGO-d082Ah5d3D6vPvahGw"
},
{
"kid": "CGt0ZWS4Lc5faiKSdi0tU0fjCAdvGROQRGU9iR7tV0A",
"kty": "EC",
"alg": "ES256",
"use": "sig",
"crv": "P-256",
"x": "DPW7n9yjfE6Rt-VvVmEdeu4QdW44qifocAPPDxACDDY",
"y": "-ejsVw8222-hg2dJWx3QV0hE4-I0Ujp7ZsWebE68JE0"
},
{
"kid": "C65q0EKQyhpd1m4fr7SKO2He_nAxgCtAdws64d2BLt8",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "ja99ybDrLvw11Z4CvNlDI-kkqJEBpSnvDf0pZF2DvBlvYmeVYL_ChqIe8E9GyHUmLMdtO_jifSgOqE5b8vILwi1kZnJR7N857uEnbWM9YTeevi_RZ-E_hr4frW2NKJ78YGvCzwLKG2GgtSjj0zuTLnSaK8fCGzqXgy6paXNhgHUSZgGwvO0YItpMlyJeqEj1wGTWz1IyA1sguF1cC7K0fojPbPoBwrhvaAeoGRPLraE0rrBsQv8iiLwnRBIez9B1j0NiUG8Iad953Y7UzaKOAw8crIEK45NIK_yxHUpxqcHLjPIcRyIyJGioRyGK7cp-_7iPLOCutQc-u46mom1_ZQ",
"e": "AQAB",
"x5c": [
"MIICmzCCAYMCBgF4BJRpbzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwMzA1MjI0NzE4WhcNMzEwMzA1MjI0ODU4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCNr33JsOsu/DXVngK82UMj6SSokQGlKe8N/SlkXYO8GW9iZ5Vgv8KGoh7wT0bIdSYsx207+OJ9KA6oTlvy8gvCLWRmclHs3znu4SdtYz1hN56+L9Fn4T+Gvh+tbY0onvxga8LPAsobYaC1KOPTO5MudJorx8IbOpeDLqlpc2GAdRJmAbC87Rgi2kyXIl6oSPXAZNbPUjIDWyC4XVwLsrR+iM9s+gHCuG9oB6gZE8utoTSusGxC/yKIvCdEEh7P0HWPQ2JQbwhp33ndjtTNoo4DDxysgQrjk0gr/LEdSnGpwcuM8hxHIjIkaKhHIYrtyn7/uI8s4K61Bz67jqaibX9lAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHrGJFhVNiQupIwkn2jiW/jBobm9CHUxOwQL5E7WdRz5uaOJ0v62PrynOQE9xim9Qk8bT3q7DThZs66U9bpIk3msKVRgXRfn5FZy1H5RKOlEEFZhGakPqSlC1yPbhUNhHXMs3GTzdGMLtYaGvSy6XM/8/zqVqVwgh6BpbAR9RfiSdyaiNTSBriu+n/tHW934G9J8UIzdfpVcb0Yt9y4o0UgIXt64NtGFq7zmNJijH88AxBZFB6eUUmQQCczebzoAjyYbVOes5gGFzboVWcyLe3iyD0vvsAVHJViXeiGoxhpKnc8ryISpRUBzsKngf5uZo3bnrD9PHLYBoGOHgzII1xw="
],
"x5t": "5GNr3LeRXHWI4YR8-QTSsF98oTI",
"x5t#S256": "Dgd0_wZZqvRuf4GEISPNHREX-1ixTMIsrPeGzk0bCxs"
},
{
"kty": "OKP",
"d": "TJ0UPkOZDPfneEDSH2ETbLQWjrALD-BPZQR-E7mgPvY",
"use": "sig",
"crv": "Ed25519",
"kid": "Q56A",
"x": "iZli54E2SkbrOvAThwrnxn1AMIOaazi_ckl6B-hbDK8"
},
{
"kty": "oct",
"use": "sig",
"kid": "hmac",
"k": "V_8Ob8dVs6JuZx6expyjShoUgFgxoaovGjmGhesL2jA"
},
{
"e": "AQAB",
"use": "enc",
"kid": "kidWithBadUse",
"kty": "RSA",
"n": "znO8fsURSvghcjbMu2nysqZhsreTkj-y46YL39kctmlj7-qqVLuvTUtw0XvsxwLi9WWczz_BsAm2Rn6LzyhvXUXjj6uMP8tk-HhWc4RMXP-esqB7y6WUmR8SioT94SykuVhWMDxwkg7kXTg_GWEYibEFJ7YM16vVZ2Na5z2vRfMRy7VARXRhDrinJmW0B-oY9FurPTyaZSDqOr-3Qkhk1jm9-6ygFsOkmnd4Ljnq28t8hq_4k3bdZSolZv11boQS8vDO-Fo_2YoQVxm4YMIjcr8bxZcali2slOEytEC5ItOKTPA_CydM62sJubw7MuTrOKh6GJrq0xnw6MtqR46-MQ"
}
]
}
+247
View File
@@ -0,0 +1,247 @@
package keyfunc
import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"sync"
"time"
)
var (
// ErrRefreshImpossible is returned when a refresh is attempted on a JWKS that was not created from a remote
// resource.
ErrRefreshImpossible = errors.New("refresh impossible: JWKS was not created from a remote resource")
// defaultRefreshTimeout is the default duration for the context used to create the HTTP request for a refresh of
// the JWKS.
defaultRefreshTimeout = time.Minute
)
// Get loads the JWKS at the given URL.
func Get(jwksURL string, options Options) (jwks *JWKS, err error) {
jwks = &JWKS{
jwksURL: jwksURL,
}
applyOptions(jwks, options)
if jwks.client == nil {
jwks.client = http.DefaultClient
}
if jwks.requestFactory == nil {
jwks.requestFactory = defaultRequestFactory
}
if jwks.responseExtractor == nil {
jwks.responseExtractor = ResponseExtractorStatusOK
}
if jwks.refreshTimeout == 0 {
jwks.refreshTimeout = defaultRefreshTimeout
}
if !options.JWKUseNoWhitelist && len(jwks.jwkUseWhitelist) == 0 {
jwks.jwkUseWhitelist = map[JWKUse]struct{}{
UseOmitted: {},
UseSignature: {},
}
}
err = jwks.refresh()
if err != nil {
if options.TolerateInitialJWKHTTPError {
if jwks.refreshErrorHandler != nil {
jwks.refreshErrorHandler(err)
}
jwks.keys = make(map[string]parsedJWK)
} else {
return nil, err
}
}
if jwks.refreshInterval != 0 || jwks.refreshUnknownKID {
if jwks.ctx == nil {
jwks.ctx = context.Background()
}
jwks.ctx, jwks.cancel = context.WithCancel(jwks.ctx)
jwks.refreshRequests = make(chan refreshRequest, 1)
go jwks.backgroundRefresh()
}
return jwks, nil
}
// Refresh manually refreshes the JWKS with the remote resource. It can bypass the rate limit if configured to do so.
// This function will return an ErrRefreshImpossible if the JWKS was created from a static source like given keys or raw
// JSON, because there is no remote resource to refresh from.
//
// This function will block until the refresh is finished or an error occurs.
func (j *JWKS) Refresh(ctx context.Context, options RefreshOptions) error {
if j.jwksURL == "" {
return ErrRefreshImpossible
}
// Check if the background goroutine was launched.
if j.refreshInterval != 0 || j.refreshUnknownKID {
ctx, cancel := context.WithCancel(ctx)
req := refreshRequest{
cancel: cancel,
ignoreRateLimit: options.IgnoreRateLimit,
}
select {
case <-ctx.Done():
return fmt.Errorf("failed to send request refresh to background goroutine: %w", j.ctx.Err())
case j.refreshRequests <- req:
}
<-ctx.Done()
if !errors.Is(ctx.Err(), context.Canceled) {
return fmt.Errorf("unexpected keyfunc background refresh context error: %w", ctx.Err())
}
} else {
err := j.refresh()
if err != nil {
return fmt.Errorf("failed to refresh JWKS: %w", err)
}
}
return nil
}
// backgroundRefresh is meant to be a separate goroutine that will update the keys in a JWKS over a given interval of
// time.
func (j *JWKS) backgroundRefresh() {
var lastRefresh time.Time
var queueOnce sync.Once
var refreshMux sync.Mutex
if j.refreshRateLimit != 0 {
lastRefresh = time.Now().Add(-j.refreshRateLimit)
}
// Create a channel that will never send anything unless there is a refresh interval.
refreshInterval := make(<-chan time.Time)
refresh := func() {
err := j.refresh()
if err != nil && j.refreshErrorHandler != nil {
j.refreshErrorHandler(err)
}
lastRefresh = time.Now()
}
// Enter an infinite loop that ends when the background ends.
for {
if j.refreshInterval != 0 {
refreshInterval = time.After(j.refreshInterval)
}
select {
case <-refreshInterval:
select {
case <-j.ctx.Done():
return
case j.refreshRequests <- refreshRequest{}:
default: // If the j.refreshRequests channel is full, don't send another request.
}
case req := <-j.refreshRequests:
refreshMux.Lock()
if req.ignoreRateLimit {
refresh()
} else if j.refreshRateLimit != 0 && lastRefresh.Add(j.refreshRateLimit).After(time.Now()) {
// Launch a goroutine that will get a reservation for a JWKS refresh or fail to and immediately return.
queueOnce.Do(func() {
go func() {
refreshMux.Lock()
wait := time.Until(lastRefresh.Add(j.refreshRateLimit))
refreshMux.Unlock()
select {
case <-j.ctx.Done():
return
case <-time.After(wait):
}
refreshMux.Lock()
defer refreshMux.Unlock()
refresh()
queueOnce = sync.Once{}
}()
})
} else {
refresh()
}
if req.cancel != nil {
req.cancel()
}
refreshMux.Unlock()
// Clean up this goroutine when its context expires.
case <-j.ctx.Done():
return
}
}
}
func defaultRequestFactory(ctx context.Context, url string) (*http.Request, error) {
return http.NewRequestWithContext(ctx, http.MethodGet, url, bytes.NewReader(nil))
}
// refresh does an HTTP GET on the JWKS URL to rebuild the JWKS.
func (j *JWKS) refresh() (err error) {
var ctx context.Context
var cancel context.CancelFunc
if j.ctx != nil {
ctx, cancel = context.WithTimeout(j.ctx, j.refreshTimeout)
} else {
ctx, cancel = context.WithTimeout(context.Background(), j.refreshTimeout)
}
defer cancel()
req, err := j.requestFactory(ctx, j.jwksURL)
if err != nil {
return fmt.Errorf("failed to create request via factory function: %w", err)
}
resp, err := j.client.Do(req)
if err != nil {
return err
}
jwksBytes, err := j.responseExtractor(ctx, resp)
if err != nil {
return fmt.Errorf("failed to extract response via extractor function: %w", err)
}
// Only reprocess if the JWKS has changed.
if len(jwksBytes) != 0 && bytes.Equal(jwksBytes, j.raw) {
return nil
}
j.raw = jwksBytes
updated, err := NewJSON(jwksBytes)
if err != nil {
return err
}
j.mux.Lock()
defer j.mux.Unlock()
j.keys = updated.keys
if j.givenKeys != nil {
for kid, key := range j.givenKeys {
// Only overwrite the key if configured to do so.
if !j.givenKIDOverride {
if _, ok := j.keys[kid]; ok {
continue
}
}
j.keys[kid] = parsedJWK{public: key.inter}
}
}
return nil
}
+115
View File
@@ -0,0 +1,115 @@
package keyfunc
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"encoding/json"
)
// GivenKey represents a cryptographic key that resides in a JWKS. In conjuncture with Options.
type GivenKey struct {
algorithm string
inter interface{}
}
// GivenKeyOptions represents the configuration options for a GivenKey.
type GivenKeyOptions struct {
// Algorithm is the given key's signing algorithm. Its value will be compared to unverified tokens' "alg" header.
//
// See RFC 8725 Section 3.1 for details.
// https://www.rfc-editor.org/rfc/rfc8725#section-3.1
//
// For a list of possible values, please see:
// https://www.rfc-editor.org/rfc/rfc7518#section-3.1
// https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms
Algorithm string
}
// NewGiven creates a JWKS from a map of given keys.
func NewGiven(givenKeys map[string]GivenKey) (jwks *JWKS) {
keys := make(map[string]parsedJWK)
for kid, given := range givenKeys {
keys[kid] = parsedJWK{
algorithm: given.algorithm,
public: given.inter,
}
}
return &JWKS{
keys: keys,
}
}
// NewGivenCustom creates a new GivenKey given an untyped variable. The key argument is expected to be a type supported
// by the jwt package used.
//
// Consider the options carefully as each field may have a security implication.
//
// See the https://pkg.go.dev/github.com/golang-jwt/jwt/v5#RegisterSigningMethod function for registering an unsupported
// signing method.
func NewGivenCustom(key interface{}, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
}
}
// NewGivenECDSA creates a new GivenKey given an ECDSA public key.
//
// Consider the options carefully as each field may have a security implication.
func NewGivenECDSA(key *ecdsa.PublicKey, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
}
}
// NewGivenEdDSA creates a new GivenKey given an EdDSA public key.
//
// Consider the options carefully as each field may have a security implication.
func NewGivenEdDSA(key ed25519.PublicKey, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
}
}
// NewGivenHMAC creates a new GivenKey given an HMAC key in a byte slice.
//
// Consider the options carefully as each field may have a security implication.
func NewGivenHMAC(key []byte, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
}
}
// NewGivenRSA creates a new GivenKey given an RSA public key.
//
// Consider the options carefully as each field may have a security implication.
func NewGivenRSA(key *rsa.PublicKey, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
}
}
// NewGivenKeysFromJSON parses a raw JSON message into a map of key IDs (`kid`) to GivenKeys. The returned map is
// suitable for passing to `NewGiven()` or as `Options.GivenKeys` to `Get()`
func NewGivenKeysFromJSON(jwksBytes json.RawMessage) (map[string]GivenKey, error) {
// Parse by making a temporary JWKS instance. No need to lock its map since it doesn't escape this function.
j, err := NewJSON(jwksBytes)
if err != nil {
return nil, err
}
keys := make(map[string]GivenKey, len(j.keys))
for kid, cryptoKey := range j.keys {
keys[kid] = GivenKey{
algorithm: cryptoKey.algorithm,
inter: cryptoKey.public,
}
}
return keys, nil
}
+239
View File
@@ -0,0 +1,239 @@
package keyfunc
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"sync"
"time"
)
var (
// ErrJWKAlgMismatch indicates that the given JWK was found, but its "alg" parameter's value did not match that of
// the JWT.
ErrJWKAlgMismatch = errors.New(`the given JWK was found, but its "alg" parameter's value did not match the expected algorithm`)
// ErrJWKUseWhitelist indicates that the given JWK was found, but its "use" parameter's value was not whitelisted.
ErrJWKUseWhitelist = errors.New(`the given JWK was found, but its "use" parameter's value was not whitelisted`)
// ErrKIDNotFound indicates that the given key ID was not found in the JWKS.
ErrKIDNotFound = errors.New("the given key ID was not found in the JWKS")
// ErrMissingAssets indicates there are required assets are missing to create a public key.
ErrMissingAssets = errors.New("required assets are missing to create a public key")
)
// ErrorHandler is a function signature that consumes an error.
type ErrorHandler func(err error)
const (
// UseEncryption is a JWK "use" parameter value indicating the JSON Web Key is to be used for encryption.
UseEncryption JWKUse = "enc"
// UseOmitted is a JWK "use" parameter value that was not specified or was empty.
UseOmitted JWKUse = ""
// UseSignature is a JWK "use" parameter value indicating the JSON Web Key is to be used for signatures.
UseSignature JWKUse = "sig"
)
// JWKUse is a set of values for the "use" parameter of a JWK.
// See https://tools.ietf.org/html/rfc7517#section-4.2.
type JWKUse string
// jsonWebKey represents a JSON Web Key inside a JWKS.
type jsonWebKey struct {
Algorithm string `json:"alg"`
Curve string `json:"crv"`
Exponent string `json:"e"`
K string `json:"k"`
ID string `json:"kid"`
Modulus string `json:"n"`
Type string `json:"kty"`
Use string `json:"use"`
X string `json:"x"`
Y string `json:"y"`
}
// parsedJWK represents a JSON Web Key parsed with fields as the correct Go types.
type parsedJWK struct {
algorithm string
public interface{}
use JWKUse
}
// JWKS represents a JSON Web Key Set (JWK Set).
type JWKS struct {
jwkUseWhitelist map[JWKUse]struct{}
cancel context.CancelFunc
client *http.Client
ctx context.Context
raw []byte
givenKeys map[string]GivenKey
givenKIDOverride bool
jwksURL string
keys map[string]parsedJWK
mux sync.RWMutex
refreshErrorHandler ErrorHandler
refreshInterval time.Duration
refreshRateLimit time.Duration
refreshRequests chan refreshRequest
refreshTimeout time.Duration
refreshUnknownKID bool
requestFactory func(ctx context.Context, url string) (*http.Request, error)
responseExtractor func(ctx context.Context, resp *http.Response) (json.RawMessage, error)
}
// rawJWKS represents a JWKS in JSON format.
type rawJWKS struct {
Keys []*jsonWebKey `json:"keys"`
}
// NewJSON creates a new JWKS from a raw JSON message.
func NewJSON(jwksBytes json.RawMessage) (jwks *JWKS, err error) {
var rawKS rawJWKS
err = json.Unmarshal(jwksBytes, &rawKS)
if err != nil {
return nil, err
}
// Iterate through the keys in the raw JWKS. Add them to the JWKS.
jwks = &JWKS{
keys: make(map[string]parsedJWK, len(rawKS.Keys)),
}
for _, key := range rawKS.Keys {
var keyInter interface{}
switch keyType := key.Type; keyType {
case ktyEC:
keyInter, err = key.ECDSA()
if err != nil {
continue
}
case ktyOKP:
keyInter, err = key.EdDSA()
if err != nil {
continue
}
case ktyOct:
keyInter, err = key.Oct()
if err != nil {
continue
}
case ktyRSA:
keyInter, err = key.RSA()
if err != nil {
continue
}
default:
// Ignore unknown key types silently.
continue
}
jwks.keys[key.ID] = parsedJWK{
algorithm: key.Algorithm,
use: JWKUse(key.Use),
public: keyInter,
}
}
return jwks, nil
}
// EndBackground ends the background goroutine to update the JWKS. It can only happen once and is only effective if the
// JWKS has a background goroutine refreshing the JWKS keys.
func (j *JWKS) EndBackground() {
if j.cancel != nil {
j.cancel()
}
}
// KIDs returns the key IDs (`kid`) for all keys in the JWKS.
func (j *JWKS) KIDs() (kids []string) {
j.mux.RLock()
defer j.mux.RUnlock()
kids = make([]string, len(j.keys))
index := 0
for kid := range j.keys {
kids[index] = kid
index++
}
return kids
}
// Len returns the number of keys in the JWKS.
func (j *JWKS) Len() int {
j.mux.RLock()
defer j.mux.RUnlock()
return len(j.keys)
}
// RawJWKS returns a copy of the raw JWKS received from the given JWKS URL.
func (j *JWKS) RawJWKS() []byte {
j.mux.RLock()
defer j.mux.RUnlock()
raw := make([]byte, len(j.raw))
copy(raw, j.raw)
return raw
}
// ReadOnlyKeys returns a read-only copy of the mapping of key IDs (`kid`) to cryptographic keys.
func (j *JWKS) ReadOnlyKeys() map[string]interface{} {
keys := make(map[string]interface{})
j.mux.Lock()
for kid, cryptoKey := range j.keys {
keys[kid] = cryptoKey.public
}
j.mux.Unlock()
return keys
}
// getKey gets the jsonWebKey from the given KID from the JWKS. It may refresh the JWKS if configured to.
func (j *JWKS) getKey(alg, kid string) (jsonKey interface{}, err error) {
j.mux.RLock()
pubKey, ok := j.keys[kid]
j.mux.RUnlock()
if !ok {
if !j.refreshUnknownKID {
return nil, ErrKIDNotFound
}
ctx, cancel := context.WithCancel(j.ctx)
req := refreshRequest{
cancel: cancel,
}
// Refresh the JWKS.
select {
case <-j.ctx.Done():
return
case j.refreshRequests <- req:
default:
// If the j.refreshRequests channel is full, return the error early.
return nil, ErrKIDNotFound
}
// Wait for the JWKS refresh to finish.
<-ctx.Done()
j.mux.RLock()
defer j.mux.RUnlock()
if pubKey, ok = j.keys[kid]; !ok {
return nil, ErrKIDNotFound
}
}
// jwkUseWhitelist might be empty if the jwks was from keyfunc.NewJSON() or if JWKUseNoWhitelist option was true.
if len(j.jwkUseWhitelist) > 0 {
_, ok = j.jwkUseWhitelist[pubKey.use]
if !ok {
return nil, fmt.Errorf(`%w: JWK "use" parameter value %q is not whitelisted`, ErrJWKUseWhitelist, pubKey.use)
}
}
if pubKey.algorithm != "" && pubKey.algorithm != alg {
return nil, fmt.Errorf(`%w: JWK "alg" parameter value %q does not match token "alg" parameter value %q`, ErrJWKAlgMismatch, pubKey.algorithm, alg)
}
return pubKey.public, nil
}
+59
View File
@@ -0,0 +1,59 @@
package keyfunc
import (
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/golang-jwt/jwt/v5"
)
var (
// ErrKID indicates that the JWT had an invalid kid.
ErrKID = errors.New("the JWT has an invalid kid")
)
// Keyfunc matches the signature of github.com/golang-jwt/jwt/v5's jwt.Keyfunc function.
func (j *JWKS) Keyfunc(token *jwt.Token) (interface{}, error) {
kid, alg, err := kidAlg(token)
if err != nil {
return nil, err
}
return j.getKey(alg, kid)
}
// Keyfunc matches the signature of github.com/golang-jwt/jwt/v5's jwt.Keyfunc function.
func (m *MultipleJWKS) Keyfunc(token *jwt.Token) (interface{}, error) {
return m.keySelector(m, token)
}
func kidAlg(token *jwt.Token) (kid, alg string, err error) {
kidInter, ok := token.Header["kid"]
if !ok {
return "", "", fmt.Errorf("%w: could not find kid in JWT header", ErrKID)
}
kid, ok = kidInter.(string)
if !ok {
return "", "", fmt.Errorf("%w: could not convert kid in JWT header to string", ErrKID)
}
alg, ok = token.Header["alg"].(string)
if !ok {
// For test coverage purposes, this should be impossible to reach because the JWT package rejects a token
// without an alg parameter in the header before calling jwt.Keyfunc.
return "", "", fmt.Errorf(`%w: the JWT header did not contain the "alg" parameter, which is required by RFC 7515 section 4.1.1`, ErrJWKAlgMismatch)
}
return kid, alg, nil
}
// base64urlTrailingPadding removes trailing padding before decoding a string from base64url. Some non-RFC compliant
// JWKS contain padding at the end values for base64url encoded public keys.
//
// Trailing padding is required to be removed from base64url encoded keys.
// RFC 7517 defines base64url the same as RFC 7515 Section 2:
// https://datatracker.ietf.org/doc/html/rfc7517#section-1.1
// https://datatracker.ietf.org/doc/html/rfc7515#section-2
func base64urlTrailingPadding(s string) ([]byte, error) {
s = strings.TrimRight(s, "=")
return base64.RawURLEncoding.DecodeString(s)
}
+72
View File
@@ -0,0 +1,72 @@
package keyfunc
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
)
// ErrMultipleJWKSSize is returned when the number of JWKS given are not enough to make a MultipleJWKS.
var ErrMultipleJWKSSize = errors.New("multiple JWKS must have one or more remote JWK Set resources")
// MultipleJWKS manages multiple JWKS and has a field for jwt.Keyfunc.
type MultipleJWKS struct {
keySelector func(multiJWKS *MultipleJWKS, token *jwt.Token) (key interface{}, err error)
sets map[string]*JWKS // No lock is required because this map is read-only after initialization.
}
// GetMultiple creates a new MultipleJWKS. A map of length one or more JWKS URLs to Options is required.
//
// Be careful when choosing Options for each JWKS in the map. If RefreshUnknownKID is set to true for all JWKS in the
// map then many refresh requests would take place each time a JWT is processed, this should be rate limited by
// RefreshRateLimit.
func GetMultiple(multiple map[string]Options, options MultipleOptions) (multiJWKS *MultipleJWKS, err error) {
if len(multiple) < 1 {
return nil, fmt.Errorf("multiple JWKS must have one or more remote JWK Set resources: %w", ErrMultipleJWKSSize)
}
if options.KeySelector == nil {
options.KeySelector = KeySelectorFirst
}
multiJWKS = &MultipleJWKS{
sets: make(map[string]*JWKS, len(multiple)),
keySelector: options.KeySelector,
}
for u, opts := range multiple {
jwks, err := Get(u, opts)
if err != nil {
return nil, fmt.Errorf("failed to get JWKS from %q: %w", u, err)
}
multiJWKS.sets[u] = jwks
}
return multiJWKS, nil
}
// JWKSets returns a copy of the map of JWK Sets. The map itself is a copy, but the JWKS are not and should be treated
// as read-only.
func (m *MultipleJWKS) JWKSets() map[string]*JWKS {
sets := make(map[string]*JWKS, len(m.sets))
for u, jwks := range m.sets {
sets[u] = jwks
}
return sets
}
// KeySelectorFirst returns the first key found in the multiple JWK Sets.
func KeySelectorFirst(multiJWKS *MultipleJWKS, token *jwt.Token) (key interface{}, err error) {
kid, alg, err := kidAlg(token)
if err != nil {
return nil, err
}
for _, jwks := range multiJWKS.sets {
key, err = jwks.getKey(alg, kid)
if err == nil {
return key, nil
}
}
return nil, fmt.Errorf("failed to find key ID in multiple JWKS: %w", ErrKIDNotFound)
}
+28
View File
@@ -0,0 +1,28 @@
package keyfunc
import (
"fmt"
)
const (
// ktyOct is the key type (kty) in the JWT header for oct.
ktyOct = "oct"
)
// Oct parses a jsonWebKey and turns it into a raw byte slice (octet). This includes HMAC keys.
func (j *jsonWebKey) Oct() (publicKey []byte, err error) {
if j.K == "" {
return nil, fmt.Errorf("%w: %s", ErrMissingAssets, ktyOct)
}
// Decode the octet key from Base64.
//
// According to RFC 7517, this is Base64 URL bytes.
// https://datatracker.ietf.org/doc/html/rfc7517#section-1.1
publicKey, err = base64urlTrailingPadding(j.K)
if err != nil {
return nil, err
}
return publicKey, nil
}
+165
View File
@@ -0,0 +1,165 @@
package keyfunc
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
)
// ErrInvalidHTTPStatusCode indicates that the HTTP status code is invalid.
var ErrInvalidHTTPStatusCode = errors.New("invalid HTTP status code")
// Options represents the configuration options for a JWKS.
//
// If either RefreshInterval is non-zero or RefreshUnknownKID is true, then a background goroutine will be launched to refresh the
// remote JWKS under the specified circumstances.
//
// When using a background refresh goroutine, make sure to use RefreshRateLimit if paired with RefreshUnknownKID. Also
// make sure to end the background refresh goroutine with the JWKS.EndBackground method when it's no longer needed.
type Options struct {
// Client is the HTTP client used to get the JWKS via HTTP.
Client *http.Client
// Ctx is the context for the keyfunc's background refresh. When the context expires or is canceled, the background
// goroutine will end.
Ctx context.Context
// GivenKeys is a map of JWT key IDs, `kid`, to their given keys. If the JWKS has a background refresh goroutine,
// these values persist across JWKS refreshes. By default, if the remote JWKS resource contains a key with the same
// `kid` any given keys with the same `kid` will be overwritten by the keys from the remote JWKS. Use the
// GivenKIDOverride option to flip this behavior.
GivenKeys map[string]GivenKey
// GivenKIDOverride will make a GivenKey override any keys with the same ID (`kid`) in the remote JWKS. The is only
// effectual if GivenKeys is provided.
GivenKIDOverride bool
// JWKUseWhitelist is a whitelist of JWK `use` parameter values that will restrict what keys can be returned for
// jwt.Keyfunc. The assumption is that jwt.Keyfunc is only used for JWT signature verification.
// The default behavior is to only return a JWK if its `use` parameter has the value `"sig"`, an empty string, or if
// the parameter was omitted entirely.
JWKUseWhitelist []JWKUse
// JWKUseNoWhitelist overrides the JWKUseWhitelist field and its default behavior. If set to true, all JWKs will be
// returned regardless of their `use` parameter value.
JWKUseNoWhitelist bool
// RefreshErrorHandler is a function that consumes errors that happen during a JWKS refresh. This is only effectual
// if a background refresh goroutine is active.
RefreshErrorHandler ErrorHandler
// RefreshInterval is the duration to refresh the JWKS in the background via a new HTTP request. If this is not zero,
// then a background goroutine will be used to refresh the JWKS once per the given interval. Make sure to call the
// JWKS.EndBackground method to end this goroutine when it's no longer needed.
RefreshInterval time.Duration
// RefreshRateLimit limits the rate at which refresh requests are granted. Only one refresh request can be queued
// at a time any refresh requests received while there is already a queue are ignored. It does not make sense to
// have RefreshInterval's value shorter than this.
RefreshRateLimit time.Duration
// RefreshTimeout is the duration for the context timeout used to create the HTTP request for a refresh of the JWKS.
// This defaults to one minute. This is used for the HTTP request and any background goroutine refreshes.
RefreshTimeout time.Duration
// RefreshUnknownKID indicates that the JWKS refresh request will occur every time a kid that isn't cached is seen.
// This is done through a background goroutine. Without specifying a RefreshInterval a malicious client could
// self-sign X JWTs, send them to this service, then cause potentially high network usage proportional to X. Make
// sure to call the JWKS.EndBackground method to end this goroutine when it's no longer needed.
//
// It is recommended this option is not used when in MultipleJWKS. This is because KID collisions SHOULD be uncommon
// meaning nearly any JWT SHOULD trigger a refresh for the number of JWKS in the MultipleJWKS minus one.
RefreshUnknownKID bool
// RequestFactory creates HTTP requests for the remote JWKS resource located at the given url. For example, an
// HTTP header could be added to indicate a User-Agent.
RequestFactory func(ctx context.Context, url string) (*http.Request, error)
// ResponseExtractor consumes a *http.Response and produces the raw JSON for the JWKS. By default, the
// ResponseExtractorStatusOK function is used. The default behavior changed in v1.4.0.
ResponseExtractor func(ctx context.Context, resp *http.Response) (json.RawMessage, error)
// TolerateInitialJWKHTTPError will tolerate any error from the initial HTTP JWKS request. If an error occurs,
// the RefreshErrorHandler will be given the error. The program will continue to run as if the error did not occur
// and a valid JWK Set with no keys was received in the response. This allows for the background goroutine to
// request the JWKS at a later time.
//
// It does not make sense to mark this field as true unless the background refresh goroutine is active.
TolerateInitialJWKHTTPError bool
}
// MultipleOptions is used to configure the behavior when multiple JWKS are used by MultipleJWKS.
type MultipleOptions struct {
// KeySelector is a function that selects the key to use for a given token. It will be used in the implementation
// for jwt.Keyfunc. If implementing this custom selector extract the key ID and algorithm from the token's header.
// Use the key ID to select a token and confirm the key's algorithm before returning it.
//
// This value defaults to KeySelectorFirst.
KeySelector func(multiJWKS *MultipleJWKS, token *jwt.Token) (key interface{}, err error)
}
// RefreshOptions are used to specify manual refresh behavior.
type RefreshOptions struct {
IgnoreRateLimit bool
}
type refreshRequest struct {
cancel context.CancelFunc
ignoreRateLimit bool
}
// ResponseExtractorStatusOK is meant to be used as the ResponseExtractor field for Options. It confirms that response
// status code is 200 OK and returns the raw JSON from the response body.
func ResponseExtractorStatusOK(ctx context.Context, resp *http.Response) (json.RawMessage, error) {
//goland:noinspection GoUnhandledErrorResult
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d", ErrInvalidHTTPStatusCode, resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
// ResponseExtractorStatusAny is meant to be used as the ResponseExtractor field for Options. It returns the raw JSON
// from the response body regardless of the response status code.
func ResponseExtractorStatusAny(ctx context.Context, resp *http.Response) (json.RawMessage, error) {
//goland:noinspection GoUnhandledErrorResult
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// applyOptions applies the given options to the given JWKS.
func applyOptions(jwks *JWKS, options Options) {
if options.Ctx != nil {
jwks.ctx, jwks.cancel = context.WithCancel(options.Ctx)
}
if options.GivenKeys != nil {
jwks.givenKeys = make(map[string]GivenKey)
for kid, key := range options.GivenKeys {
jwks.givenKeys[kid] = key
}
}
if !options.JWKUseNoWhitelist {
jwks.jwkUseWhitelist = make(map[JWKUse]struct{})
for _, use := range options.JWKUseWhitelist {
jwks.jwkUseWhitelist[use] = struct{}{}
}
}
jwks.client = options.Client
jwks.givenKIDOverride = options.GivenKIDOverride
jwks.refreshErrorHandler = options.RefreshErrorHandler
jwks.refreshInterval = options.RefreshInterval
jwks.refreshRateLimit = options.RefreshRateLimit
jwks.refreshTimeout = options.RefreshTimeout
jwks.refreshUnknownKID = options.RefreshUnknownKID
jwks.requestFactory = options.RequestFactory
jwks.responseExtractor = options.ResponseExtractor
}
+43
View File
@@ -0,0 +1,43 @@
package keyfunc
import (
"crypto/rsa"
"fmt"
"math/big"
)
const (
// ktyRSA is the key type (kty) in the JWT header for RSA.
ktyRSA = "RSA"
)
// RSA parses a jsonWebKey and turns it into an RSA public key.
func (j *jsonWebKey) RSA() (publicKey *rsa.PublicKey, err error) {
if j.Exponent == "" || j.Modulus == "" {
return nil, fmt.Errorf("%w: %s", ErrMissingAssets, ktyRSA)
}
// Decode the exponent from Base64.
//
// According to RFC 7518, this is a Base64 URL unsigned integer.
// https://tools.ietf.org/html/rfc7518#section-6.3
exponent, err := base64urlTrailingPadding(j.Exponent)
if err != nil {
return nil, err
}
modulus, err := base64urlTrailingPadding(j.Modulus)
if err != nil {
return nil, err
}
publicKey = &rsa.PublicKey{}
// Turn the exponent into an integer.
//
// According to RFC 7517, these numbers are in big-endian format.
// https://tools.ietf.org/html/rfc7517#appendix-A.1
publicKey.E = int(big.NewInt(0).SetBytes(exponent).Uint64())
publicKey.N = big.NewInt(0).SetBytes(modulus)
return publicKey, nil
}