mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-22 06:15:44 +00:00
.
This commit is contained in:
+201
@@ -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.
|
||||
+233
File diff suppressed because one or more lines are too long
+69
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user