├── .deepsource.toml ├── .github └── workflows │ ├── go.yml │ └── lock-threads.yml ├── .gitignore ├── LICENSE ├── Makefile ├── NOTICE.txt ├── README.md ├── fixtures ├── private.json ├── public.json └── symmetric.json ├── gin ├── jose.go ├── jose_benchmark_test.go ├── jose_example_test.go └── jose_test.go ├── go.mod ├── go.sum ├── jose.go ├── jose_test.go ├── jwk.go ├── jwk_client.go ├── jwk_client_test.go ├── jwk_example_test.go ├── jwk_test.go ├── jws.go ├── jws_test.go ├── key_cacher.go ├── mux ├── jose.go ├── jose_example_test.go └── jose_test.go ├── rejecter.go ├── rejecter_test.go ├── secrets ├── cypher.go ├── cypher_example_test.go └── cypher_test.go └── tests └── integration_test.go /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "go" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | import_root = "github.com/krakendio/krakend-jsonschema" 9 | 10 | [[transformers]] 11 | name = "gofmt" 12 | enabled = true 13 | 14 | [[transformers]] 15 | name = "gofumpt" 16 | enabled = true -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: "1.23" 20 | 21 | - name: Test 22 | run: make test 23 | -------------------------------------------------------------------------------- /.github/workflows/lock-threads.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v3 20 | with: 21 | pr-inactive-days: '90' 22 | issue-inactive-days: '90' 23 | add-issue-labels: 'locked' 24 | issue-comment: > 25 | This issue was marked as resolved a long time ago and now has been 26 | automatically locked as there has not been any recent activity after it. 27 | You can still open a new issue and reference this link. 28 | pr-comment: > 29 | This pull request was marked as resolved a long time ago and now has been 30 | automatically locked as there has not been any recent activity after it. 31 | You can still open a new issue and reference this link. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | coverage.out 3 | cert.pem 4 | key.pem 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test build 2 | 3 | generate: 4 | go generate ./... 5 | 6 | test: generate 7 | go test ./... -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | KrakenD 2 | Copyright 2016 - 2021 DevOps Faith 3 | This product includes software developed at 4 | DevOps Faith (https://devops.faith/). 5 | The Initial Developer of the key_identify_strategy to validate different JWK keys is Oracle. 6 | Copyright (C) 2021, Oracle and/or its affiliates. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # krakend-jose 2 | JOSE component for the KrakenD framework 3 | -------------------------------------------------------------------------------- /fixtures/private.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty":"EC", 5 | "crv":"P-256", 6 | "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 7 | "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 8 | "d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE", 9 | "use":"enc", 10 | "kid":"1" 11 | }, 12 | { 13 | "kty":"RSA", 14 | "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 15 | "e":"AQAB", 16 | "d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", 17 | "p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", 18 | "q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", 19 | "dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", 20 | "dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", 21 | "qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU", 22 | "alg":"RS256", 23 | "kid":"2011-04-29" 24 | }, 25 | { 26 | "p": "2B4PjrWuJI-NarIJW9tPIb4EtBFBNGl-UpmRdZruIpU6GWbhWJTxbfSVLPGS7Rm3oQDhW7Y9MMv8GzmeLjFWzk_WFqBn-CAT4hfoQTqxekJnFvxnwHaZttBQWic39TFWjqWotspd-X9ZCWXsMa3-aWCt-M4yniziKF_f4QY00zONHpnLXrPDoYLjKyVKnUZ9igZVR_OGvotOvDIuiPiog8vzJgcs1IbrkV-KMmdlZIhVUwbPCt26ZXtAsZd-mffwIqT_dReJ6Uoq6OAdjdRRF8ClCDLKFo4rz2AojOpqoY4xXg_mUIbc3c6vYmnSLtCz8ZGCmc67uLkELImdwe__OQ", 27 | "kty": "RSA", 28 | "q": "y_4QID3Kk9VS47gk_nc_FAMMBCOWdl7LIJ4od6atMVjwyQvszKov1EUj7EKZCuPKuPGSxdX2KZka0UgOpivhkTBxsPeIeWFhPZ28pj9PjcjkJXRgnApdHYKyvVKoK8Sb-STXCEZ_dbmIr3849BobFcPeBmQleU8bMGTidcrK7xB0XYs6R3wcchowNbBfb0OSacjLaQiUQvmZYsVWimfN-EznnXtG4ngerIKlnMUyPRLV-6kb4uMHZzhcCGgR5_-whKTchVRUDlk2-8UueV9Iv-w96QmfAVWSTF-3NVCGae7FOAK26B_S6K9SYXFqHR3b_IYLDqaeDCBd1SCK1ydSkw", 29 | "d": "JMcE99YyKtJxBND7ft29uLnNmxJQ9iZafkqd_bRUBKFHiymU7kgXEFENWZiQJG7PzQJ3GQX3zDk95-_DCv7AskmCqDSub7vWh8qZ17rEkWdQ_3ZX4RgA-UmJdCk5jQFZ-cgmEXMrHp11wYQU364l9f__Cp60XBG4GuZ275ud_AvL2yRAPXXFNmHYhqOUuJfeI90mkRBrUWEwpyUlvJincQw5Hr0hT4dBzIN9uaWpN3Je8JJv7e30OeQVFMJWd0gGupsR6wLMDROgr2AzhVsjO2w1QnkKah9j3ySVJYVstDhqd1bE8IezA0AcPQOO8uraRqn_LKgqOszYfom70AXHVpXhMcu1Pow6DBsQ9xM5oaL9kCJy-y7eq3heWV3INThxomyy6cQ15mc6G_tVnw6K5c8RF3F3LOs4Hx2e3d6ACd6UG4IYPXD5vmZNqdTTK9bV_SXN73FUfla7lX17bGYCeiiczuv2u57CpzOj0vNJESLkOcSeB81GveIkTF7ka1jqM6aggaVexKSWDVIdWI13gkKbWfqaEq_sde6sbwwkEcIuXe9uIc6iKrv302y3TKb8xMTSYyE6vw6jNDZFy4ZgT2479MdbN4YztkmZEqnz-A6YLw6Em06sO6RwojWSgZgDyjnqzd5uj-RlW0lz6Kw3d73R5mniDautsX0BH2HYPUE", 30 | "e": "AQAB", 31 | "use": "sig", 32 | "kid": "4k512", 33 | "qi": "Vv2dw2DKiGSaeqIdArVMSP1tyapdyI7M6oSp8bPDNK7ZOC6-EBQ8-s1XSbTMIGRLzYEuPc6zytpjDNyg0q8UZKaisA5-n1jE7OIQr9KNGOw9LoVc3QoGL8l8MUwKo2iLkjx0ykr6U-qQX10Z3FUmE4rtzvjR-duRpaL0h6ag8QS7l1MJ8_YFfHlsyCkPO2JB5d_4aqqUELcS3xqj7uIbmvQ-vs7h-yFZPf4UL8EkAXBRv0nsjMkXrTNKDJlUoUXqN5YYMB_uxeSwuL8sV3-tQBb_m6mSBRiH0osuMVlN-U6zur9WSZ-tzbAsK_T6Sg9KGE98AWywhzcLV1i6nePCvg", 34 | "dp": "LSlC-QBwLoWs-JHRsNdIVvW9R9-fL5On6RzcL6Y7gxCJ_oroJjmhpeeJUMX-fPt1yvhDH4YajVrjCNFmg6Kd0CkQxNOqRkh1vzZdu1vHgJPltQDzsV1XS8OGNwChCeTQgKiPc_sf3iZFddhNnigM1Pp2AFsear1YWTWHtB825LeOEsnynIyWIecXD4oQaIM1cqVWJt3111WHE36tCMRlen0hB15SeOrkyREc8OF4Z6Sxp9LxawTgkZpK75GHzCbqkGLIWzCbChwMkXNb2ap3c643DlqGrmXxM7mVX7UQqUfsewp5h3RXLSamsbsJR-0m3SEaGIgB5F0NST1MsR00yQ", 35 | "alg": "RS512", 36 | "dq": "VXdyxi9daqbb_bCvPzYy5W2JYUqPxbRdoqLhDZB1y9EZwQIQCofbVjJJpLkaaeeajfYYqm7EnhUZmUl5acHE1hHX8G8lbMWR-kDWQ0kPSbY7cD3cJERrUuvpe0zgTMYpGy-GMF9pE6fSTsUc1ZkCdBIGOeNHmBW-rH34K6X1dEgOmQq2NjEXse6q-DOXKITFGTEZmD4R8CWnUCyh34cyUq_V8rMttcDiCSELxi0QqWn04WWu21aHSYXheVww9GS6TjjMc8grF3_le4LXv95eGpseLT7ssaBqlTkdB2dfiY6Sbq8kdwiuUvLxrwXXka9q9dgrVc4VNbtx1NO3mAFPpQ", 37 | "n": "rDZRxrKOZTJVcRMZ-Gng81V_d58PAdEKZqtIEnrM4EVKDMBGrHc1WASjyfeLd-VyIdfVxnCKd0akWOyqhaE_TkCe8W2ly_Lx8h6cwmhDiIV-n8cRCW-c0EUd4cJasNEVWRRDfHljm8a4NSaHIQ1Yh-ahl-t-1_G8ey5-GVyCFDnMLj3P47jvB19P-hNgbtqXFAitgwSLC0bJWlEnnQ2b8W8s50_Sk80aMD0PPSdXYLcGtjP0l9loscsoLqr4M3rOOT85GxVreapbgneaQfgb7BbW7lYhRjJyQUU33Mg3fPvztDmVwN8RZM26JjARYpWkR8dU6yLg9arAVhTSTgWMfMxF0jgwT9lBZdm89-wmmY_N2lvuRO85cN4kqYpeH1PDWJTYnwMSsbRezXbh-ruFA9BhDMwj6aXYgvIHs6ilqLov6V2TGhiGDi7tw2LGnl7Gz_ONfFSskFAUUJd0ydcLdep4U9y6O0H5tEGezxc47QMntHjKiEtB9JVQnvx5PlaY0B1Tx1kUecgoahSS12OIOm0SKAsfnQ3xAOhbWL-8BoMfg7tRBBS38yk5mzk9AYqDKsBLiT-C3aCNU5b9VEwK5xVjhY8px5rs_nzQiqN0UIGQ5R0ODt6FVJ5aFBx0SWZPRuDKsLSZMZ854rjLQGVlO6tAFyD3uGa_uURdV_0-z7s" 38 | }, 39 | { 40 | "p": "-_CSaLWVQj8X8OP9jwGsWPrSCR7-abY6D66UW76h9Hagr7bKXi-STgoXDVs1LUvppff175D4eqW_doCeFl6gOk7xwmuAlBmenm5ZK4OfhIQOOOx1hfmN85DaYNjlaQOE_rhsyc_zhLJpWkecGIb0uugqIlbZcmYGU9XFxCpuPBCLEnIonC2lA_bcylg2eCfiMiPqSxstsj2UL1pptHsMu1fs6xjcwYCWqS-nGIszEKm-BU9EO43MtvupVuZUM0M_Bjg032iBqc5zNbW8ePrj9a8gfoJaJQnL8G3SxWivWZwJuWZXkRwIHH4vvf0K2O4Qe3ndxScatMvmQ-wv-l_07w", 41 | "kty": "RSA", 42 | "q": "22vuqRZdRJ380teV7CzRN2HlYIPQXHLbLRNBWYOzJPzcH-Z-o2iWpkVN0YpQTBzXIGyoyV_tYTI38MobCzt193YyYx0cKl4rkI1E0zOopg_UvMKpXFEGmJJeVJrKyp7Beq-cFnzEFKdujta-CJ9R94x-7oTsWp5jgLc7qRqTWKdXap_lrtLvTcAUXcXjCe2wivi8mS9cnUi4Lx3tjevFkgRjJwngJ0unpYtxnkqMjFIe5kRaOZJVgTp7KWzhL-AoHTZEL2oPrVg8G4Rqm_gGEAAyrUkYTCpy-xzm6DGpfOLVDGaychZNybRnYtB698F-WbKD0hkivx2UPTOH4XZBNw", 43 | "d": "epNc5lN0Vo5Lcu4P0u6HQtQonaU4_Mu2iaKP801W8IFv0AGbfD_8TKsGkEnagOk1noYWLZIgjURvb0FPOMBq6g-DUrJhHYvG3CgXcMy4waY0YF4XVqXvBlKMgXaM631aFmTBv6k_imm3DJd91UQi4E1yhhKFVysyFKYX_jBxQh1kUTGosqf-6Hbhn59GerNBRg7_FnXLMXbZACpmbFP0p3Zir41P0X_SBhBNUoZvhD7CsGEuZkAWABr-5-dg1VbQa7pVq-dnmo0chvgG4ZBUcqVtEk7zuJBEDNfh4c0YeDDSD_22fPFBXwAa097qI72MkyhEjPef5ujl2GNwS6e2pjnVYKahxWFWZPrn7grgaURBH4g4giF0u8YmPTs7iBu6ApBGZfKziyvqKNInU9Pv8dfQOYCIyDXYMpFV_nTITAfe-YT6Zibs70L0YE6se7gQwCIR08z0jtYq6ao7UvpaDaRonyuuyPtJrgXCdZNQ2Ebs7rEjk847K6uhOsx-Fztz_v76QQ046H5DfThJiGvSo3ghhujKSyGE95KMSxMmRLcBPspAwzkBwtuMjY-JFD5cZh3Eh1n0cNKUSTUa5vHeoSOLRfalm3r2X_SE0PuGlEa4wrGbdAL_Ok-cAQqm2OjSVe1qQfSyDSaSLWmseHt2Y1WedpEwM_5MQeLjkc8KHHU", 44 | "e": "AQAB", 45 | "use": "sig", 46 | "kid": "384", 47 | "qi": "GKE65EngI04c4f_2_lAvZfcpL_Q66IX3NNC3wIRgogXRtZHxJx7NwYioIpuOD5G3MwYrJ4G6m1zPNnwPaKfrbDXHzXN11QlrpCxBqfH7DcWJWYItgUlfmvFhA1fKPQN-QZOTXmT8xb03RgIp9P9atnv91RGWTUm-iZDhmN_qe74ntM0Z7VwR-muNOBq2O9UqCstNx8PNNwyZVc9_7Mk0tr1rfJZWEoyNS9-mUeA4UfpzMvSoX0pbWEjluGjIzsjiNotiuPMqab8sfb7EADTz0cG4yZKFeCobR2wuxuPI4W9SUrbi58F5tyG0_ZP-kK_KRCNC6GtzXNmxGVNkfPD0Gw", 48 | "dp": "I0ZTuYVzGIts9rV1UwiQ8QRtVAma5YI2Luvqqc3PL0NMzE1zmCxg97xB7gTPNUBjvnExZhGOON3sKNNpTwiAtqlHAEm8_v93Tgd1RLpxI0S6GQ5ChjeiwG-Po--6bzGm9WMtGDSdi_7O8W0FVgnPSjwnahdu6q2ORT6xKf4m4RIP5s1FEljxudRepZYkSj6LVSniLBHqHAKzNM4b7sErzy9IZMJeZllyvs2FP9J9m9-oU2X9z7jS6Ovjkd89-s93i49jF8SyIuxdBTAk62t1b3jNu-jUZksw2sEFuc4mfw1x7xt9NzwNhq_ET32WfzkV5bPycSBAhP_nhasULKIy0Q", 49 | "alg": "RS384", 50 | "dq": "ONOaKsZ0_0lfageVd2YgBRTgf_-DXf4ND352JUW7hq_KHqTmVVHH-mXmgocsxpmNiYM6r3SdmNeVr1HYcS9EHQPKhurKGZrr83LyyTQO0Hs3IG_VxufhKnB157lzHcjB2RFT_mrvqV23f1zUOI1ygAct9H4ObGWq5XIPDHuqf4JPXHju1jkg6uT9IVAXvcGGQGetCItAlCd6NBGvLVtLfsaOG1UubV7lmzUjBtjNz6WRBRfcuh5Q8LgBn7foFisamH4uaI0yZDV-062WorA4ebpjZYeRuGAiCTDyCF6cQ5DHln-eZXerS7riL7BIuJOvMnHOPAJwgkiZ1MPcxCKJPw", 51 | "n": "1_EFrNULC4De3C2hOHcl8n6TPGlxeEJ4lmkQfn3S2ybnTF_Jl_YYKJ-6mhSYUjf23Ac3eD5AT4eiDRgDUTvqTIaOq2Aukt8y0daclq3hLFutDcz6cBvlPxD7gaYFCZ2JPJkE0CVR6JkAF3E2CDroLBle-4b9tNDnjIBi50ONXwgWuhOZDe0WJ6Ldq-ZqRzxfHKLMKFwA9VO3kmoP8VFBzdrCFh4DTwgtyffqttc6KRkzIXpPloJoJX9ZiraLcEiZOl4UiUC0JQADqVn9ImxCqUjvecFSu73xBQeqBRlcPfhggyIL1ztljXXKQUpMzWzqjhwIiqZ_cyBwGGWhl8W3LC_JTflkCUjoV2vTzj3EW4hMuOiGXmX58GuZkRFmt6pZe_WrRR5srLffM9lKBOpMmWImco_cNdHTsF57AmzJsR6cpyZruXhDWpBzUP9xpn0JFJoTqLLvYLpBg0x7G6eWhYarPmVeHIRqPMM2JNLValI7jgde5mIzrpbWJT5J3plUm59QlOfts4x9XFQFI2f_LQHkkVPEPVwqqheaZR1t9OsOviFzbyAzceFBXOkrwJ0zm1AUOpNK8TIWcdv7IA9PtpD3v85xXCu0_M89geZZ2opzWSkZIUxZJY26ZCw1aZscEDqBlCizd2xnUQnmeCell5KCspWHasHjcBRyaxz4Tlk" 52 | }, 53 | { 54 | "p": "wvIw46ae6crAT6QmX1fhOFgGyQRs8KEOyyFU2Ery0sTj-0N8oBcjPC0B5dExQb3K8fSrNzcRekIy8T66pEIClp4gjVfORmYNSRmahk17cMhKLFMtMatSb9z_eDY-hLx3W_r1YTKTbNQs20AZrdB-Jkxvp6AmDuUv2DFnETbCSpH_MILPLmpuOfgiJVG4hFOalMgW842A5tKc3zKQOuJu5eLwLsSbt69GbzjDqYZzgClBXMKeeBZle1MPv5JzBp1NVj390ohzy6EcipGAFkNZGPUGKjm8yqmRNOnBctq0VQnlu9pveH7201yW7kgPHT7dsnAw6xTFH42s5bg4b4_wGw", 55 | "kty": "RSA", 56 | "q": "vIZBZ5oQaZrLV_wKJ2jXBoyv2BfJMy5QdszhzBN8AAfV7QYc-T3d7UgfUOCPO9-Lh8GyhnniACSNCjNKYbe1-tJor4gwUwYnbgaDYJmihqBtQOUBcg5aPOzlotPxvsfODFWANXbORGGJf75qE6a__S-ijauEr1a5rNH0xGbYKZJXeQ3lDOhYHyhUQZH-4qIBoqWxpcIikauhaCLMaKhuSlbfJyf7CSeS_-9xTUvUQJTAGg89swYdr0NOSpcW375xgxkC9MY6heaJ20eYTjSFopMv0aA-1A3e2BhwiATdRZYBLCxxjqbJqqr4EmodxDTtXEf_D9KbmRYOaK1sjrFA9w", 57 | "d": "VxaGg0-xgK0C2FFNlmNVEmPFJUHlRiXs1lWdrsAZSTcViSoelRmlU7nqiAoRBpxnRZByHcmZc77aDlVnQFRFvE3TgJymwc3hqPQOCGmwAPnoL5-4tZaDxSHoV3DMTgnm72E2_RAZwQ6p9Jyx0-TyxJGQ0UawFrABj5rBmtZsMfAQ-bAzlcdfn-tRVlGWdvrMGSGfexH2Gokh4b_lo9_Kt70kPj9koFGUK2CJX8zyDBm9z-GnusZisWTtG6rX38ItTmpCxTW9IVOI9KsifmRoQXN7oCUtG-oiEh2z9CHknqZeCoz6RD6S7Qdbb-Qik7iKevfiZTNFGzRkoi3_fMqxWGkieRh2cznL6nGT5YUi2aoT4edPqJPFuAnqXgGe6srKCCGssipVuAF9P7PIJoXYNgmbwsQLllVATiEbaRTseW4vYgirLvgY_XRNOw-bUqgMahW6EgvTjwwFwlW0aEpBqiIGanYbXhvc67yfRfKPqhWxZ526aTIenB9CPvLFQq8tZ1Nbs4L4EDfX3DNFZ0aug7xXZe2x40KqnUJ5jlbdtIcBgImyHVJIPH0tBGIJpNq8VOqlXCMm5R-uMl2IY2br_mAQ4zNOjhZv4Af5MMGAebqu3y1hSUPX11rrT7yg5ynz2pWZKY9h-Rexrz6NoItd9LhAXFxgVbG46B7bNa7MftE", 58 | "e": "AQAB", 59 | "use": "sig", 60 | "kid": "p256", 61 | "qi": "d3U0hkO7UU8Aru-cS_Fnr0l8XLUYpu70rMouug4XSKwuLqsh_UKHTidVun5y7STvAjS7SMhlTVJ5uSv1Xi44slg0EOttBwGQgLrtb7-IyT0ygVUNiy700zVXou565ExiI2rGO81O70kEV_qRbioPVL_eC4zHjQpszuRWaY2XMDCpqAm8j37BdyAt0qPJSgLJdfOSxH4TZvST-WhOymn7ytuXPHYlUf_-_Iz5DaIZyDkRlzC0YJSfM-DfTXabPKPThkQYzNwOpwVA95fycV8q-vkC4A0fxtfYY_UEJPZ7J7uvVncsiTF0Ujiyg95iNrw5HLSkDjQzX0zd5LHxMHSNxw", 62 | "dp": "r4KSx3JMYhobF9imf3JEH5EI0tO8LMwj4Heqxmx7v4xp4N56hjsuis_OhJTZLuHro_huaXCXuKV-7blXGekgIXBNyFMEMFdSoLx_ZWi-uORp-wwLhUtEFdg23IihsqlnIJWJc6-wEOvZUq-cLuOln19zqsvBV-m5MIAR1eqh87cLtCYg1x4VDPW78DNp1yDZ7BdUqoN8SNPXTg9c0NbplujA6qoxs6cG172YUrjspbb-1L-XInCvft9iL0xvaMYYSYvYSEJ3Vh4_7vVeBy0tOw3mNf_zW9ZcF_JYKsqCTolxnrXjY-2S1OnoLLENgnwF5s-hCxSRAH5x6gP4M2C4fw", 63 | "alg": "PS256", 64 | "dq": "mQ2OD-QeKv5G09mFDNWqrGCPaip1aB3TqX9QWXQAGa5C2Tk7UIYPpFIHSovk_UDRcJeqrk5JUsBZ2MwnOQoCre0gnFE-mkjGviZ_hm90aUPZLvQ8pjJMxGW3UOgsvSxNden8OmuHrjBZhuI6EFYyTATePZHgGNGZ0FpsEv9DwwxSA69qI36F_FWrgchbW_cWyMsoXGAt3IGwenC17sm_MI8ygAGPZrFAQJROMei8Guoow39YIf13IBDOgPQE2AUu9rXZlotQe-DcTq_jUKqHS6OfENq95z6TinDc-zCny5SqwwgZ8SwrUWmYGKsaG260vVSeGGRtem1TqVwSZA43pQ", 65 | "n": "j5AYeyizlLzftYmA9sJVf7wnGx222Z3idVBPkOvH6NIrh_g4P58eCqiYSeapb0erHgFWWiyA3xwwnH8sUDr4qbX5CRffbtzdJKM9-YHlVTDrlZ9chysZtA2VX8xNx20CqUbY6lUYCfHnjUTj5xxZl9CJ3tJXZxs6FG1sjP6g7svB78fU-PzDffIMOCtascikWw__xcxWujgTcH4niSPe35IlUXERBgqW3ZfROiysu-WV5CCbI00F8s9VzQAmzpXWBQus9KT2shbIs2YfH9q_GqGU0n4Jo-M9-SWrlHnWBPbwHR_-3-LjnPPsQ-ScPeOARG0XFdpOckSq3RCFY2kngvGEDTC-5Iqj4RnL3ilpUW3iZSeFWfBI6zGQgjAHqOqujsMmrByxTq-Oow_aBm8hhcb4NCrxOvw79p9FRJF8bYDU3Ilhp03GFR6ihV-q4r8s3uZ8J1zM7v8w3H_RveS-OiZNZbm6lrifIjXfL09YrFYYFyXhTxOx5OvMnQGpYjqMslzUGmbTCj5J1Cb4fjjQbrjaUB02TLlpxBFxIh8S6MIGMJBH-APEKuMmq-eI5CB2mtDApX1FNdwtpu47x931Kcr9B1kNa98aiHD7o-kMTam83tLFmaxFnKDfIZKCdJgYFpD975WOOdGuRxCK0AhcliqlrAf3l6YPR41eb5ySag0" 66 | }, 67 | { 68 | "p": "3pWLqWlii4bd8Ie2uXl8_nzZKUpwmt55VoJRcb3-zY3-PR823YNByVouRgUPwhSH76Kual7ZAsCOKyqRXnqTFnLvGOuJC9zGi44rB7riuj0cLAKy3-DUpGKhjpX8kNJ7nr2hurqCjrlicQIU5lmaRXHKbd4jcEkEEIWbVmGKbYIWq7tOChCmvM-EuYlzAckjw8jumg1FmZ_631HGCfSLMu0755AwULlEJ5DuZ4-wB-ezCUPt5IwjIcg_S1MBYNdSMVoSu0yaiwU5jBc6YxlJ9J10a7VzYrWdslyMQVvOCyLJcuSdgoyL7OL1aIl-HiXiGI5HfPNEDYjd3wUY9Fd5Ew", 69 | "kty": "RSA", 70 | "q": "oqz9wwF_hlMjTyUAXZZ0c13KoDH4LNelGzkazpRYcZ0gylcN2NlKMoj9KMYaqwHTQfImubhmTOfsXPAZTV3lx8f7pLsbsFCnyhGYzxfYgVOe0IEdLeGiw75I2jXeEbXN8pXgG_-5BU9_GF1BBNyFygGALLh7lJpZ1cxWBhJAfJi0Qerti4xs1xAWrX7XVZKFODj8MnI-mWu16fxC-MNGGDRogjBn_OLhB5D5oosWnY6KiOeB7j3WYlwitNKxVwXrVuMKx9-au8fKtQ03BV8OWBOerjpbuIIwg5hvnlBC685YoBXwYQowyMoT-agf3-jmHvqb8SYe2Qqnh4Zqx2ijkQ", 71 | "d": "DiLcOTd5cD3o7dCU0hdPtYNCHJQ--wHyHISSn83K78D84HVJZzCvZREBzGv6sbgVaF8iIrr8CGMN4wNfpf9aT53_QSGKJEF9yQjCf-3KMacn1VwCQwUsfG6WxtWoN724-5sfmYF_kSfNAOGnipf45_vsw8KxH4faSMbJxj4n6SQdi9WzyJEFwe5idZ6w81bmqFyRuySRT27la_QvA3O8OG8Uz_Oh8GGuktfMg3hSxJZV2ddxzPUlXKFwVl4MZTqbitqluyNVhhJkRUXXMKZEmzeAwQSuPW6fFLo1w4ayzLdxAUERdnaGFVymhAVCQq6EBmAN6X0Y96xfvssPJpgzOxclMfiof905G768RIdeafGx_GjzExvKohP2BnBOFqr6AngNaCNNjvZN-VfHFBm7Pa-0AQ1MyZ3zQezYi7yfDoikIPCc_9wXvTdPtoyxh7cgg5h8SDN5YP6bFGbXN0tABJZDxtzJjMvnDN6NEGII7EcBqm3nMNEGdMK0KqP2H6DqtHSdBUwid9VNkFP0GGW1yEM6gylsC1QjF_4DAfuBQUw38M2LVhz7CIOkweJV2Vci1AuyhRasqvlPddEimovn2KN0CZr7QmeiPP-pCKu9vlzA3JLzWNhjEID08PkxSurX2t7ynEHoaHpenYTUbUOwO1jG4poPoIP6xeaIZb_iksE", 72 | "e": "AQAB", 73 | "use": "sig", 74 | "kid": "p384", 75 | "qi": "SZ5wu7Nog8mu5c9UutqPgUNB42IQnAkaBzX5OAwUyBys3L2LhzJsS84HPgqqgdDLBOYcJ5G9JJhcmBCHD2n1uWQWkiO3JhGfwCcEyGU-NK51VMiasaaiRlhT_mug6SQWeq1F8I16n_TL9gQHDJFxI-EX4ei7OqQIUcnn2ilMiEa4tq8tKsc0jUn0n2a1VqKa-qGF-OuB3ZBDPYV9XSljUFUl8G_2NkB2EwAJz9W25NtCampyZihg8TYzYe9t_F7ait_gM4XqHHMn3mgwH113FjGCJvoGeXFbpGDLeutHpXxrZtkXeUCKlAUtbm0nCn1iakAPbMJS7tuyCp9STe5rMg", 76 | "dp": "fkLmjpsxQ2Sl0SOrC1tXBCVeKoYHilJZEjGqcZMNTx0U8ycZwF4Mm2OEjEOixL1QvybROZXEFEOWYfYrsCRn-3wHFWPGwevi1Jc44ZGpu2ue53hAb67h8L3iVzfNpXlAlrOLYpkaJkwTA3fi0yhQ0sPRyLER-Ufx_k1L6Jw8IhKBObluc1cuvjwZVgYaLxEiMJVyhACzUonljXidllgZ_jGEob6xKZluh-M22ZgwistsS_00nUjBRRCKlvfm-EAsZ8JojcbOg73a_xTOEhjuUsxelhq_8WaEpgDJrumc38RD8eCtWLjRRvcXrs0PtZpXvwY2lzKHkAXrle8WUMSt8Q", 77 | "alg": "PS384", 78 | "dq": "LGETTZrgBm4x6MqIYf_EvyBsQe2_7cPa2CVpCchwpHYgVvMO5QTKeCTlI7V_2v1Be_Eq3WnQBtUSp3wc-v1NzYXiQduFv0ERtU-9p2my9_0vC2Td6AFxXQNRbq5Zae12ROLcY-cMu8UnQCdYsJHUsfpc1FZz6GN_dJMtOLTMAPHL41vK-FIT80wBU82Rw1eYnnD1ZHHEYCiBVVcrkh_7xGivxiycgeIHUAAeClj1j4AujRDTYoJeNTE7iqccGEiWHnsMGuEGnonv5Bq50u5OxHp9xLxI_3oM8Cmq8b6lsxz1Ep9Jl7-m9XAjqO1T3Mr11Ke72Tr0GXQNpixmfwYt8Q", 79 | "n": "jXELfmLAcDlDwsZ-RHrzSnQkk4RSNBiWpdIo-YdCbM6LWg5Mn4tRcPfngtgtj2PxUn0CpwKwAnc3gHqr3eE2wWZHGUd-x8hsmIbmT0YLE_xOWCrSO9rF0-CLqeA6DiVkf2r7GZqUd2p4j8bMLzHF_2g6JQfF7g88SsU9PuwmvmsJ7OXMf3H7RN8bR-GdbnIMAlCYQohXtFds185mssyhgGcS9P7mwJdK08WBVuCV9jGnXU4TZr_qLrKSyrRc_dm6VNHTW5OHXRg-1Xsh8ayQ0zbLadE6AUjLXhYkBm2GqRYwVcDKR8H0vUX0nRvWuZncs_IY6u8iMIQ8yDgzV8HZhP-DJazsqYsgyOIvsia7krWfeITBlxbiq2d_A8DtstKypIrbgNnqgpkypxWy2cnY0LeMk4fnrcqSgjI3TIZHH_q1i8u4A-h7pmwENJK99GYtBDDN8VgAtNidegAxUkADjUWsgaBWtWuxrP3_EbicEYSzNfNfTm-3HYjeEoJkxLOid7sh5BTcoDFbuSpj_r7H__JYTtnpdpryhoprMQ15tJXUh8iTUoL3HOy9SSqwsHwyH889rj-C0_6l836PrnBbHKyc_ZTfWFSpZZHQHNxUyFVqinTXzbBb4DCysdqaA4ZpCBNZa0fnf-znvIXWFhAYR-eT9sZIEztwV0EdOwxarMM" 80 | }, 81 | { 82 | "p": "1yGRnCVYFDkmKIf0e49H4PGJVl6Y1ElWmIYa23_jhVTMkUvgPeO5IRI4TmFU1K4QYMdaLnMmtfy_E_j5dkO3sfHnETEXVkkxLErND08WD8vlmLVJGrC7ziRQCxN_28f6TPkb0AKXvU-t6Eog7TgolnWrSSxMMHTQEg5T7ZLe1P0j6SFjabC88NI0N0dL9UJokNObXjmIN_3AZF10rsA7mQCLJl3Kl6XFzeXpv6X0_PPefJAFqFWXQodjHtvWrHb5FDNVxahi130rldNfzy-06AW46i2Zmb1t-4svn5L1twvt51RVpsiKsh6PZXFkRbUPMvV5JRaIU8cEdkXQ_4BIKw", 83 | "kty": "RSA", 84 | "q": "tlJ3hZq9p2CG2aQIfeO9hRcKwjAoUoEfMYuu9w50q7t3r8-YdQECZhreLUgko_8GGjjsi-LtUrwKtl7JWKgEgjGajs-ZuI1kk9MvE2pyqwFCzjJzEX8pYbCh45ikvOUd7ykTzBwRL_2M1f9Sok4nRzsR1yVhmzgUxSkoVgG-WKvJWHA2LFjRjUSDbezBLMqv5UEa5y03kiu1ZKDvuOqlcJPgqlOk5sHtDF0Hd-7iYaNxJZSiOa-6COGZtTquTwdhp8O9BiZxoKn0r3duVQhFzCgmWG-y5o86nV9QtLyPTCTiZbKrxrqaEokQXTDjum8lRaFoww1pUHXV_S5MXNTW5w", 85 | "d": "WwpayQcxiuXs9QjOTrwP05pKvMKGBehV8Jqe5vx6mDxz9u1MWt0WiV0OaGJKpm-IkyqICzkzbRJB8UVhQDWDLt-mFacr7QHaK2S_ReVdVW8A1A_5fstKT6oo3fHfZY72QaNWA_K-noRctpWwsXX-WXsNPPVDpUTeMDQnPYP6bw2siMl71uPqmfwhDjx9JSnOO6mbGg6x4Mkxx_ZjUHQ9hGgPLM-3hEoLIZnAofH626OuuMGgT8ShUlE0nbwI2s375A1FjadVPhPQ-eKQlGgmAwejY7eorUev9geaV-tcaPFGtkDR7VEvbkbaMA13y4xWovLvUQnGmirdpPfEyQrAeQlGd1Z6XEAnAdL4ofXaIfv54vYi36yTaDBg8iSV-1oeEskkVnvGWqWcvhSIz9p4EVuM2pEuhQVzvHlUtOU0693UfvxyEg5iakUrmS75LnDGGE2-dvtLeKtQSymONBu7fM3-USKLzrwsp95ivrp2iuBw87TQHJJc1Xk8SE6pOO9FLneVJGhexhmhrvvaQSoGS_eN3rDpj0uvOz-sixWL5GEhnv_a2jR64zMwcbR7ABfUtY4LonvujYv6cyHsJ1Ip2da3CFYVuZgVboj9GhUUsvguu9yFhmpInkwDtxeOwocOd6p_9o45EkryeoHNeIMDfT8-vo5MKeoGfY6DCuZE9Ak", 86 | "e": "AQAB", 87 | "use": "sig", 88 | "kid": "p512", 89 | "qi": "glt7b3sDe1KWnEu2PIxWFgvaaJVRgmF5NcIDhZfc8reRXP3_ym-2ORITyWhU8wiWo789NiJB2b9V2FU2U-vKO1bhMS40JrL_u_iSHG0c4OduAg2sMky1gJUrwMn5ryg2zg8jIfp0pE2_VzrhaHmHovO_29ytYhELrtZw1cO8EzBus77Mx1aDQPBmgDggi3qDVHRVoaAfmdFV7UzjbGujdcP_kZTUzc4SakYZKMSTcMi-Ie070LXd-HBDVinTA59wY-3Yw8lF9xUWvlIb-mIaAcoj4vlHgH_r08Fi7R6UrwB9VgfBnsamfHoXOLNCiMRLOAkJ09oMYzMjW3xXXql9nA", 90 | "dp": "i7J7uHazjGZT9gva4YV4OoT-FrzU5z08Y0zqEdEpMIKRWGR6GBZROD15nMQtbvIdJostb25NG-4lMlYBwB9XINEhqBNFwT1EFiv4ntFPVTClRhWqhA9x1PnVlGu0IkboraUxv1B_smoUVvwUB5bpHFMKyDhgvTKNjrwJGgO-eXZsJE4PHSmsd4E4cRD2LSxTdIBEqtoXL6yyizn4tCWWTdOBiIdtMC5wlTSQJmgg_0VpG1_2nIn_8C60WeyNqW6Ubd6r-u9OR_o7HE6nL4YeJnDY1BCKZsAQtEB8y1WGA7hWY1prSCN_lQytN0V7xJw_Y2rtoPzRx_07U4YgolluAw", 91 | "alg": "PS512", 92 | "dq": "BdQHDBdRPruANA7hUpzoHwwIhQKjF1gXVi8f7bfhlOfOQKmDNyJMIFV1ir8GpNNvL4FoVmRvr7hhI0lOaG4ejsblKPGL-XFTqMIUfCtn9P16VDsaoJGPEhdiZXMouP0eAtouTtUK25zgVqrtylBzQvypalZAk4SsbU0OhAlUO2NqcVBM-wfv0vNAvE_YMQVdsBdvs7onT53kE45te1zM4xdnFCrOV64VVNLYfUX-qJ5f7JKeyPdMHCKSFlAE6HPU9Fb2gO7TrQ4hy0YnGcLE8GzMJQGVF3e2qPc2DkEE16tKLXQTt6uZZ0RIaKI6dWg-KcaqIaoZFsBdkdVcDY-kOw", 93 | "n": "mTcqtoe6YhrmPJixmM5AqJF0tNexsd1_0MblYugRfcCGIR1GfHdWmBcw7qmm_pTMwS_x1pe-F5Jk0Fx4u7Bzuqaq0wDkKtjajNujPy3eEiVoN1S_XuR9L6MuKZdaSCuff9ibZtwi-zh9qMsnh5xJQX-JuQt7Yc8-moBluNbVg2Lhry5MIMM6S-gkxtSfGUa3w0Gcwi2LHO6fVQcU9os4ryowO1C-Fx-_fqh9dDQzrmv1Z-H6QB1aW40iWR6qGRrF_gkVHxLwTDYcJbsl82jDm2C-dEXDvKIeuXWb8UJpV4JpDCiZ-WuX0EnkuW8IJSgMnf5pLpA6gdSjMFqFTBv9CITqxvBnoa5Q8TgKi-Ft0EGMMZfTrH4m1AE3-Q8uYOwjo6La6dy_kQwiqN2NjRwCnOMyzcuHCrZNswMpe3srHUqpxtCQdzX4E5x5AlhbWlS5yPzVixcnVfk1jtLSQY97s7rioxr9GiEIutymLT3BAysdzObqLPob_cur4Scm6AuAtMCIuaMZz3Hzsh7GkD6J7vdIaRyvNTUVPrcSa9WCoh51WpaUPBhEbyseLIGlug4yongeHJe6x4yLSMW51gA_Y8v4wAZyhHbHcNRqk8efx_xOS3YcNQf6W4DG3W_eCrX3oCxdq10gslSJKDkEJF-RX10mY5BOEhrH0oeMGwCxEM0" 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /fixtures/public.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty":"EC", 5 | "crv":"P-256", 6 | "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 7 | "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 8 | "use":"enc", 9 | "kid":"1" 10 | }, 11 | { 12 | "kty":"RSA", 13 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 14 | "e":"AQAB", 15 | "alg":"RS256", 16 | "kid":"2011-04-29" 17 | }, 18 | { 19 | "kty": "RSA", 20 | "e": "AQAB", 21 | "use": "sig", 22 | "kid": "4k512", 23 | "alg": "RS512", 24 | "n": "rDZRxrKOZTJVcRMZ-Gng81V_d58PAdEKZqtIEnrM4EVKDMBGrHc1WASjyfeLd-VyIdfVxnCKd0akWOyqhaE_TkCe8W2ly_Lx8h6cwmhDiIV-n8cRCW-c0EUd4cJasNEVWRRDfHljm8a4NSaHIQ1Yh-ahl-t-1_G8ey5-GVyCFDnMLj3P47jvB19P-hNgbtqXFAitgwSLC0bJWlEnnQ2b8W8s50_Sk80aMD0PPSdXYLcGtjP0l9loscsoLqr4M3rOOT85GxVreapbgneaQfgb7BbW7lYhRjJyQUU33Mg3fPvztDmVwN8RZM26JjARYpWkR8dU6yLg9arAVhTSTgWMfMxF0jgwT9lBZdm89-wmmY_N2lvuRO85cN4kqYpeH1PDWJTYnwMSsbRezXbh-ruFA9BhDMwj6aXYgvIHs6ilqLov6V2TGhiGDi7tw2LGnl7Gz_ONfFSskFAUUJd0ydcLdep4U9y6O0H5tEGezxc47QMntHjKiEtB9JVQnvx5PlaY0B1Tx1kUecgoahSS12OIOm0SKAsfnQ3xAOhbWL-8BoMfg7tRBBS38yk5mzk9AYqDKsBLiT-C3aCNU5b9VEwK5xVjhY8px5rs_nzQiqN0UIGQ5R0ODt6FVJ5aFBx0SWZPRuDKsLSZMZ854rjLQGVlO6tAFyD3uGa_uURdV_0-z7s" 25 | }, 26 | { 27 | "kty": "RSA", 28 | "e": "AQAB", 29 | "use": "sig", 30 | "kid": "384", 31 | "alg": "RS384", 32 | "n": "1_EFrNULC4De3C2hOHcl8n6TPGlxeEJ4lmkQfn3S2ybnTF_Jl_YYKJ-6mhSYUjf23Ac3eD5AT4eiDRgDUTvqTIaOq2Aukt8y0daclq3hLFutDcz6cBvlPxD7gaYFCZ2JPJkE0CVR6JkAF3E2CDroLBle-4b9tNDnjIBi50ONXwgWuhOZDe0WJ6Ldq-ZqRzxfHKLMKFwA9VO3kmoP8VFBzdrCFh4DTwgtyffqttc6KRkzIXpPloJoJX9ZiraLcEiZOl4UiUC0JQADqVn9ImxCqUjvecFSu73xBQeqBRlcPfhggyIL1ztljXXKQUpMzWzqjhwIiqZ_cyBwGGWhl8W3LC_JTflkCUjoV2vTzj3EW4hMuOiGXmX58GuZkRFmt6pZe_WrRR5srLffM9lKBOpMmWImco_cNdHTsF57AmzJsR6cpyZruXhDWpBzUP9xpn0JFJoTqLLvYLpBg0x7G6eWhYarPmVeHIRqPMM2JNLValI7jgde5mIzrpbWJT5J3plUm59QlOfts4x9XFQFI2f_LQHkkVPEPVwqqheaZR1t9OsOviFzbyAzceFBXOkrwJ0zm1AUOpNK8TIWcdv7IA9PtpD3v85xXCu0_M89geZZ2opzWSkZIUxZJY26ZCw1aZscEDqBlCizd2xnUQnmeCell5KCspWHasHjcBRyaxz4Tlk" 33 | }, 34 | { 35 | "p": "wvIw46ae6crAT6QmX1fhOFgGyQRs8KEOyyFU2Ery0sTj-0N8oBcjPC0B5dExQb3K8fSrNzcRekIy8T66pEIClp4gjVfORmYNSRmahk17cMhKLFMtMatSb9z_eDY-hLx3W_r1YTKTbNQs20AZrdB-Jkxvp6AmDuUv2DFnETbCSpH_MILPLmpuOfgiJVG4hFOalMgW842A5tKc3zKQOuJu5eLwLsSbt69GbzjDqYZzgClBXMKeeBZle1MPv5JzBp1NVj390ohzy6EcipGAFkNZGPUGKjm8yqmRNOnBctq0VQnlu9pveH7201yW7kgPHT7dsnAw6xTFH42s5bg4b4_wGw", 36 | "kty": "RSA", 37 | "q": "vIZBZ5oQaZrLV_wKJ2jXBoyv2BfJMy5QdszhzBN8AAfV7QYc-T3d7UgfUOCPO9-Lh8GyhnniACSNCjNKYbe1-tJor4gwUwYnbgaDYJmihqBtQOUBcg5aPOzlotPxvsfODFWANXbORGGJf75qE6a__S-ijauEr1a5rNH0xGbYKZJXeQ3lDOhYHyhUQZH-4qIBoqWxpcIikauhaCLMaKhuSlbfJyf7CSeS_-9xTUvUQJTAGg89swYdr0NOSpcW375xgxkC9MY6heaJ20eYTjSFopMv0aA-1A3e2BhwiATdRZYBLCxxjqbJqqr4EmodxDTtXEf_D9KbmRYOaK1sjrFA9w", 38 | "d": "VxaGg0-xgK0C2FFNlmNVEmPFJUHlRiXs1lWdrsAZSTcViSoelRmlU7nqiAoRBpxnRZByHcmZc77aDlVnQFRFvE3TgJymwc3hqPQOCGmwAPnoL5-4tZaDxSHoV3DMTgnm72E2_RAZwQ6p9Jyx0-TyxJGQ0UawFrABj5rBmtZsMfAQ-bAzlcdfn-tRVlGWdvrMGSGfexH2Gokh4b_lo9_Kt70kPj9koFGUK2CJX8zyDBm9z-GnusZisWTtG6rX38ItTmpCxTW9IVOI9KsifmRoQXN7oCUtG-oiEh2z9CHknqZeCoz6RD6S7Qdbb-Qik7iKevfiZTNFGzRkoi3_fMqxWGkieRh2cznL6nGT5YUi2aoT4edPqJPFuAnqXgGe6srKCCGssipVuAF9P7PIJoXYNgmbwsQLllVATiEbaRTseW4vYgirLvgY_XRNOw-bUqgMahW6EgvTjwwFwlW0aEpBqiIGanYbXhvc67yfRfKPqhWxZ526aTIenB9CPvLFQq8tZ1Nbs4L4EDfX3DNFZ0aug7xXZe2x40KqnUJ5jlbdtIcBgImyHVJIPH0tBGIJpNq8VOqlXCMm5R-uMl2IY2br_mAQ4zNOjhZv4Af5MMGAebqu3y1hSUPX11rrT7yg5ynz2pWZKY9h-Rexrz6NoItd9LhAXFxgVbG46B7bNa7MftE", 39 | "e": "AQAB", 40 | "use": "sig", 41 | "kid": "p256", 42 | "alg": "PS256", 43 | "n": "j5AYeyizlLzftYmA9sJVf7wnGx222Z3idVBPkOvH6NIrh_g4P58eCqiYSeapb0erHgFWWiyA3xwwnH8sUDr4qbX5CRffbtzdJKM9-YHlVTDrlZ9chysZtA2VX8xNx20CqUbY6lUYCfHnjUTj5xxZl9CJ3tJXZxs6FG1sjP6g7svB78fU-PzDffIMOCtascikWw__xcxWujgTcH4niSPe35IlUXERBgqW3ZfROiysu-WV5CCbI00F8s9VzQAmzpXWBQus9KT2shbIs2YfH9q_GqGU0n4Jo-M9-SWrlHnWBPbwHR_-3-LjnPPsQ-ScPeOARG0XFdpOckSq3RCFY2kngvGEDTC-5Iqj4RnL3ilpUW3iZSeFWfBI6zGQgjAHqOqujsMmrByxTq-Oow_aBm8hhcb4NCrxOvw79p9FRJF8bYDU3Ilhp03GFR6ihV-q4r8s3uZ8J1zM7v8w3H_RveS-OiZNZbm6lrifIjXfL09YrFYYFyXhTxOx5OvMnQGpYjqMslzUGmbTCj5J1Cb4fjjQbrjaUB02TLlpxBFxIh8S6MIGMJBH-APEKuMmq-eI5CB2mtDApX1FNdwtpu47x931Kcr9B1kNa98aiHD7o-kMTam83tLFmaxFnKDfIZKCdJgYFpD975WOOdGuRxCK0AhcliqlrAf3l6YPR41eb5ySag0" 44 | }, 45 | { 46 | "p": "3pWLqWlii4bd8Ie2uXl8_nzZKUpwmt55VoJRcb3-zY3-PR823YNByVouRgUPwhSH76Kual7ZAsCOKyqRXnqTFnLvGOuJC9zGi44rB7riuj0cLAKy3-DUpGKhjpX8kNJ7nr2hurqCjrlicQIU5lmaRXHKbd4jcEkEEIWbVmGKbYIWq7tOChCmvM-EuYlzAckjw8jumg1FmZ_631HGCfSLMu0755AwULlEJ5DuZ4-wB-ezCUPt5IwjIcg_S1MBYNdSMVoSu0yaiwU5jBc6YxlJ9J10a7VzYrWdslyMQVvOCyLJcuSdgoyL7OL1aIl-HiXiGI5HfPNEDYjd3wUY9Fd5Ew", 47 | "kty": "RSA", 48 | "q": "oqz9wwF_hlMjTyUAXZZ0c13KoDH4LNelGzkazpRYcZ0gylcN2NlKMoj9KMYaqwHTQfImubhmTOfsXPAZTV3lx8f7pLsbsFCnyhGYzxfYgVOe0IEdLeGiw75I2jXeEbXN8pXgG_-5BU9_GF1BBNyFygGALLh7lJpZ1cxWBhJAfJi0Qerti4xs1xAWrX7XVZKFODj8MnI-mWu16fxC-MNGGDRogjBn_OLhB5D5oosWnY6KiOeB7j3WYlwitNKxVwXrVuMKx9-au8fKtQ03BV8OWBOerjpbuIIwg5hvnlBC685YoBXwYQowyMoT-agf3-jmHvqb8SYe2Qqnh4Zqx2ijkQ", 49 | "d": "DiLcOTd5cD3o7dCU0hdPtYNCHJQ--wHyHISSn83K78D84HVJZzCvZREBzGv6sbgVaF8iIrr8CGMN4wNfpf9aT53_QSGKJEF9yQjCf-3KMacn1VwCQwUsfG6WxtWoN724-5sfmYF_kSfNAOGnipf45_vsw8KxH4faSMbJxj4n6SQdi9WzyJEFwe5idZ6w81bmqFyRuySRT27la_QvA3O8OG8Uz_Oh8GGuktfMg3hSxJZV2ddxzPUlXKFwVl4MZTqbitqluyNVhhJkRUXXMKZEmzeAwQSuPW6fFLo1w4ayzLdxAUERdnaGFVymhAVCQq6EBmAN6X0Y96xfvssPJpgzOxclMfiof905G768RIdeafGx_GjzExvKohP2BnBOFqr6AngNaCNNjvZN-VfHFBm7Pa-0AQ1MyZ3zQezYi7yfDoikIPCc_9wXvTdPtoyxh7cgg5h8SDN5YP6bFGbXN0tABJZDxtzJjMvnDN6NEGII7EcBqm3nMNEGdMK0KqP2H6DqtHSdBUwid9VNkFP0GGW1yEM6gylsC1QjF_4DAfuBQUw38M2LVhz7CIOkweJV2Vci1AuyhRasqvlPddEimovn2KN0CZr7QmeiPP-pCKu9vlzA3JLzWNhjEID08PkxSurX2t7ynEHoaHpenYTUbUOwO1jG4poPoIP6xeaIZb_iksE", 50 | "e": "AQAB", 51 | "use": "sig", 52 | "kid": "p384", 53 | "alg": "PS384", 54 | "n": "jXELfmLAcDlDwsZ-RHrzSnQkk4RSNBiWpdIo-YdCbM6LWg5Mn4tRcPfngtgtj2PxUn0CpwKwAnc3gHqr3eE2wWZHGUd-x8hsmIbmT0YLE_xOWCrSO9rF0-CLqeA6DiVkf2r7GZqUd2p4j8bMLzHF_2g6JQfF7g88SsU9PuwmvmsJ7OXMf3H7RN8bR-GdbnIMAlCYQohXtFds185mssyhgGcS9P7mwJdK08WBVuCV9jGnXU4TZr_qLrKSyrRc_dm6VNHTW5OHXRg-1Xsh8ayQ0zbLadE6AUjLXhYkBm2GqRYwVcDKR8H0vUX0nRvWuZncs_IY6u8iMIQ8yDgzV8HZhP-DJazsqYsgyOIvsia7krWfeITBlxbiq2d_A8DtstKypIrbgNnqgpkypxWy2cnY0LeMk4fnrcqSgjI3TIZHH_q1i8u4A-h7pmwENJK99GYtBDDN8VgAtNidegAxUkADjUWsgaBWtWuxrP3_EbicEYSzNfNfTm-3HYjeEoJkxLOid7sh5BTcoDFbuSpj_r7H__JYTtnpdpryhoprMQ15tJXUh8iTUoL3HOy9SSqwsHwyH889rj-C0_6l836PrnBbHKyc_ZTfWFSpZZHQHNxUyFVqinTXzbBb4DCysdqaA4ZpCBNZa0fnf-znvIXWFhAYR-eT9sZIEztwV0EdOwxarMM" 55 | }, 56 | { 57 | "p": "1yGRnCVYFDkmKIf0e49H4PGJVl6Y1ElWmIYa23_jhVTMkUvgPeO5IRI4TmFU1K4QYMdaLnMmtfy_E_j5dkO3sfHnETEXVkkxLErND08WD8vlmLVJGrC7ziRQCxN_28f6TPkb0AKXvU-t6Eog7TgolnWrSSxMMHTQEg5T7ZLe1P0j6SFjabC88NI0N0dL9UJokNObXjmIN_3AZF10rsA7mQCLJl3Kl6XFzeXpv6X0_PPefJAFqFWXQodjHtvWrHb5FDNVxahi130rldNfzy-06AW46i2Zmb1t-4svn5L1twvt51RVpsiKsh6PZXFkRbUPMvV5JRaIU8cEdkXQ_4BIKw", 58 | "kty": "RSA", 59 | "q": "tlJ3hZq9p2CG2aQIfeO9hRcKwjAoUoEfMYuu9w50q7t3r8-YdQECZhreLUgko_8GGjjsi-LtUrwKtl7JWKgEgjGajs-ZuI1kk9MvE2pyqwFCzjJzEX8pYbCh45ikvOUd7ykTzBwRL_2M1f9Sok4nRzsR1yVhmzgUxSkoVgG-WKvJWHA2LFjRjUSDbezBLMqv5UEa5y03kiu1ZKDvuOqlcJPgqlOk5sHtDF0Hd-7iYaNxJZSiOa-6COGZtTquTwdhp8O9BiZxoKn0r3duVQhFzCgmWG-y5o86nV9QtLyPTCTiZbKrxrqaEokQXTDjum8lRaFoww1pUHXV_S5MXNTW5w", 60 | "d": "WwpayQcxiuXs9QjOTrwP05pKvMKGBehV8Jqe5vx6mDxz9u1MWt0WiV0OaGJKpm-IkyqICzkzbRJB8UVhQDWDLt-mFacr7QHaK2S_ReVdVW8A1A_5fstKT6oo3fHfZY72QaNWA_K-noRctpWwsXX-WXsNPPVDpUTeMDQnPYP6bw2siMl71uPqmfwhDjx9JSnOO6mbGg6x4Mkxx_ZjUHQ9hGgPLM-3hEoLIZnAofH626OuuMGgT8ShUlE0nbwI2s375A1FjadVPhPQ-eKQlGgmAwejY7eorUev9geaV-tcaPFGtkDR7VEvbkbaMA13y4xWovLvUQnGmirdpPfEyQrAeQlGd1Z6XEAnAdL4ofXaIfv54vYi36yTaDBg8iSV-1oeEskkVnvGWqWcvhSIz9p4EVuM2pEuhQVzvHlUtOU0693UfvxyEg5iakUrmS75LnDGGE2-dvtLeKtQSymONBu7fM3-USKLzrwsp95ivrp2iuBw87TQHJJc1Xk8SE6pOO9FLneVJGhexhmhrvvaQSoGS_eN3rDpj0uvOz-sixWL5GEhnv_a2jR64zMwcbR7ABfUtY4LonvujYv6cyHsJ1Ip2da3CFYVuZgVboj9GhUUsvguu9yFhmpInkwDtxeOwocOd6p_9o45EkryeoHNeIMDfT8-vo5MKeoGfY6DCuZE9Ak", 61 | "e": "AQAB", 62 | "use": "sig", 63 | "kid": "p512", 64 | "alg": "PS512", 65 | "n": "mTcqtoe6YhrmPJixmM5AqJF0tNexsd1_0MblYugRfcCGIR1GfHdWmBcw7qmm_pTMwS_x1pe-F5Jk0Fx4u7Bzuqaq0wDkKtjajNujPy3eEiVoN1S_XuR9L6MuKZdaSCuff9ibZtwi-zh9qMsnh5xJQX-JuQt7Yc8-moBluNbVg2Lhry5MIMM6S-gkxtSfGUa3w0Gcwi2LHO6fVQcU9os4ryowO1C-Fx-_fqh9dDQzrmv1Z-H6QB1aW40iWR6qGRrF_gkVHxLwTDYcJbsl82jDm2C-dEXDvKIeuXWb8UJpV4JpDCiZ-WuX0EnkuW8IJSgMnf5pLpA6gdSjMFqFTBv9CITqxvBnoa5Q8TgKi-Ft0EGMMZfTrH4m1AE3-Q8uYOwjo6La6dy_kQwiqN2NjRwCnOMyzcuHCrZNswMpe3srHUqpxtCQdzX4E5x5AlhbWlS5yPzVixcnVfk1jtLSQY97s7rioxr9GiEIutymLT3BAysdzObqLPob_cur4Scm6AuAtMCIuaMZz3Hzsh7GkD6J7vdIaRyvNTUVPrcSa9WCoh51WpaUPBhEbyseLIGlug4yongeHJe6x4yLSMW51gA_Y8v4wAZyhHbHcNRqk8efx_xOS3YcNQf6W4DG3W_eCrX3oCxdq10gslSJKDkEJF-RX10mY5BOEhrH0oeMGwCxEM0" 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /fixtures/symmetric.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "oct", 5 | "alg": "A128KW", 6 | "k": "GawgguFyGrWKav7AX4VKUg", 7 | "kid": "sim1" 8 | }, 9 | { 10 | "kty": "oct", 11 | "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", 12 | "kid": "sim2", 13 | "alg": "HS256" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /gin/jose.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/go-jose/go-jose/v3/jwt" 12 | auth0 "github.com/krakend/go-auth0/v2" 13 | krakendjose "github.com/krakendio/krakend-jose/v2" 14 | "github.com/luraproject/lura/v2/config" 15 | "github.com/luraproject/lura/v2/logging" 16 | "github.com/luraproject/lura/v2/proxy" 17 | ginlura "github.com/luraproject/lura/v2/router/gin" 18 | ) 19 | 20 | func HandlerFactory(hf ginlura.HandlerFactory, logger logging.Logger, rejecterF krakendjose.RejecterFactory) ginlura.HandlerFactory { 21 | return TokenSignatureValidator(TokenSigner(hf, logger), logger, rejecterF) 22 | } 23 | 24 | func TokenSigner(hf ginlura.HandlerFactory, logger logging.Logger) ginlura.HandlerFactory { 25 | return func(cfg *config.EndpointConfig, prxy proxy.Proxy) gin.HandlerFunc { 26 | logPrefix := "[ENDPOINT: " + cfg.Endpoint + "][JWTSigner]" 27 | signerCfg, signer, err := krakendjose.NewSigner(cfg, nil) 28 | if err == krakendjose.ErrNoSignerCfg { 29 | logger.Debug(logPrefix, "Signer disabled") 30 | return hf(cfg, prxy) 31 | } 32 | if err != nil { 33 | logger.Error(logPrefix, "Unable to create the signer:", err.Error()) 34 | return erroredHandler 35 | } 36 | 37 | logger.Debug(logPrefix, "Signer enabled") 38 | 39 | return func(c *gin.Context) { 40 | proxyReq := ginlura.NewRequest(cfg.HeadersToPass)(c, cfg.QueryString) 41 | ctx, cancel := context.WithTimeout(c, cfg.Timeout) 42 | defer cancel() 43 | 44 | response, err := prxy(ctx, proxyReq) 45 | if err != nil { 46 | logger.Error(logPrefix, "Proxy response:", err.Error()) 47 | c.AbortWithStatus(http.StatusBadRequest) 48 | return 49 | } 50 | 51 | if response == nil { 52 | logger.Error(logPrefix, "Empty proxy response") 53 | c.AbortWithStatus(http.StatusBadRequest) 54 | return 55 | } 56 | 57 | if err := krakendjose.SignFields(signerCfg.KeysToSign, signer, response); err != nil { 58 | logger.Error(logPrefix, "Signing fields:", err.Error()) 59 | c.AbortWithStatus(http.StatusBadRequest) 60 | return 61 | } 62 | 63 | for k, v := range response.Metadata.Headers { 64 | c.Header(k, v[0]) 65 | } 66 | c.JSON(response.Metadata.StatusCode, response.Data) 67 | } 68 | } 69 | } 70 | 71 | func TokenSignatureValidator(hf ginlura.HandlerFactory, logger logging.Logger, rejecterF krakendjose.RejecterFactory) ginlura.HandlerFactory { 72 | return func(cfg *config.EndpointConfig, prxy proxy.Proxy) gin.HandlerFunc { 73 | logPrefix := "[ENDPOINT: " + cfg.Endpoint + "][JWTValidator]" 74 | if rejecterF == nil { 75 | rejecterF = new(krakendjose.NopRejecterFactory) 76 | } 77 | rejecter := rejecterF.New(logger, cfg) 78 | 79 | handler := hf(cfg, prxy) 80 | scfg, err := krakendjose.GetSignatureConfig(cfg) 81 | if err == krakendjose.ErrNoValidatorCfg { 82 | logger.Info(logPrefix, "Validator disabled for this endpoint") 83 | return handler 84 | } 85 | if err != nil { 86 | logger.Warning(logPrefix, "Unable to parse the configuration:", err.Error()) 87 | return erroredHandler 88 | } 89 | 90 | validator, err := krakendjose.NewValidator(scfg, FromCookie, FromHeader) 91 | if err != nil { 92 | logger.Fatal(logPrefix, "Unable to create the validator:", err.Error()) 93 | return erroredHandler 94 | } 95 | 96 | var aclCheck func(string, map[string]interface{}, []string) bool 97 | 98 | if scfg.RolesKeyIsNested && strings.Contains(scfg.RolesKey, ".") && scfg.RolesKey[:4] != "http" { 99 | logger.Debug(logPrefix, fmt.Sprintf("Roles will be matched against the nested key: '%s'", scfg.RolesKey)) 100 | aclCheck = krakendjose.CanAccessNested 101 | } else { 102 | logger.Debug(logPrefix, fmt.Sprintf("Roles will be matched against the key: '%s'", scfg.RolesKey)) 103 | aclCheck = krakendjose.CanAccess 104 | } 105 | 106 | var scopesMatcher func(string, map[string]interface{}, []string) bool 107 | 108 | if len(scfg.Scopes) > 0 && scfg.ScopesKey != "" { 109 | if scfg.ScopesMatcher == "all" { 110 | logger.Debug(logPrefix, fmt.Sprintf("Constraint added: tokens must contain a claim '%s' with all these scopes: %v", scfg.ScopesKey, scfg.Scopes)) 111 | scopesMatcher = krakendjose.ScopesAllMatcher 112 | } else { 113 | logger.Debug(logPrefix, fmt.Sprintf("Constraint added: tokens must contain a claim '%s' with any of these scopes: %v", scfg.ScopesKey, scfg.Scopes)) 114 | scopesMatcher = krakendjose.ScopesAnyMatcher 115 | } 116 | } else { 117 | logger.Debug(logPrefix, "No scope validation required") 118 | scopesMatcher = krakendjose.ScopesDefaultMatcher 119 | } 120 | 121 | if scfg.OperationDebug { 122 | logger.Debug(logPrefix, "Validator enabled for this endpoint. Operation debug is enabled") 123 | } else { 124 | logger.Debug(logPrefix, "Validator enabled for this endpoint") 125 | } 126 | 127 | paramExtractor := extractRequiredJWTClaims(cfg) 128 | 129 | return func(c *gin.Context) { 130 | token, err := validator.ValidateRequest(c.Request) 131 | if err != nil { 132 | if scfg.OperationDebug { 133 | logger.Error(logPrefix, "Unable to validate the token:", err.Error()) 134 | } 135 | c.AbortWithStatus(http.StatusUnauthorized) 136 | return 137 | } 138 | 139 | claims := map[string]interface{}{} 140 | err = validator.Claims(c.Request, token, &claims) 141 | if err != nil { 142 | if scfg.OperationDebug { 143 | logger.Error(logPrefix, "Token sent by client is invalid:", err.Error()) 144 | } 145 | c.AbortWithStatus(http.StatusUnauthorized) 146 | return 147 | } 148 | 149 | if rejecter.Reject(claims) { 150 | if scfg.OperationDebug { 151 | logger.Error(logPrefix, "Token sent by client rejected") 152 | } 153 | c.AbortWithStatus(http.StatusUnauthorized) 154 | return 155 | } 156 | 157 | if !aclCheck(scfg.RolesKey, claims, scfg.Roles) { 158 | if scfg.OperationDebug { 159 | logger.Error(logPrefix, "Token sent by client does not have sufficient roles") 160 | } 161 | c.AbortWithStatus(http.StatusForbidden) 162 | return 163 | } 164 | 165 | if !scopesMatcher(scfg.ScopesKey, claims, scfg.Scopes) { 166 | if scfg.OperationDebug { 167 | logger.Error(logPrefix, "Token sent by client does not have the required scopes") 168 | } 169 | c.AbortWithStatus(http.StatusForbidden) 170 | return 171 | } 172 | 173 | propagateHeaders(cfg, scfg.PropagateClaimsToHeader, claims, c, logger) 174 | 175 | paramExtractor(c, claims) 176 | 177 | handler(c) 178 | } 179 | } 180 | } 181 | 182 | func erroredHandler(c *gin.Context) { 183 | c.AbortWithStatus(http.StatusUnauthorized) 184 | } 185 | 186 | func propagateHeaders(cfg *config.EndpointConfig, propagationCfg [][]string, claims map[string]interface{}, c *gin.Context, logger logging.Logger) { 187 | logPrefix := "[ENDPOINT: " + cfg.Endpoint + "][PropagateHeaders]" 188 | if len(propagationCfg) > 0 { 189 | headersToPropagate, err := krakendjose.CalculateHeadersToPropagate(propagationCfg, claims) 190 | if err != nil { 191 | logger.Warning(logPrefix, err.Error()) 192 | } 193 | for k, v := range headersToPropagate { 194 | // Set header value - replaces existing one 195 | c.Request.Header.Set(k, v) 196 | } 197 | } 198 | } 199 | 200 | var jwtParamsPattern = regexp.MustCompile(`{{\.JWT\.([^}]*)}}`) 201 | 202 | func extractRequiredJWTClaims(cfg *config.EndpointConfig) func(*gin.Context, map[string]interface{}) { 203 | var required []string 204 | 205 | for _, backend := range cfg.Backend { 206 | for _, match := range jwtParamsPattern.FindAllStringSubmatch(backend.URLPattern, -1) { 207 | if len(match) < 2 { 208 | continue 209 | } 210 | required = append(required, match[1]) 211 | } 212 | } 213 | if len(required) == 0 { 214 | return func(_ *gin.Context, _ map[string]interface{}) {} 215 | } 216 | 217 | return func(c *gin.Context, claims map[string]interface{}) { 218 | cl := krakendjose.Claims(claims) 219 | for _, param := range required { 220 | // TODO: check for nested claims 221 | v, ok := cl.Get(param) 222 | if !ok { 223 | continue 224 | } 225 | c.Params = append(c.Params, gin.Param{Key: "JWT." + param, Value: v}) 226 | } 227 | } 228 | } 229 | 230 | func FromCookie(key string) func(r *http.Request) (*jwt.JSONWebToken, error) { 231 | if key == "" { 232 | key = "access_token" 233 | } 234 | return func(r *http.Request) (*jwt.JSONWebToken, error) { 235 | cookie, err := r.Cookie(key) 236 | if err != nil { 237 | return nil, auth0.ErrTokenNotFound 238 | } 239 | return jwt.ParseSigned(cookie.Value) 240 | } 241 | } 242 | 243 | func FromHeader(header string) func(r *http.Request) (*jwt.JSONWebToken, error) { 244 | if header == "" { 245 | header = "Authorization" 246 | } 247 | return func(r *http.Request) (*jwt.JSONWebToken, error) { 248 | raw := r.Header.Get(header) 249 | if len(raw) > 7 && strings.EqualFold(raw[0:7], "BEARER ") { 250 | raw = raw[7:] 251 | } 252 | if raw == "" { 253 | return nil, auth0.ErrTokenNotFound 254 | } 255 | return jwt.ParseSigned(raw) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /gin/jose_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | krakendjose "github.com/krakendio/krakend-jose/v2" 8 | "github.com/luraproject/lura/v2/config" 9 | ) 10 | 11 | func BenchmarkValidation_ES256(b *testing.B) { 12 | cfg := &config.EndpointConfig{ 13 | Backend: []*config.Backend{}, 14 | ExtraConfig: map[string]interface{}{ 15 | krakendjose.ValidatorNamespace: map[string]interface{}{ 16 | "alg": "ES256", 17 | "jwk_local_path": "../fixtures/public.json", 18 | }, 19 | }, 20 | } 21 | scfg, _ := krakendjose.GetSignatureConfig(cfg) 22 | validator, _ := krakendjose.NewValidator(scfg, FromCookie, FromHeader) 23 | 24 | req, _ := http.NewRequest("GET", "/", nil) 25 | req.Header.Add("Authorization", "BEARER eyJhbGciOiJFUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.1bNeUdXVB1HULFizcd92JuCj9EL_LCdGUMMbsAlxue84I61EWWXJ0SbmJU_Gm8obTyQlXf2UgptynARytgfU0A") 26 | 27 | b.ResetTimer() 28 | b.ReportAllocs() 29 | 30 | for i := 0; i < b.N; i++ { 31 | validator.ValidateRequest(req) 32 | } 33 | } 34 | 35 | func BenchmarkValidation_RS256(b *testing.B) { 36 | cfg := &config.EndpointConfig{ 37 | Backend: []*config.Backend{}, 38 | ExtraConfig: map[string]interface{}{ 39 | krakendjose.ValidatorNamespace: map[string]interface{}{ 40 | "alg": "RS256", 41 | "jwk_local_path": "../fixtures/public.json", 42 | }, 43 | }, 44 | } 45 | scfg, _ := krakendjose.GetSignatureConfig(cfg) 46 | validator, _ := krakendjose.NewValidator(scfg, FromCookie, FromHeader) 47 | 48 | req, _ := http.NewRequest("GET", "/", nil) 49 | req.Header.Add("Authorization", "BEARER eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.NrLwxZK8UhS6CV2ijdJLUfAinpjBn5_uliZCdzQ7v-Dc8lcv1AQA9cYsG63RseKWH9u6-TqPKMZQ56WfhqL028BLDdQCiaeuBoLzYU1tQLakA1V0YmouuEVixWLzueVaQhyGx-iKuiuFhzHWZSqFqSehiyzI9fb5O6Gcc2L6rMEoxQMaJomVS93h-t013MNq3ADLWTXRaO-negydqax_WmzlVWp_RDroR0s5J2L2klgmBXVwh6SYy5vg7RrnuN3S8g4oSicJIi9NgnG-dDikuaOg2DeFUt-mYq_j_PbNXf9TUl5hl4kEy7E0JauJ17d1BUuTl3ChY4BOmhQYRN0dYg") 50 | 51 | b.ResetTimer() 52 | b.ReportAllocs() 53 | 54 | for i := 0; i < b.N; i++ { 55 | validator.ValidateRequest(req) 56 | } 57 | } 58 | 59 | func BenchmarkValidation_RS384(b *testing.B) { 60 | cfg := &config.EndpointConfig{ 61 | Backend: []*config.Backend{}, 62 | ExtraConfig: map[string]interface{}{ 63 | krakendjose.ValidatorNamespace: map[string]interface{}{ 64 | "alg": "RS384", 65 | "jwk_local_path": "../fixtures/public.json", 66 | }, 67 | }, 68 | } 69 | scfg, _ := krakendjose.GetSignatureConfig(cfg) 70 | validator, _ := krakendjose.NewValidator(scfg, FromCookie, FromHeader) 71 | 72 | req, _ := http.NewRequest("GET", "/", nil) 73 | req.Header.Add("Authorization", "BEARER eyJhbGciOiJSUzM4NCIsImtpZCI6IjM4NCJ9.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.MnbhVVK-8o-wxSF3p159Ao5gu3cO9cfepOfBJnHaLnl9sEGlZNZchlVsscybFF_oq4Pm3yfG-oXC7zf5W3Mi3SULLwXUPXB28yBk27un6-5431FJTBombFN8njf0dLUFJ1IsDBiGX0CBaQ_cge_p06fc6K7PP97mqnECKyoJRzBzY5V79iXy3eMImhFfwVSYSJC3pnC-UzADspFja3IKYIJDNsmMCKM2hM0HYJI3AlERCNdBPKi5h12BM7zmkVjBlfs90AwL71r22B-b2kA3RlOJ_jnOY1AAGwsbxmRG-HH-Kdy7w87Iib5duOgje905j0I1sf13pPIfWFzJ3pEQZU5Y0ZdBgBbvWtYDjjmhfMo8Y0ZBWdF3WvQd3Z7OaEFWJi2D18JFUIILUPAFjDItrq72r8bPmu6v612ZDH5-A0uxoikdkinTTl7CaFyEt9Fi-juTrcOfvoSvyJ3-LGbpUBXTxevaQyI_vOmWs5xduAZWe3Lk061pRCi1YJXCzyEIlcUADQudSp8h26obLiGtAzs9Ftff34-BWkGHggfSACRDN2S-rm9rOy4vF6efSzt5QjfNMqsqPqohPyIl3d91-5W3-GKUKSgVyryOqRFkSUwLHC6-uK3JWtCqetswOTZoJNEQkk256Muys1LzJucNFtAgMaXg5OVUecYDeMKo7mk") 74 | 75 | b.ResetTimer() 76 | b.ReportAllocs() 77 | 78 | for i := 0; i < b.N; i++ { 79 | validator.ValidateRequest(req) 80 | } 81 | } 82 | 83 | func BenchmarkValidation_RS512(b *testing.B) { 84 | cfg := &config.EndpointConfig{ 85 | Backend: []*config.Backend{}, 86 | ExtraConfig: map[string]interface{}{ 87 | krakendjose.ValidatorNamespace: map[string]interface{}{ 88 | "alg": "RS512", 89 | "jwk_local_path": "../fixtures/public.json", 90 | }, 91 | }, 92 | } 93 | scfg, _ := krakendjose.GetSignatureConfig(cfg) 94 | validator, _ := krakendjose.NewValidator(scfg, FromCookie, FromHeader) 95 | 96 | req, _ := http.NewRequest("GET", "/", nil) 97 | req.Header.Add("Authorization", "BEARER eyJhbGciOiJSUzUxMiIsImtpZCI6IjRrNTEyIn0.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.hk6pz4ro8dIQN6lQpu5VSfKhnCWg3d0jRN7tKVJavk_WLsXt9FSvSbTMMVBVDo4Ea9oXDEbamelISn_ViNiP9JyIYdMcU1fVoxagpKl2PAvSH_wxzfc-45McSV3NshPeCANMiuq0pjD0-RE31TfEC515sbAKfMNePf1Aw2Zut3bve5Ol2ZReW_T3XeJivAaOpvZK1nZ9UNhevzszJ_l8Y8d1uhzA9IpfiWFLyH0VyYevrgLThMk--OjET2sOje-mA8YhL2yz5c3IEMAKGaly2U76mgukvlpcB8P-N69kC0f_EdyCo1-04tcoyLwBIglhhO4la3s2TyK7lQXma0iE0m5BG42bjCZ-R07vGg-zsnYt0GJlYOutpulfbqC-BXBbbuSqP8LaomzriVukzzaDw5As1coKJy6n9F8eNrQLdUPZHFtYBQoGZGQFlF2IGcUYGm3_Zm3fnIbzMS6nucRIc7nRCaeu0XP3_sErs-nsjc4JT-N2u3IGJtCDLb-op8WbIhef_eV_RPPq141M-rH7PHOzM2uFyO3tGEx1xnHNEYhk9hpq3cmYPEEMYtCgn6FDsq84PGJpmhqy3-4j-k_erIQyP8pGYGzCzkxdHeq_iFWLGE2TAAcpx60aH8GMwNjU5StcBzP8fetqTIrEVSJn4YHYCgIw2J3C9bPQTrmhyvM") 98 | 99 | b.ResetTimer() 100 | b.ReportAllocs() 101 | 102 | for i := 0; i < b.N; i++ { 103 | validator.ValidateRequest(req) 104 | } 105 | } 106 | 107 | func BenchmarkValidation_PS256(b *testing.B) { 108 | cfg := &config.EndpointConfig{ 109 | Backend: []*config.Backend{}, 110 | ExtraConfig: map[string]interface{}{ 111 | krakendjose.ValidatorNamespace: map[string]interface{}{ 112 | "alg": "PS256", 113 | "jwk_local_path": "../fixtures/public.json", 114 | }, 115 | }, 116 | } 117 | scfg, _ := krakendjose.GetSignatureConfig(cfg) 118 | validator, _ := krakendjose.NewValidator(scfg, FromCookie, FromHeader) 119 | 120 | req, _ := http.NewRequest("GET", "/", nil) 121 | req.Header.Add("Authorization", "BEARER eyJhbGciOiJQUzI1NiIsImtpZCI6InAyNTYifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.hnDYFvoQElI7PtAPXB-pYfFWAv2Ceg7Z-Xk76MFExi-57hCh0ivKGoaGjyCKBi_xznmzuZ4SPRtF5ARRH1B_3YKg2-ImetOEt9jdXbrwc77zSDzh78q_JiLYNoMv1at6TuAEFFJqoaE0XkJdyPbiCgwwb2FREVhob9zeXPaz90MzcKHHBJsEtxWdLFrXbXDmfkzdQEzwnk1kSi80xNRYdqYxWSus8PvWR0-boJ7OGfURXXKUSvwRhKUhglqpzxMltlJEeIykvzLzSXsgPnpubcu_ug5TJD0tW-7739V3_3zerqbE7xsHj1Hw1jPHiZhmSmoKFjI4OOKg-Ij-9RvqrIImW_mAWUUW3n40BMvt8WgV2qJCR64C8t13n006ev71MO4S1wqs-vzEPNPofSGaPL4n3zZEBn5cDMt5NjgHZNq-eVQT4izACELQf-zBdAnqE9yhCgC_6zBc1bBIFdOlq6kF7YNJdLdD9tOSAQN8hWutFDNLFHSrC3rP7j4HShm8eI1m4FarsHzTxrmDZLjeya0U5iStC0r5PwA2csdy1WfxServv-WH3ZhVvaRcyyBcLHaFvqCIc68h3Q6Y6m1W6X_LNzBqE9WMIsfdp9aqQn8cgUPIEA5xM2kerD5S0zU_LJFO1e0fq3gG1NtV0NDT-dGW8VX_szYG6dYBWwd35BE") 122 | 123 | b.ResetTimer() 124 | b.ReportAllocs() 125 | 126 | for i := 0; i < b.N; i++ { 127 | validator.ValidateRequest(req) 128 | } 129 | } 130 | 131 | func BenchmarkValidation_PS384(b *testing.B) { 132 | cfg := &config.EndpointConfig{ 133 | Backend: []*config.Backend{}, 134 | ExtraConfig: map[string]interface{}{ 135 | krakendjose.ValidatorNamespace: map[string]interface{}{ 136 | "alg": "PS384", 137 | "jwk_local_path": "../fixtures/public.json", 138 | }, 139 | }, 140 | } 141 | scfg, _ := krakendjose.GetSignatureConfig(cfg) 142 | validator, _ := krakendjose.NewValidator(scfg, FromCookie, FromHeader) 143 | 144 | req, _ := http.NewRequest("GET", "/", nil) 145 | req.Header.Add("Authorization", "BEARER eyJhbGciOiJQUzM4NCIsImtpZCI6InAzODQifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.Uf1vLmFcYNPGG8PC-Ej3cTcCdLXKVFAbwguCmkvsnmibOgzYD6gPX675QOkh0XKZWUId80-AVHylDOuR8bx-5QEYLX1crYXfTumW9CQ2_iKaOeMfhpELxQAk5N59qkLIDQRhPZL3DkG78kVBv3dKTHkrc7UvsHuJp9Yupdx6Ik0BHGBu8p6XgRBrBbF81Nh1mxbQhgoclNe1k_SLaDYwIhXBsjHzeT6SoNp--nxP9RJ0R85EVtgtVm7cX1fP6JEqWX4UPESlmR_9Ze2K7kft4GAywuZNIIW2Y6kSTEJhNnRkMUnux22O1wk2GPmJEvJmzkZ4o9b9d_oGChETod5KHzvIIbOZRhOkeJZ1EGK-y40X-N1uhKe1TM48Qf4CVCj-sIz3udmg17NC6zB-z70M0YtnI5xvhxuoMlQR2A4EP-gW9NFao4EYsLo5QM56GNj4r_3EvF7DC-KbBwg51ixT8m5fIT0SZjbDW-Znzjo4Xz_1LpeXzHxi1K-b_JUOn9TiZyZy9LVEQppzLX4S6XSpCze3gEwc7Fm9nda8xMGwr8nVebHFXDlTXXZvBOGMiQDosiS1Cl7u0ysrJJ0DguUTVogKirCS0gEPElpXkc9FGwjIrGLNuSSWt3bxKEaHUicSW9K5vRSJDowtDpM932wKOfe3S3EM0LdveaHZXvqljLg") 146 | 147 | b.ResetTimer() 148 | b.ReportAllocs() 149 | 150 | for i := 0; i < b.N; i++ { 151 | validator.ValidateRequest(req) 152 | } 153 | } 154 | 155 | func BenchmarkValidation_PS512(b *testing.B) { 156 | cfg := &config.EndpointConfig{ 157 | Backend: []*config.Backend{}, 158 | ExtraConfig: map[string]interface{}{ 159 | krakendjose.ValidatorNamespace: map[string]interface{}{ 160 | "alg": "PS512", 161 | "jwk_local_path": "../fixtures/public.json", 162 | }, 163 | }, 164 | } 165 | scfg, _ := krakendjose.GetSignatureConfig(cfg) 166 | validator, _ := krakendjose.NewValidator(scfg, FromCookie, FromHeader) 167 | 168 | req, _ := http.NewRequest("GET", "/", nil) 169 | req.Header.Add("Authorization", "BEARER eyJhbGciOiJQUzUxMiIsImtpZCI6InA1MTIifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.YIcRqRMGRRaeP7ImWKOpRJhveYK8TFHs4YlO3_YMFOBrJB4QSVYs_Z54EniNcBfrUwgu1EoqbEgh1mtpexUmwkgc6oWc69QhdEgWeITFRzxKhC9TF9V7l7HW713vCXfYgdFJ3-8hr0yNfoMz69tKHhwJnEMCnM6INj_jgFzuvgw3V2mlgWHCItx-J5MUY32d4E6AIHcgXAtQbBnVeke9y9JVlP2a27eZE6njW-d5zZxpHrRvwv_z1V2qWxZUpPxjZJV8n24vVi68saM9dF5OcDBX5xU8ntMOyb6AH_Jw2oE6fIsu1GyRfTmcCQJVmn-rh-A0gzvrT14WOPV9tYvNol8HGsSBDI8S86C0aD8b1VpjJufhqvgZZDUUFIcVLv0rFOSa4_vYwR_MjxxYeXqk7f9wVygK9SpD9QtYP_EdRzB7wZynX2jnyW6QGTcwuuP7OvbG_2Lpp20--1TBqCWeDpeyFv5uJT7iLKyFUXFyvC3tjBqobo-HBZOYiUInzfRCyofsRTqaoca1w-DsNdQCoAF4T1We7JECZIae2yn5owDf7qWl8S6qLk8BnfSeKZX540ppDbVy0EntN4ufT19GnMMl4-6ZTvl1TtNykOMoDF4ARU-yBbSmQlnG_47vfZ6NqWTEWFw-kcpuHiMEOKBjGTKFD6WwVGksxds6-oYOU0o") 170 | 171 | b.ResetTimer() 172 | b.ReportAllocs() 173 | 174 | for i := 0; i < b.N; i++ { 175 | validator.ValidateRequest(req) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /gin/jose_example_test.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | krakendjose "github.com/krakendio/krakend-jose/v2" 15 | "github.com/luraproject/lura/v2/config" 16 | "github.com/luraproject/lura/v2/logging" 17 | "github.com/luraproject/lura/v2/proxy" 18 | ginlura "github.com/luraproject/lura/v2/router/gin" 19 | ) 20 | 21 | func Example_RS256() { 22 | privateServer := httptest.NewServer(jwkEndpoint("private")) 23 | defer privateServer.Close() 24 | publicServer := httptest.NewServer(jwkEndpoint("public")) 25 | defer publicServer.Close() 26 | 27 | verifierCfg := newVerifierEndpointCfg("RS256", publicServer.URL, []string{"role_a"}) 28 | verifierCfg.ExtraConfig[krakendjose.ValidatorNamespace].(map[string]interface{})["operation_debug"] = true 29 | 30 | runValidationCycle( 31 | newSignerEndpointCfg("RS256", "2011-04-29", privateServer.URL), 32 | verifierCfg, 33 | ) 34 | 35 | // output: 36 | // token request 37 | // 201 38 | // {"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.u1fK05FpXctB-VkhhT3xu2WSIkEr1_VM71ald-yeKTesxhxg68TsHFEOBCgoXPuCviOP8QnUKNuVSeyMJh9z3nnrfQIjo9VZ2yicZu6ImYptSQ2DJbR80GDSPp-H7KnjaR9AAY0HZ0M-KUTaHdLABZFr307nkOeaJn_5jMpav7pqa7nrU3sI1CLX5pYVTggG6t7Zoqj2ebzzqdRxQEtdmZkD_NfH-3w3t-H0ylVdeBnPh-RvlspxC_mJzyUIJ0BwPlZpabppHm1ISySa4kwnwxEYnux0oZcb3PSoOZZZA467JySZ69PRlenNPdfGPL6E3uL1nqPHcxhte7ikSG4Q6Q","exp":2051882755,"refresh_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uMTI4NzZidmN4OThlcnR5dWlvcCIsInN1YiI6IjEyMzQ1Njc4OTBxd2VydHl1aW8ifQ.jwmNRj7gRcAgeeG9WqB2I8mqRVFZtw3uw5uSBJD8MmCVGRGPJ83ytEbqF3A-ya9IbdL5lJZ5LDUhwkO9xnkLZPBClDYBP81h0ZU7KR3vJnH9ZNkgpUiu1XLfkpJ6tSuZXPLj5-Lxymr3Mf8PdWey5YjEfk6mN_xfBHZR_XZbwsVbiv_nWhp-qeltPkXraShEpsDFFfzjRFrGprFi1S00OFDBcObbmXtZ8GTyJgSN8vO_rU-vkt6no1phKHzuyaS5D6GdjrxDXv7pHYL-OWifBiElMs09PAd16rZy3-qSIDZS7vHo724cG9UYMgxSE86PvjGP_dOJCOf64p_wPkkBRw"} 39 | // map[Content-Type:[application/json; charset=utf-8]] 40 | // unauthorized request 41 | // 401 42 | // authorized request 43 | // 200 44 | // {} 45 | // application/json; charset=utf-8 46 | // dummy request 47 | // 200 48 | // {} 49 | // application/json; charset=utf-8 50 | // refresh token request 51 | // 201 52 | // {"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.u1fK05FpXctB-VkhhT3xu2WSIkEr1_VM71ald-yeKTesxhxg68TsHFEOBCgoXPuCviOP8QnUKNuVSeyMJh9z3nnrfQIjo9VZ2yicZu6ImYptSQ2DJbR80GDSPp-H7KnjaR9AAY0HZ0M-KUTaHdLABZFr307nkOeaJn_5jMpav7pqa7nrU3sI1CLX5pYVTggG6t7Zoqj2ebzzqdRxQEtdmZkD_NfH-3w3t-H0ylVdeBnPh-RvlspxC_mJzyUIJ0BwPlZpabppHm1ISySa4kwnwxEYnux0oZcb3PSoOZZZA467JySZ69PRlenNPdfGPL6E3uL1nqPHcxhte7ikSG4Q6Q","exp":2051882755,"refresh_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uMTI4NzZidmN4OThlcnR5dWlvcCIsInN1YiI6IjEyMzQ1Njc4OTBxd2VydHl1aW8ifQ.jwmNRj7gRcAgeeG9WqB2I8mqRVFZtw3uw5uSBJD8MmCVGRGPJ83ytEbqF3A-ya9IbdL5lJZ5LDUhwkO9xnkLZPBClDYBP81h0ZU7KR3vJnH9ZNkgpUiu1XLfkpJ6tSuZXPLj5-Lxymr3Mf8PdWey5YjEfk6mN_xfBHZR_XZbwsVbiv_nWhp-qeltPkXraShEpsDFFfzjRFrGprFi1S00OFDBcObbmXtZ8GTyJgSN8vO_rU-vkt6no1phKHzuyaS5D6GdjrxDXv7pHYL-OWifBiElMs09PAd16rZy3-qSIDZS7vHo724cG9UYMgxSE86PvjGP_dOJCOf64p_wPkkBRw"} 53 | // application/json; charset=utf-8 54 | // DEBUG: [ENDPOINT: /private][JWTSigner] Signer disabled 55 | // DEBUG: [ENDPOINT: /private][JWTValidator] Roles will be matched against the key: 'roles' 56 | // DEBUG: [ENDPOINT: /private][JWTValidator] No scope validation required 57 | // DEBUG: [ENDPOINT: /private][JWTValidator] Validator enabled for this endpoint. Operation debug is enabled 58 | // DEBUG: [ENDPOINT: /token][JWTSigner] Signer enabled 59 | // INFO: [ENDPOINT: /token][JWTValidator] Validator disabled for this endpoint 60 | // DEBUG: [ENDPOINT: /refresh_token][JWTSigner] Signer enabled 61 | // DEBUG: [ENDPOINT: /refresh_token][JWTValidator] Roles will be matched against the key: 'roles' 62 | // DEBUG: [ENDPOINT: /refresh_token][JWTValidator] No scope validation required 63 | // DEBUG: [ENDPOINT: /refresh_token][JWTValidator] Validator enabled for this endpoint. Operation debug is enabled 64 | // DEBUG: [ENDPOINT: /private][JWTSigner] Signer disabled 65 | // INFO: [ENDPOINT: /private][JWTValidator] Validator disabled for this endpoint 66 | // ERROR: [ENDPOINT: /private][JWTValidator] Unable to validate the token: Token not found 67 | } 68 | 69 | func Example_HS256() { 70 | server := httptest.NewServer(jwkEndpoint("symmetric")) 71 | defer server.Close() 72 | 73 | runValidationCycle( 74 | newSignerEndpointCfg("HS256", "sim2", server.URL), 75 | newVerifierEndpointCfg("HS256", server.URL, []string{"role_a"}), 76 | ) 77 | 78 | // output: 79 | // token request 80 | // 201 81 | // {"access_token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InNpbTIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.xG6O62h475Y-EknyLFerPOUX6ATKCoIYEq4QsQsuw-Q","exp":2051882755,"refresh_token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InNpbTIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uMTI4NzZidmN4OThlcnR5dWlvcCIsInN1YiI6IjEyMzQ1Njc4OTBxd2VydHl1aW8ifQ.8rd0w9_H7Z_0J37nKvqQNwJnP25VrQcVAAa5sc3Fsw0"} 82 | // map[Content-Type:[application/json; charset=utf-8]] 83 | // unauthorized request 84 | // 401 85 | // authorized request 86 | // 200 87 | // {} 88 | // application/json; charset=utf-8 89 | // dummy request 90 | // 200 91 | // {} 92 | // application/json; charset=utf-8 93 | // refresh token request 94 | // 201 95 | // {"access_token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InNpbTIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.xG6O62h475Y-EknyLFerPOUX6ATKCoIYEq4QsQsuw-Q","exp":2051882755,"refresh_token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InNpbTIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uMTI4NzZidmN4OThlcnR5dWlvcCIsInN1YiI6IjEyMzQ1Njc4OTBxd2VydHl1aW8ifQ.8rd0w9_H7Z_0J37nKvqQNwJnP25VrQcVAAa5sc3Fsw0"} 96 | // application/json; charset=utf-8 97 | // DEBUG: [ENDPOINT: /private][JWTSigner] Signer disabled 98 | // DEBUG: [ENDPOINT: /private][JWTValidator] Roles will be matched against the key: 'roles' 99 | // DEBUG: [ENDPOINT: /private][JWTValidator] No scope validation required 100 | // DEBUG: [ENDPOINT: /private][JWTValidator] Validator enabled for this endpoint 101 | // DEBUG: [ENDPOINT: /token][JWTSigner] Signer enabled 102 | // INFO: [ENDPOINT: /token][JWTValidator] Validator disabled for this endpoint 103 | // DEBUG: [ENDPOINT: /refresh_token][JWTSigner] Signer enabled 104 | // DEBUG: [ENDPOINT: /refresh_token][JWTValidator] Roles will be matched against the key: 'roles' 105 | // DEBUG: [ENDPOINT: /refresh_token][JWTValidator] No scope validation required 106 | // DEBUG: [ENDPOINT: /refresh_token][JWTValidator] Validator enabled for this endpoint 107 | // DEBUG: [ENDPOINT: /private][JWTSigner] Signer disabled 108 | // INFO: [ENDPOINT: /private][JWTValidator] Validator disabled for this endpoint 109 | } 110 | 111 | func Example_HS256_cookie() { 112 | server := httptest.NewServer(jwkEndpoint("symmetric")) 113 | defer server.Close() 114 | 115 | sCfg := newSignerEndpointCfg("HS256", "sim2", server.URL) 116 | _, signer, _ := krakendjose.NewSigner(sCfg, nil) 117 | verifierCfg := newVerifierEndpointCfg("HS256", server.URL, []string{"role_a"}) 118 | 119 | externalTokenIssuer := func(rw http.ResponseWriter, _ *http.Request) { 120 | resp, _ := tokenIssuer(context.Background(), new(proxy.Request)) 121 | data, ok := resp.Data["access_token"] 122 | if !ok { 123 | rw.WriteHeader(http.StatusBadRequest) 124 | return 125 | } 126 | token, _ := signer(data) 127 | cookie := &http.Cookie{ 128 | Name: "access_token", 129 | Value: token, 130 | Expires: time.Now().Add(time.Hour), 131 | } 132 | http.SetCookie(rw, cookie) 133 | } 134 | 135 | loginRequest, _ := http.NewRequest("GET", "/", new(bytes.Buffer)) 136 | w := httptest.NewRecorder() 137 | externalTokenIssuer(w, loginRequest) 138 | 139 | buf := new(bytes.Buffer) 140 | logger, _ := logging.NewLogger("DEBUG", buf, "") 141 | hf := HandlerFactory(ginlura.EndpointHandler, logger, nil) 142 | 143 | gin.SetMode(gin.TestMode) 144 | engine := gin.New() 145 | 146 | engine.GET(verifierCfg.Endpoint, hf(verifierCfg, proxy.NoopProxy)) 147 | 148 | request, _ := http.NewRequest("GET", verifierCfg.Endpoint, new(bytes.Buffer)) 149 | if len(w.Result().Cookies()) == 0 { 150 | fmt.Println("unexpected number of cookies") 151 | return 152 | } 153 | request.AddCookie(w.Result().Cookies()[0]) 154 | 155 | w = httptest.NewRecorder() 156 | engine.ServeHTTP(w, request) 157 | 158 | fmt.Println(w.Result().StatusCode) 159 | fmt.Println(w.Body.String()) 160 | fmt.Println(w.Result().Header.Get("Content-Type")) 161 | 162 | printLog(buf) 163 | 164 | // output: 165 | // 200 166 | // {} 167 | // application/json; charset=utf-8 168 | // DEBUG: [ENDPOINT: /private][JWTSigner] Signer disabled 169 | // DEBUG: [ENDPOINT: /private][JWTValidator] Roles will be matched against the key: 'roles' 170 | // DEBUG: [ENDPOINT: /private][JWTValidator] No scope validation required 171 | // DEBUG: [ENDPOINT: /private][JWTValidator] Validator enabled for this endpoint 172 | } 173 | 174 | func runValidationCycle(signerEndpointCfg, validatorEndpointCfg *config.EndpointConfig) { 175 | buf := new(bytes.Buffer) 176 | logger, _ := logging.NewLogger("DEBUG", buf, "") 177 | hf := HandlerFactory(ginlura.EndpointHandler, logger, nil) 178 | 179 | mixedCfg := &config.EndpointConfig{ 180 | Timeout: time.Second, 181 | Endpoint: "/refresh_token", 182 | Method: signerEndpointCfg.Method, 183 | Backend: signerEndpointCfg.Backend, 184 | ExtraConfig: config.ExtraConfig{ 185 | krakendjose.SignerNamespace: signerEndpointCfg.ExtraConfig[krakendjose.SignerNamespace], 186 | krakendjose.ValidatorNamespace: validatorEndpointCfg.ExtraConfig[krakendjose.ValidatorNamespace], 187 | }, 188 | } 189 | 190 | gin.SetMode(gin.TestMode) 191 | engine := gin.New() 192 | 193 | engine.GET(validatorEndpointCfg.Endpoint, hf(validatorEndpointCfg, proxy.NoopProxy)) 194 | engine.POST(signerEndpointCfg.Endpoint, hf(signerEndpointCfg, tokenIssuer)) 195 | engine.POST(mixedCfg.Endpoint, hf(mixedCfg, tokenIssuer)) 196 | engine.GET("/", hf(&config.EndpointConfig{ 197 | Timeout: time.Second, 198 | Endpoint: "/private", 199 | Backend: []*config.Backend{ 200 | { 201 | URLPattern: "/", 202 | Host: []string{"http://example.com/"}, 203 | Timeout: time.Second, 204 | }, 205 | }, 206 | }, proxy.NoopProxy)) 207 | 208 | fmt.Println("token request") 209 | req := httptest.NewRequest("POST", signerEndpointCfg.Endpoint, new(bytes.Buffer)) 210 | 211 | w := httptest.NewRecorder() 212 | engine.ServeHTTP(w, req) 213 | 214 | fmt.Println(w.Result().StatusCode) 215 | fmt.Println(w.Body.String()) 216 | fmt.Println(w.Result().Header) 217 | 218 | responseData := struct { 219 | AccessToken string `json:"access_token"` 220 | RefreshToken string `json:"refresh_token"` 221 | Expiration int `json:"exp"` 222 | }{} 223 | json.Unmarshal(w.Body.Bytes(), &responseData) 224 | 225 | fmt.Println("unauthorized request") 226 | req = httptest.NewRequest("GET", validatorEndpointCfg.Endpoint, new(bytes.Buffer)) 227 | w = httptest.NewRecorder() 228 | engine.ServeHTTP(w, req) 229 | 230 | fmt.Println(w.Code) 231 | 232 | fmt.Println("authorized request") 233 | req = httptest.NewRequest("GET", validatorEndpointCfg.Endpoint, new(bytes.Buffer)) 234 | req.Header.Set("Authorization", "BEARER "+responseData.AccessToken) 235 | w = httptest.NewRecorder() 236 | engine.ServeHTTP(w, req) 237 | 238 | fmt.Println(w.Code) 239 | fmt.Println(w.Body.String()) 240 | fmt.Println(w.Result().Header.Get("Content-Type")) 241 | 242 | fmt.Println("dummy request") 243 | req = httptest.NewRequest("GET", "/", new(bytes.Buffer)) 244 | w = httptest.NewRecorder() 245 | engine.ServeHTTP(w, req) 246 | 247 | fmt.Println(w.Code) 248 | fmt.Println(w.Body.String()) 249 | fmt.Println(w.Result().Header.Get("Content-Type")) 250 | 251 | fmt.Println("refresh token request") 252 | req = httptest.NewRequest("POST", mixedCfg.Endpoint, new(bytes.Buffer)) 253 | req.Header.Set("Authorization", "BEARER "+responseData.AccessToken) 254 | w = httptest.NewRecorder() 255 | engine.ServeHTTP(w, req) 256 | 257 | fmt.Println(w.Code) 258 | fmt.Println(w.Body.String()) 259 | fmt.Println(w.Result().Header.Get("Content-Type")) 260 | 261 | printLog(buf) 262 | } 263 | 264 | func tokenIssuer(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 265 | return &proxy.Response{ 266 | Data: map[string]interface{}{ 267 | "access_token": map[string]interface{}{ 268 | "aud": "http://api.example.com", 269 | "iss": "http://example.com", 270 | "sub": "1234567890qwertyuio", 271 | "jti": "mnb23vcsrt756yuiomnbvcx98ertyuiop", 272 | "roles": []string{"role_a", "role_b"}, 273 | "exp": 2051882755, 274 | }, 275 | "refresh_token": map[string]interface{}{ 276 | "aud": "http://api.example.com", 277 | "iss": "http://example.com", 278 | "sub": "1234567890qwertyuio", 279 | "jti": "mnb23vcsrt756yuiomn12876bvcx98ertyuiop", 280 | "exp": 2051882755, 281 | }, 282 | "exp": 2051882755, 283 | }, 284 | Metadata: proxy.Metadata{ 285 | StatusCode: 201, 286 | }, 287 | IsComplete: true, 288 | }, nil 289 | } 290 | 291 | func newSignerEndpointCfg(alg, ID, URL string) *config.EndpointConfig { 292 | return &config.EndpointConfig{ 293 | Timeout: time.Second, 294 | Endpoint: "/token", 295 | Method: "POST", 296 | Backend: []*config.Backend{ 297 | { 298 | URLPattern: "/token", 299 | Host: []string{"http://example.com/"}, 300 | Timeout: time.Second, 301 | }, 302 | }, 303 | ExtraConfig: config.ExtraConfig{ 304 | krakendjose.SignerNamespace: map[string]interface{}{ 305 | "alg": alg, 306 | "kid": ID, 307 | "jwk_url": URL, 308 | "keys_to_sign": []string{"access_token", "refresh_token"}, 309 | "disable_jwk_security": true, 310 | "cache": true, 311 | }, 312 | }, 313 | } 314 | } 315 | 316 | func newVerifierEndpointCfg(alg, URL string, roles []string) *config.EndpointConfig { 317 | return &config.EndpointConfig{ 318 | Timeout: time.Second, 319 | Endpoint: "/private", 320 | Backend: []*config.Backend{ 321 | { 322 | URLPattern: "/", 323 | Host: []string{"http://example.com/"}, 324 | Timeout: time.Second, 325 | }, 326 | }, 327 | ExtraConfig: config.ExtraConfig{ 328 | krakendjose.ValidatorNamespace: map[string]interface{}{ 329 | "alg": alg, 330 | "jwk_url": URL, 331 | "audience": []string{"http://api.example.com"}, 332 | "issuer": "http://example.com", 333 | "roles": roles, 334 | "propagate_claims": [][]string{ 335 | {"jti", "x-krakend-jti"}, 336 | {"sub", "x-krakend-sub"}, 337 | {"nonexistent", "x-krakend-ne"}, 338 | {"sub", "x-krakend-replace"}, 339 | }, 340 | "disable_jwk_security": true, 341 | "cache": true, 342 | }, 343 | }, 344 | } 345 | } 346 | 347 | func printLog(buf *bytes.Buffer) { 348 | for _, l := range strings.Split(buf.String(), "\n") { 349 | if len(l) <= 20 { 350 | fmt.Println(l) 351 | continue 352 | } 353 | fmt.Println(l[20:]) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /gin/jose_test.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/gin-gonic/gin" 13 | jose "github.com/krakendio/krakend-jose/v2" 14 | "github.com/luraproject/lura/v2/config" 15 | "github.com/luraproject/lura/v2/logging" 16 | "github.com/luraproject/lura/v2/proxy" 17 | ginlura "github.com/luraproject/lura/v2/router/gin" 18 | ) 19 | 20 | func TestTokenSignatureValidator(t *testing.T) { 21 | server := httptest.NewServer(jwkEndpoint("public")) 22 | defer server.Close() 23 | 24 | validatorEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{"role_a"}) 25 | 26 | forbidenEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{"role_c"}) 27 | forbidenEndpointCfg.Endpoint = "/forbiden" 28 | 29 | registeredEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{}) 30 | registeredEndpointCfg.Endpoint = "/registered" 31 | registeredEndpointCfg.Backend[0].URLPattern = "/{{.JWT.sub}}/{{.JWT.jti}}?foo={{.JWT.iss}}" 32 | 33 | propagateHeadersEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{}) 34 | propagateHeadersEndpointCfg.Endpoint = "/propagateheaders" 35 | 36 | token := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.u1fK05FpXctB-VkhhT3xu2WSIkEr1_VM71ald-yeKTesxhxg68TsHFEOBCgoXPuCviOP8QnUKNuVSeyMJh9z3nnrfQIjo9VZ2yicZu6ImYptSQ2DJbR80GDSPp-H7KnjaR9AAY0HZ0M-KUTaHdLABZFr307nkOeaJn_5jMpav7pqa7nrU3sI1CLX5pYVTggG6t7Zoqj2ebzzqdRxQEtdmZkD_NfH-3w3t-H0ylVdeBnPh-RvlspxC_mJzyUIJ0BwPlZpabppHm1ISySa4kwnwxEYnux0oZcb3PSoOZZZA467JySZ69PRlenNPdfGPL6E3uL1nqPHcxhte7ikSG4Q6Q" 37 | 38 | dummyProxy := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 39 | return &proxy.Response{ 40 | Data: map[string]interface{}{ 41 | "aaaa": map[string]interface{}{ 42 | "foo": "a", 43 | "bar": "b", 44 | }, 45 | "bbbb": true, 46 | "cccc": 1234567890, 47 | }, 48 | IsComplete: true, 49 | Metadata: proxy.Metadata{ 50 | StatusCode: 200, 51 | }, 52 | }, nil 53 | } 54 | 55 | buf := new(bytes.Buffer) 56 | logger, _ := logging.NewLogger("DEBUG", buf, "") 57 | hf := HandlerFactory(ginlura.EndpointHandler, logger, nil) 58 | 59 | gin.SetMode(gin.TestMode) 60 | engine := gin.New() 61 | 62 | assertProxy := func(ctx context.Context, r *proxy.Request) (*proxy.Response, error) { 63 | if v, ok := r.Params["JWT.sub"]; !ok { 64 | t.Errorf("JWT param not injected: %v", r.Params) 65 | } else if v != "1234567890qwertyuio" { 66 | t.Errorf("wrong JWT param injected (sub): %v", v) 67 | } 68 | 69 | if v, ok := r.Params["JWT.jti"]; !ok { 70 | t.Errorf("JWT param not injected: %v", r.Params) 71 | } else if v != "mnb23vcsrt756yuiomnbvcx98ertyuiop" { 72 | t.Errorf("wrong JWT param injected (jti): %v", v) 73 | } 74 | 75 | if v, ok := r.Params["JWT.iss"]; !ok { 76 | t.Errorf("JWT param not injected: %v", r.Params) 77 | } else if v != "http://example.com" { 78 | t.Errorf("wrong JWT param injected (iss): %v", v) 79 | } 80 | 81 | return dummyProxy(ctx, r) 82 | } 83 | 84 | engine.GET(validatorEndpointCfg.Endpoint, hf(validatorEndpointCfg, dummyProxy)) 85 | engine.GET(forbidenEndpointCfg.Endpoint, hf(forbidenEndpointCfg, dummyProxy)) 86 | engine.GET(registeredEndpointCfg.Endpoint, hf(registeredEndpointCfg, assertProxy)) 87 | engine.GET(propagateHeadersEndpointCfg.Endpoint, hf(propagateHeadersEndpointCfg, dummyProxy)) 88 | 89 | req := httptest.NewRequest("GET", forbidenEndpointCfg.Endpoint, new(bytes.Buffer)) 90 | 91 | w := httptest.NewRecorder() 92 | engine.ServeHTTP(w, req) 93 | 94 | if w.Code != http.StatusUnauthorized { 95 | t.Errorf("unexpected status code: %d", w.Code) 96 | } 97 | if body := w.Body.String(); body != "" { 98 | t.Errorf("unexpected body: %s", body) 99 | } 100 | 101 | req = httptest.NewRequest("GET", validatorEndpointCfg.Endpoint, new(bytes.Buffer)) 102 | req.Header.Set("Authorization", "BEARER "+token) 103 | 104 | w = httptest.NewRecorder() 105 | engine.ServeHTTP(w, req) 106 | 107 | if w.Code != http.StatusOK { 108 | t.Errorf("unexpected status code: %d", w.Code) 109 | } 110 | if body := w.Body.String(); body != "{\"aaaa\":{\"bar\":\"b\",\"foo\":\"a\"},\"bbbb\":true,\"cccc\":1234567890}" { 111 | t.Errorf("unexpected body: %s", body) 112 | } 113 | 114 | if log := buf.String(); !strings.Contains(log, "DEBUG: [ENDPOINT: /propagateheaders][JWTSigner] Signer disabled") { 115 | t.Error(log) 116 | t.Fail() 117 | return 118 | } 119 | 120 | req = httptest.NewRequest("GET", forbidenEndpointCfg.Endpoint, new(bytes.Buffer)) 121 | req.Header.Set("Authorization", "BEARER "+token) 122 | 123 | w = httptest.NewRecorder() 124 | engine.ServeHTTP(w, req) 125 | 126 | if w.Code != http.StatusForbidden { 127 | t.Errorf("unexpected status code: %d", w.Code) 128 | } 129 | if body := w.Body.String(); body != "" { 130 | t.Errorf("unexpected body: %s", body) 131 | } 132 | 133 | req = httptest.NewRequest("GET", registeredEndpointCfg.Endpoint, new(bytes.Buffer)) 134 | req.Header.Set("Authorization", "BEARER "+token) 135 | 136 | w = httptest.NewRecorder() 137 | engine.ServeHTTP(w, req) 138 | 139 | if w.Code != http.StatusOK { 140 | t.Errorf("unexpected status code: %d", w.Code) 141 | } 142 | if body := w.Body.String(); body != "{\"aaaa\":{\"bar\":\"b\",\"foo\":\"a\"},\"bbbb\":true,\"cccc\":1234567890}" { 143 | t.Errorf("unexpected body: %s", body) 144 | } 145 | 146 | req = httptest.NewRequest("GET", propagateHeadersEndpointCfg.Endpoint, new(bytes.Buffer)) 147 | req.Header.Set("Authorization", "BEARER "+token) 148 | // Check header-overwrite: it must be overwritten by a claim in the JWT! 149 | req.Header.Set("x-krakend-replace", "abc") 150 | req.Header.Set("x-krakend-ne", "fake_non_existing") 151 | 152 | w = httptest.NewRecorder() 153 | engine.ServeHTTP(w, req) 154 | 155 | if req.Header.Get("x-krakend-jti") == "" { 156 | t.Error("JWT claim not propagated to header: jti") 157 | } else if req.Header.Get("x-krakend-jti") != "mnb23vcsrt756yuiomnbvcx98ertyuiop" { 158 | t.Errorf("wrong JWT claim propagated for 'jti': %v", req.Header.Get("x-krakend-jti")) 159 | } 160 | 161 | // Check that existing header values are overwritten 162 | if req.Header.Get("x-krakend-replace") == "abc" { 163 | t.Error("JWT claim not propagated to x-krakend-replace header: sub") 164 | } else if req.Header.Get("x-krakend-replace") != "1234567890qwertyuio" { 165 | t.Errorf("wrong JWT claim propagated for 'sub': %v", req.Header.Get("x-krakend-replace")) 166 | } 167 | 168 | if req.Header.Get("x-krakend-sub") == "" { 169 | t.Error("JWT claim not propagated to header: sub") 170 | } else if req.Header.Get("x-krakend-sub") != "1234567890qwertyuio" { 171 | t.Errorf("wrong JWT claim propagated for 'sub': %v", req.Header.Get("x-krakend-sub")) 172 | } 173 | 174 | if req.Header.Get("x-krakend-ne") != "" { 175 | t.Error("JWT claim propagated, although it shouldn't: nonexistent") 176 | } 177 | 178 | if w.Code != http.StatusOK { 179 | t.Errorf("unexpected status code: %d", w.Code) 180 | } 181 | if body := w.Body.String(); body != "{\"aaaa\":{\"bar\":\"b\",\"foo\":\"a\"},\"bbbb\":true,\"cccc\":1234567890}" { 182 | t.Errorf("unexpected body: %s", body) 183 | } 184 | } 185 | 186 | func TestCustomHeaderName(t *testing.T) { 187 | server := httptest.NewServer(jwkEndpoint("public")) 188 | defer server.Close() 189 | 190 | nonDefaultAuthHeaderEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{}) 191 | nonDefaultAuthHeaderEndpointCfg.Endpoint = "/custom-header" 192 | nonDefaultAuthHeaderEndpointCfg.ExtraConfig[jose.ValidatorNamespace].(map[string]interface{})["auth_header_name"] = "X-Custom-Auth" 193 | 194 | token := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.u1fK05FpXctB-VkhhT3xu2WSIkEr1_VM71ald-yeKTesxhxg68TsHFEOBCgoXPuCviOP8QnUKNuVSeyMJh9z3nnrfQIjo9VZ2yicZu6ImYptSQ2DJbR80GDSPp-H7KnjaR9AAY0HZ0M-KUTaHdLABZFr307nkOeaJn_5jMpav7pqa7nrU3sI1CLX5pYVTggG6t7Zoqj2ebzzqdRxQEtdmZkD_NfH-3w3t-H0ylVdeBnPh-RvlspxC_mJzyUIJ0BwPlZpabppHm1ISySa4kwnwxEYnux0oZcb3PSoOZZZA467JySZ69PRlenNPdfGPL6E3uL1nqPHcxhte7ikSG4Q6Q" 195 | 196 | dummyProxy := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 197 | return &proxy.Response{ 198 | Data: map[string]interface{}{ 199 | "aaaa": map[string]interface{}{ 200 | "foo": "a", 201 | "bar": "b", 202 | }, 203 | "bbbb": true, 204 | "cccc": 1234567890, 205 | }, 206 | IsComplete: true, 207 | Metadata: proxy.Metadata{ 208 | StatusCode: 200, 209 | }, 210 | }, nil 211 | } 212 | 213 | buf := new(bytes.Buffer) 214 | logger, _ := logging.NewLogger("DEBUG", buf, "") 215 | hf := HandlerFactory(ginlura.EndpointHandler, logger, nil) 216 | 217 | gin.SetMode(gin.TestMode) 218 | engine := gin.New() 219 | 220 | engine.GET(nonDefaultAuthHeaderEndpointCfg.Endpoint, hf(nonDefaultAuthHeaderEndpointCfg, dummyProxy)) 221 | 222 | req := httptest.NewRequest("GET", nonDefaultAuthHeaderEndpointCfg.Endpoint, new(bytes.Buffer)) 223 | req.Header.Set("X-Custom-Auth", "BEARER "+token) 224 | 225 | w := httptest.NewRecorder() 226 | engine.ServeHTTP(w, req) 227 | 228 | if w.Code != http.StatusOK { 229 | t.Errorf("unexpected status code: %d", w.Code) 230 | } 231 | if body := w.Body.String(); body != "{\"aaaa\":{\"bar\":\"b\",\"foo\":\"a\"},\"bbbb\":true,\"cccc\":1234567890}" { 232 | t.Errorf("unexpected body: %s", body) 233 | } 234 | 235 | req = httptest.NewRequest("GET", nonDefaultAuthHeaderEndpointCfg.Endpoint, new(bytes.Buffer)) 236 | req.Header.Set("Authorization", "BEARER "+token) 237 | 238 | w = httptest.NewRecorder() 239 | engine.ServeHTTP(w, req) 240 | 241 | if w.Code != http.StatusUnauthorized { 242 | t.Errorf("unexpected status code: %d", w.Code) 243 | } 244 | if body := w.Body.String(); body != "" { 245 | t.Errorf("unexpected body: %s", body) 246 | } 247 | 248 | req = httptest.NewRequest("GET", nonDefaultAuthHeaderEndpointCfg.Endpoint, new(bytes.Buffer)) 249 | 250 | w = httptest.NewRecorder() 251 | engine.ServeHTTP(w, req) 252 | 253 | if w.Code != http.StatusUnauthorized { 254 | t.Errorf("unexpected status code: %d", w.Code) 255 | } 256 | if body := w.Body.String(); body != "" { 257 | t.Errorf("unexpected body: %s", body) 258 | } 259 | } 260 | 261 | func TestTokenSigner_error(t *testing.T) { 262 | ts := TokenSigner( 263 | func(_ *config.EndpointConfig, _ proxy.Proxy) gin.HandlerFunc { 264 | return func(_ *gin.Context) { 265 | t.Error("the injected handler should not be called") 266 | } 267 | }, 268 | logging.NoOp, 269 | ) 270 | 271 | gin.SetMode(gin.TestMode) 272 | r := gin.New() 273 | r.GET("/", ts(&config.EndpointConfig{ExtraConfig: config.ExtraConfig{jose.SignerNamespace: config.ExtraConfig{}}}, proxy.NoopProxy)) 274 | 275 | w := httptest.NewRecorder() 276 | req, _ := http.NewRequest(http.MethodGet, "/", http.NoBody) 277 | r.ServeHTTP(w, req) 278 | 279 | if w.Code != http.StatusUnauthorized { 280 | t.Errorf("unexpected status code: %d", w.Code) 281 | } 282 | } 283 | 284 | func TestTokenSignatureValidator_error(t *testing.T) { 285 | ts := TokenSignatureValidator( 286 | func(_ *config.EndpointConfig, _ proxy.Proxy) gin.HandlerFunc { 287 | return func(_ *gin.Context) { 288 | t.Error("the injected handler should not be called") 289 | } 290 | }, 291 | logging.NoOp, 292 | nil, 293 | ) 294 | 295 | gin.SetMode(gin.TestMode) 296 | r := gin.New() 297 | r.GET("/", ts(&config.EndpointConfig{ExtraConfig: config.ExtraConfig{jose.ValidatorNamespace: config.ExtraConfig{}}}, proxy.NoopProxy)) 298 | 299 | w := httptest.NewRecorder() 300 | req, _ := http.NewRequest(http.MethodGet, "/", http.NoBody) 301 | r.ServeHTTP(w, req) 302 | 303 | if w.Code != http.StatusUnauthorized { 304 | t.Errorf("unexpected status code: %d", w.Code) 305 | } 306 | } 307 | 308 | func jwkEndpoint(name string) http.HandlerFunc { 309 | data, err := os.ReadFile("../fixtures/" + name + ".json") 310 | return func(rw http.ResponseWriter, _ *http.Request) { 311 | if err != nil { 312 | rw.WriteHeader(500) 313 | return 314 | } 315 | rw.Header().Set("Content-Type", "application/json") 316 | rw.Write(data) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/krakendio/krakend-jose/v2 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/gin-gonic/gin v1.9.1 9 | github.com/go-jose/go-jose/v3 v3.0.4 10 | github.com/krakend/go-auth0/v2 v2.0.1 11 | github.com/luraproject/lura/v2 v2.7.0 12 | gocloud.dev v0.39.0 13 | gocloud.dev/secrets/hashivault v0.39.0 14 | ) 15 | 16 | require ( 17 | cloud.google.com/go v0.115.0 // indirect 18 | cloud.google.com/go/auth v0.8.1 // indirect 19 | cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect 20 | cloud.google.com/go/compute/metadata v0.5.0 // indirect 21 | cloud.google.com/go/iam v1.1.13 // indirect 22 | cloud.google.com/go/kms v1.18.5 // indirect 23 | cloud.google.com/go/longrunning v0.5.12 // indirect 24 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect 25 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect 26 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect 27 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect 28 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect 29 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect 30 | github.com/aws/aws-sdk-go v1.55.5 // indirect 31 | github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect 32 | github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect 33 | github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect 34 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect 35 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect 36 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect 37 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 38 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect 40 | github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 // indirect 41 | github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect 42 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect 43 | github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect 44 | github.com/aws/smithy-go v1.20.3 // indirect 45 | github.com/bytedance/sonic v1.9.1 // indirect 46 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 47 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 48 | github.com/felixge/httpsnoop v1.0.4 // indirect 49 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 50 | github.com/gin-contrib/sse v0.1.0 // indirect 51 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 52 | github.com/go-logr/logr v1.4.2 // indirect 53 | github.com/go-logr/stdr v1.2.2 // indirect 54 | github.com/go-playground/locales v0.14.1 // indirect 55 | github.com/go-playground/universal-translator v0.18.1 // indirect 56 | github.com/go-playground/validator/v10 v10.14.0 // indirect 57 | github.com/goccy/go-json v0.10.2 // indirect 58 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 59 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 60 | github.com/google/s2a-go v0.1.8 // indirect 61 | github.com/google/uuid v1.6.0 // indirect 62 | github.com/google/wire v0.6.0 // indirect 63 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 64 | github.com/googleapis/gax-go/v2 v2.13.0 // indirect 65 | github.com/hashicorp/errwrap v1.1.0 // indirect 66 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 67 | github.com/hashicorp/go-multierror v1.1.1 // indirect 68 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 69 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 70 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect 71 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 72 | github.com/hashicorp/go-sockaddr v1.0.6 // indirect 73 | github.com/hashicorp/hcl v1.0.0 // indirect 74 | github.com/hashicorp/vault/api v1.14.0 // indirect 75 | github.com/jmespath/go-jmespath v0.4.0 // indirect 76 | github.com/json-iterator/go v1.1.12 // indirect 77 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 78 | github.com/krakendio/flatmap v1.1.1 // indirect 79 | github.com/kylelemons/godebug v1.1.0 // indirect 80 | github.com/leodido/go-urn v1.2.4 // indirect 81 | github.com/mattn/go-isatty v0.0.20 // indirect 82 | github.com/mitchellh/go-homedir v1.1.0 // indirect 83 | github.com/mitchellh/mapstructure v1.5.0 // indirect 84 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 85 | github.com/modern-go/reflect2 v1.0.2 // indirect 86 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 87 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 88 | github.com/ryanuber/go-glob v1.0.0 // indirect 89 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 90 | github.com/ugorji/go/codec v1.2.11 // indirect 91 | github.com/valyala/fastrand v1.1.0 // indirect 92 | go.opencensus.io v0.24.0 // indirect 93 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect 94 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 95 | go.opentelemetry.io/otel v1.28.0 // indirect 96 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 97 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 98 | golang.org/x/arch v0.3.0 // indirect 99 | golang.org/x/crypto v0.36.0 // indirect 100 | golang.org/x/net v0.38.0 // indirect 101 | golang.org/x/oauth2 v0.22.0 // indirect 102 | golang.org/x/sync v0.12.0 // indirect 103 | golang.org/x/sys v0.31.0 // indirect 104 | golang.org/x/text v0.23.0 // indirect 105 | golang.org/x/time v0.6.0 // indirect 106 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect 107 | google.golang.org/api v0.191.0 // indirect 108 | google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect 109 | google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 // indirect 110 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 // indirect 111 | google.golang.org/grpc v1.65.0 // indirect 112 | google.golang.org/protobuf v1.34.2 // indirect 113 | gopkg.in/yaml.v3 v3.0.1 // indirect 114 | ) 115 | 116 | replace github.com/dgrijalva/jwt-go v3.2.0+incompatible => github.com/golang-jwt/jwt v3.2.1+incompatible 117 | -------------------------------------------------------------------------------- /jose.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | jose "github.com/go-jose/go-jose/v3" 12 | "github.com/go-jose/go-jose/v3/jwt" 13 | "github.com/krakend/go-auth0/v2" 14 | "github.com/luraproject/lura/v2/proxy" 15 | ) 16 | 17 | var ErrNoHeadersToPropagate = fmt.Errorf("header propagation is disabled because there is no propagate_claims attribute") 18 | 19 | type ExtractorFactory func(string) func(r *http.Request) (*jwt.JSONWebToken, error) 20 | 21 | func NewValidator(signatureConfig *SignatureConfig, cookieEf, headerEf ExtractorFactory) (*auth0.JWTValidator, error) { 22 | sa, ok := supportedAlgorithms[signatureConfig.Alg] 23 | if !ok { 24 | return nil, fmt.Errorf("JOSE: unknown algorithm %s", signatureConfig.Alg) 25 | } 26 | te := auth0.FromMultiple( 27 | auth0.RequestTokenExtractorFunc(headerEf(signatureConfig.AuthHeaderName)), 28 | auth0.RequestTokenExtractorFunc(cookieEf(signatureConfig.CookieKey)), 29 | ) 30 | 31 | decodedFs, err := DecodeFingerprints(signatureConfig.Fingerprints) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | cfg := SecretProviderConfig{ 37 | URI: signatureConfig.URI, 38 | CacheEnabled: signatureConfig.CacheEnabled, 39 | CacheDuration: signatureConfig.CacheDuration, 40 | Fingerprints: decodedFs, 41 | Cs: signatureConfig.CipherSuites, 42 | LocalCA: signatureConfig.LocalCA, 43 | AllowInsecure: signatureConfig.DisableJWKSecurity, 44 | LocalPath: signatureConfig.LocalPath, 45 | SecretURL: signatureConfig.SecretURL, 46 | CipherKey: signatureConfig.CipherKey, 47 | KeyIdentifyStrategy: signatureConfig.KeyIdentifyStrategy, 48 | UnknownKeysTTL: signatureConfig.UnknownKeysTTL, 49 | } 50 | 51 | sp, err := SecretProvider(cfg, te) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | leeway, err := time.ParseDuration(signatureConfig.Leeway) 57 | if err != nil { 58 | leeway = time.Second 59 | } 60 | 61 | return auth0.NewValidatorWithLeeway( 62 | auth0.NewConfiguration( 63 | sp, 64 | signatureConfig.Audience, 65 | signatureConfig.Issuer, 66 | sa, 67 | ), 68 | te, 69 | leeway, 70 | ), nil 71 | } 72 | 73 | func CanAccessNested(roleKey string, claims map[string]interface{}, required []string) bool { 74 | if len(required) == 0 { 75 | return true 76 | } 77 | 78 | tmp := claims 79 | keys := strings.Split(roleKey, ".") 80 | 81 | for _, key := range keys[:len(keys)-1] { 82 | v, ok := tmp[key] 83 | if !ok { 84 | return false 85 | } 86 | tmp, ok = v.(map[string]interface{}) 87 | if !ok { 88 | return false 89 | } 90 | } 91 | return CanAccess(keys[len(keys)-1], tmp, required) 92 | } 93 | 94 | func CanAccess(roleKey string, claims map[string]interface{}, required []string) bool { 95 | if len(required) == 0 { 96 | return true 97 | } 98 | 99 | tmp, ok := claims[roleKey] 100 | if !ok { 101 | return false 102 | } 103 | 104 | roles, ok := tmp.([]interface{}) 105 | if ok { 106 | for _, role := range required { 107 | for _, r := range roles { 108 | if r.(string) == role { 109 | return true 110 | } 111 | } 112 | } 113 | return false 114 | } 115 | 116 | roleString, ok := tmp.(string) 117 | if !ok { 118 | return false 119 | } 120 | roless := strings.Split(roleString, " ") 121 | 122 | for _, role := range required { 123 | for _, r := range roless { 124 | if r == role { 125 | return true 126 | } 127 | } 128 | } 129 | return false 130 | } 131 | 132 | func getNestedClaim(nestedKey string, claims map[string]interface{}) (string, map[string]interface{}) { 133 | tmp := claims 134 | keys := strings.Split(nestedKey, ".") 135 | 136 | for _, key := range keys[:len(keys)-1] { 137 | v, ok := tmp[key] 138 | if !ok { 139 | return nestedKey, nil 140 | } 141 | tmp, ok = v.(map[string]interface{}) 142 | if !ok { 143 | return nestedKey, nil 144 | } 145 | } 146 | 147 | return keys[len(keys)-1], tmp 148 | } 149 | 150 | func ScopesAllMatcher(scopesKey string, claims map[string]interface{}, requiredScopes []string) bool { 151 | if len(requiredScopes) == 0 { 152 | return true 153 | } 154 | 155 | tmpClaims := claims 156 | tmpKey := scopesKey 157 | 158 | if strings.Contains(scopesKey, ".") { 159 | tmpKey, tmpClaims = getNestedClaim(scopesKey, claims) 160 | } 161 | 162 | tmp, ok := tmpClaims[tmpKey] 163 | if !ok { 164 | return false 165 | } 166 | 167 | matchAll := func(required []string, given []string) bool { 168 | for _, rScope := range required { 169 | matched := false 170 | for _, pScope := range given { 171 | if rScope == pScope { 172 | matched = true 173 | } 174 | } 175 | if !matched { // required scope was not found --> immediately return 176 | return false 177 | } 178 | } 179 | // all required scopes have been found in provided (claims) scopes 180 | return true 181 | } 182 | 183 | scopes, ok := tmp.([]interface{}) 184 | if ok { 185 | if len(scopes) > 0 { 186 | return matchAll(requiredScopes, convertToStringSlice(scopes)) 187 | } 188 | } 189 | 190 | scopeString, ok := tmp.(string) 191 | if !ok { 192 | return false 193 | } 194 | 195 | presentScopes := strings.Split(scopeString, " ") 196 | if len(presentScopes) > 0 { 197 | return matchAll(requiredScopes, presentScopes) 198 | } 199 | 200 | return false 201 | } 202 | 203 | func ScopesDefaultMatcher(_ string, _ map[string]interface{}, _ []string) bool { 204 | return true 205 | } 206 | 207 | func ScopesAnyMatcher(scopesKey string, claims map[string]interface{}, requiredScopes []string) bool { 208 | if len(requiredScopes) == 0 { 209 | return true 210 | } 211 | 212 | tmpClaims := claims 213 | tmpKey := scopesKey 214 | 215 | if strings.Contains(scopesKey, ".") { 216 | tmpKey, tmpClaims = getNestedClaim(scopesKey, claims) 217 | } 218 | 219 | tmp, ok := tmpClaims[tmpKey] 220 | if !ok { 221 | return false 222 | } 223 | 224 | matchAny := func(required []string, given []string) bool { 225 | for _, rScope := range required { 226 | for _, pScope := range given { 227 | if rScope == pScope { 228 | return true // found any of the required scopes --> return 229 | } 230 | } 231 | } 232 | 233 | // none of the scopes have been found in provided (claims) scopes 234 | return false 235 | } 236 | 237 | scopes, ok := tmp.([]interface{}) 238 | if ok { 239 | if len(scopes) > 0 { 240 | return matchAny(requiredScopes, convertToStringSlice(scopes)) 241 | } 242 | } 243 | 244 | scopeClaim, ok := tmp.(string) 245 | if !ok { 246 | return false 247 | } 248 | 249 | presentScopes := strings.Split(scopeClaim, " ") 250 | if len(presentScopes) > 0 { 251 | return matchAny(requiredScopes, presentScopes) 252 | } 253 | 254 | return false 255 | } 256 | 257 | func SignFields(keys []string, signer Signer, response *proxy.Response) error { 258 | for _, key := range keys { 259 | tmp, ok := response.Data[key] 260 | if !ok { 261 | continue 262 | } 263 | data, ok := tmp.(map[string]interface{}) 264 | if !ok { 265 | continue 266 | } 267 | token, err := signer(data) 268 | if err != nil { 269 | return err 270 | } 271 | response.Data[key] = token 272 | } 273 | return nil 274 | } 275 | 276 | type Claims map[string]interface{} 277 | 278 | const epsilon = 1e-6 279 | 280 | func (c Claims) Get(name string) (string, bool) { 281 | tmp, ok := c[name] 282 | if !ok { 283 | return "", ok 284 | } 285 | 286 | var normalized string 287 | 288 | switch v := tmp.(type) { 289 | case string: 290 | normalized = v 291 | case int: 292 | normalized = fmt.Sprintf("%d", v) 293 | case float64: 294 | if r := math.Round(v); math.Abs(v-r) <= epsilon { 295 | return fmt.Sprintf("%d", int(r)), ok 296 | } 297 | normalized = fmt.Sprintf("%f", v) 298 | case []interface{}: 299 | if len(v) > 0 { 300 | normalized = fmt.Sprintf("%v", v[0]) 301 | for _, elem := range v[1:] { 302 | normalized += fmt.Sprintf(",%v", elem) 303 | } 304 | } 305 | default: 306 | b, _ := json.Marshal(v) 307 | normalized = string(b) 308 | } 309 | 310 | return normalized, ok 311 | } 312 | 313 | func CalculateHeadersToPropagate(propagationCfg [][]string, claims map[string]interface{}) (map[string]string, error) { 314 | if len(propagationCfg) == 0 { 315 | return nil, ErrNoHeadersToPropagate 316 | } 317 | propagated := make(map[string]string) 318 | 319 | var err error 320 | for _, tuple := range propagationCfg { 321 | if len(tuple) != 2 { 322 | err = fmt.Errorf("invalid number of claims to propagate: %+v", tuple) 323 | continue 324 | } 325 | fromClaim := tuple[0] 326 | toHeader := tuple[1] 327 | 328 | c := Claims(claims) 329 | if strings.Contains(fromClaim, ".") && (len(fromClaim) < 4 || fromClaim[:4] != "http") { 330 | var claimsMap map[string]interface{} 331 | fromClaim, claimsMap = getNestedClaim(fromClaim, claims) 332 | c = Claims(claimsMap) 333 | } 334 | v, _ := c.Get(fromClaim) 335 | propagated[toHeader] = v 336 | } 337 | 338 | return propagated, err 339 | } 340 | 341 | var supportedAlgorithms = map[string]jose.SignatureAlgorithm{ 342 | "EdDSA": jose.EdDSA, 343 | "HS256": jose.HS256, 344 | "HS384": jose.HS384, 345 | "HS512": jose.HS512, 346 | "RS256": jose.RS256, 347 | "RS384": jose.RS384, 348 | "RS512": jose.RS512, 349 | "ES256": jose.ES256, 350 | "ES384": jose.ES384, 351 | "ES512": jose.ES512, 352 | "PS256": jose.PS256, 353 | "PS384": jose.PS384, 354 | "PS512": jose.PS512, 355 | } 356 | 357 | func SupportedAlgorithm(s string) (jose.SignatureAlgorithm, bool) { 358 | a, ok := supportedAlgorithms[s] 359 | return a, ok 360 | } 361 | 362 | func convertToStringSlice(input []interface{}) []string { 363 | result := make([]string, len(input)) 364 | 365 | for i, v := range input { 366 | result[i] = v.(string) 367 | } 368 | 369 | return result 370 | } 371 | -------------------------------------------------------------------------------- /jose_test.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/go-jose/go-jose/v3/jwt" 10 | ) 11 | 12 | func nopExtractor(_ string) func(r *http.Request) (*jwt.JSONWebToken, error) { 13 | return func(_ *http.Request) (*jwt.JSONWebToken, error) { return nil, nil } 14 | } 15 | 16 | func Test_NewValidator_unkownAlg(t *testing.T) { 17 | _, err := NewValidator(&SignatureConfig{ 18 | Alg: "random", 19 | }, nopExtractor, nopExtractor) 20 | if err == nil || err.Error() != "JOSE: unknown algorithm random" { 21 | t.Errorf("unexpected error: %v", err) 22 | } 23 | } 24 | 25 | func TestCanAccess(t *testing.T) { 26 | for _, v := range []struct { 27 | name string 28 | roleKey string 29 | claims map[string]interface{} 30 | requirements []string 31 | expected bool 32 | }{ 33 | { 34 | name: "simple_success", 35 | roleKey: "role", 36 | claims: map[string]interface{}{"role": []interface{}{"a", "b"}}, 37 | requirements: []string{"a"}, 38 | expected: true, 39 | }, 40 | { 41 | name: "simple_space_success", 42 | roleKey: "role", 43 | claims: map[string]interface{}{"role": "a b"}, 44 | requirements: []string{"a"}, 45 | expected: true, 46 | }, 47 | { 48 | name: "single_success", 49 | roleKey: "role", 50 | claims: map[string]interface{}{"role": "a"}, 51 | requirements: []string{"a"}, 52 | expected: true, 53 | }, 54 | { 55 | name: "simple_sfail", 56 | roleKey: "role", 57 | claims: map[string]interface{}{"role": []interface{}{"c", "b"}}, 58 | requirements: []string{"a"}, 59 | expected: false, 60 | }, 61 | { 62 | name: "multiple_success", 63 | roleKey: "role", 64 | claims: map[string]interface{}{"role": []interface{}{"c"}}, 65 | requirements: []string{"a", "b", "c"}, 66 | expected: true, 67 | }, 68 | } { 69 | t.Run(v.name, func(t *testing.T) { 70 | if res := CanAccess(v.roleKey, v.claims, v.requirements); res != v.expected { 71 | t.Errorf("'%s' have %v, want %v", v.name, res, v.expected) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func TestCanAccessNested(t *testing.T) { 78 | for _, v := range []struct { 79 | name string 80 | roleKey string 81 | claims map[string]interface{} 82 | requirements []string 83 | expected bool 84 | }{ 85 | { 86 | name: "simple_success", 87 | roleKey: "role", 88 | claims: map[string]interface{}{"role": []interface{}{"a", "b"}}, 89 | requirements: []string{"a"}, 90 | expected: true, 91 | }, 92 | { 93 | name: "simple_sfail", 94 | roleKey: "role", 95 | claims: map[string]interface{}{"role": []interface{}{"c", "b"}}, 96 | requirements: []string{"a"}, 97 | expected: false, 98 | }, 99 | { 100 | name: "multiple_success", 101 | roleKey: "role", 102 | claims: map[string]interface{}{"role": []interface{}{"c"}}, 103 | requirements: []string{"a", "b", "c"}, 104 | expected: true, 105 | }, 106 | { 107 | name: "struct_success", 108 | roleKey: "data.role", 109 | claims: map[string]interface{}{"data": map[string]interface{}{"role": []interface{}{"c"}}}, 110 | requirements: []string{"a", "b", "c"}, 111 | expected: true, 112 | }, 113 | { 114 | name: "complex_struct_success", 115 | roleKey: "data.data.data.data.data.data.data.role", 116 | claims: map[string]interface{}{ 117 | "data": map[string]interface{}{ 118 | "data": map[string]interface{}{ 119 | "data": map[string]interface{}{ 120 | "data": map[string]interface{}{ 121 | "data": map[string]interface{}{ 122 | "data": map[string]interface{}{ 123 | "data": map[string]interface{}{ 124 | "role": []interface{}{"c"}, 125 | }, 126 | }, 127 | }, 128 | }, 129 | }, 130 | }, 131 | }, 132 | }, 133 | requirements: []string{"a", "b", "c"}, 134 | expected: true, 135 | }, 136 | } { 137 | t.Run(v.name, func(t *testing.T) { 138 | if res := CanAccessNested(v.roleKey, v.claims, v.requirements); res != v.expected { 139 | t.Errorf("'%s' have %v, want %v", v.name, res, v.expected) 140 | } 141 | }) 142 | } 143 | } 144 | 145 | func TestScopesAllMatcher(t *testing.T) { 146 | for _, v := range []struct { 147 | name string 148 | scopesKey string 149 | claims map[string]interface{} 150 | requiredScopes []string 151 | expected bool 152 | }{ 153 | { 154 | name: "all_simple_success_for_scope_slice", 155 | scopesKey: "scope", 156 | claims: map[string]interface{}{"scope": []interface{}{"a", "b"}}, 157 | requiredScopes: []string{"a", "b"}, 158 | expected: true, 159 | }, 160 | { 161 | name: "all_simple_fail_for_scope_slice", 162 | scopesKey: "scope", 163 | claims: map[string]interface{}{"scope": []interface{}{"a", "b"}}, 164 | requiredScopes: []string{"c"}, 165 | expected: false, 166 | }, 167 | { 168 | name: "all_missingone_fail_for_scope_slice", 169 | scopesKey: "scope", 170 | claims: map[string]interface{}{"scope": []interface{}{"a", "b"}}, 171 | requiredScopes: []string{"a", "b", "c"}, 172 | expected: false, 173 | }, 174 | { 175 | name: "all_one_simple_success_for_scope_slice", 176 | scopesKey: "scope", 177 | claims: map[string]interface{}{"scope": []interface{}{"a", "b"}}, 178 | requiredScopes: []string{"b"}, 179 | expected: true, 180 | }, 181 | { 182 | name: "all_no_req_scopes_success_for_scope_slice", 183 | scopesKey: "scope", 184 | claims: map[string]interface{}{"scope": []interface{}{"a", "b"}}, 185 | requiredScopes: []string{}, 186 | expected: true, 187 | }, 188 | { 189 | name: "all_struct_success_for_scope_slice", 190 | scopesKey: "data.scope", 191 | claims: map[string]interface{}{"data": map[string]interface{}{"scope": []interface{}{"a", "b"}}}, 192 | requiredScopes: []string{"a", "b"}, 193 | expected: true, 194 | }, 195 | { 196 | name: "all_deep_struct_success_for_scope_slice", 197 | scopesKey: "data.data.data.data.data.data.data.scope", 198 | claims: map[string]interface{}{ 199 | "data": map[string]interface{}{ 200 | "data": map[string]interface{}{ 201 | "data": map[string]interface{}{ 202 | "data": map[string]interface{}{ 203 | "data": map[string]interface{}{ 204 | "data": map[string]interface{}{ 205 | "data": map[string]interface{}{ 206 | "scope": []interface{}{"a", "b"}, 207 | }, 208 | }, 209 | }, 210 | }, 211 | }, 212 | }, 213 | }, 214 | }, 215 | requiredScopes: []string{"a", "b"}, 216 | expected: true, 217 | }, 218 | { 219 | name: "all_simple_success", 220 | scopesKey: "scope", 221 | claims: map[string]interface{}{"scope": "a b"}, 222 | requiredScopes: []string{"a", "b"}, 223 | expected: true, 224 | }, 225 | { 226 | name: "all_simple_fail", 227 | scopesKey: "scope", 228 | claims: map[string]interface{}{"scope": "a b"}, 229 | requiredScopes: []string{"c"}, 230 | expected: false, 231 | }, 232 | { 233 | name: "all_missingone_fail", 234 | scopesKey: "scope", 235 | claims: map[string]interface{}{"scope": "a b"}, 236 | requiredScopes: []string{"a", "b", "c"}, 237 | expected: false, 238 | }, 239 | { 240 | name: "all_one_simple_success", 241 | scopesKey: "scope", 242 | claims: map[string]interface{}{"scope": "a b"}, 243 | requiredScopes: []string{"b"}, 244 | expected: true, 245 | }, 246 | { 247 | name: "all_no_req_scopes_success", 248 | scopesKey: "scope", 249 | claims: map[string]interface{}{"scope": "a b"}, 250 | requiredScopes: []string{}, 251 | expected: true, 252 | }, 253 | { 254 | name: "all_struct_success", 255 | scopesKey: "data.scope", 256 | claims: map[string]interface{}{"data": map[string]interface{}{"scope": "a b"}}, 257 | requiredScopes: []string{"a", "b"}, 258 | expected: true, 259 | }, 260 | { 261 | name: "all_deep_struct_success", 262 | scopesKey: "data.data.data.data.data.data.data.scope", 263 | claims: map[string]interface{}{ 264 | "data": map[string]interface{}{ 265 | "data": map[string]interface{}{ 266 | "data": map[string]interface{}{ 267 | "data": map[string]interface{}{ 268 | "data": map[string]interface{}{ 269 | "data": map[string]interface{}{ 270 | "data": map[string]interface{}{ 271 | "scope": "a b", 272 | }, 273 | }, 274 | }, 275 | }, 276 | }, 277 | }, 278 | }, 279 | }, 280 | requiredScopes: []string{"a", "b"}, 281 | expected: true, 282 | }, 283 | } { 284 | t.Run(v.name, func(t *testing.T) { 285 | if res := ScopesAllMatcher(v.scopesKey, v.claims, v.requiredScopes); res != v.expected { 286 | t.Errorf("'%s' have %v, want %v", v.name, res, v.expected) 287 | } 288 | }) 289 | } 290 | } 291 | 292 | func TestScopesAnyMatcher(t *testing.T) { 293 | for _, v := range []struct { 294 | name string 295 | scopesKey string 296 | claims map[string]interface{} 297 | requiredScopes []string 298 | expected bool 299 | }{ 300 | { 301 | name: "any_simple_success_for_scope_slice", 302 | scopesKey: "scope", 303 | claims: map[string]interface{}{"scope": []interface{}{"a", "b"}}, 304 | requiredScopes: []string{"a", "b"}, 305 | expected: true, 306 | }, 307 | { 308 | name: "any_simple_fail_for_scope_slice", 309 | scopesKey: "scope", 310 | claims: map[string]interface{}{"scope": []interface{}{"a", "b"}}, 311 | requiredScopes: []string{"c"}, 312 | expected: false, 313 | }, 314 | { 315 | name: "any_missingone_success_for_scope_slice", 316 | scopesKey: "scope", 317 | claims: map[string]interface{}{"scope": []interface{}{"a"}}, 318 | requiredScopes: []string{"a", "b"}, 319 | expected: true, 320 | }, 321 | { 322 | name: "any_one_simple_success_for_scope_slice", 323 | scopesKey: "scope", 324 | claims: map[string]interface{}{"scope": []interface{}{"a", "b"}}, 325 | requiredScopes: []string{"b"}, 326 | expected: true, 327 | }, 328 | { 329 | name: "any_no_req_scopes_success_for_scope_slice", 330 | scopesKey: "scope", 331 | claims: map[string]interface{}{"scope": []interface{}{"a", "b"}}, 332 | requiredScopes: []string{}, 333 | expected: true, 334 | }, 335 | { 336 | name: "any_struct_success_for_scope_slice", 337 | scopesKey: "data.scope", 338 | claims: map[string]interface{}{"data": map[string]interface{}{"scope": []interface{}{"a"}}}, 339 | requiredScopes: []string{"a", "b"}, 340 | expected: true, 341 | }, 342 | { 343 | name: "any_deep_struct_success_for_scope_slice", 344 | scopesKey: "data.data.data.data.data.data.data.scope", 345 | claims: map[string]interface{}{ 346 | "data": map[string]interface{}{ 347 | "data": map[string]interface{}{ 348 | "data": map[string]interface{}{ 349 | "data": map[string]interface{}{ 350 | "data": map[string]interface{}{ 351 | "data": map[string]interface{}{ 352 | "data": map[string]interface{}{ 353 | "scope": []interface{}{"a"}, 354 | }, 355 | }, 356 | }, 357 | }, 358 | }, 359 | }, 360 | }, 361 | }, 362 | requiredScopes: []string{"a", "b"}, 363 | expected: true, 364 | }, 365 | { 366 | name: "any_simple_success", 367 | scopesKey: "scope", 368 | claims: map[string]interface{}{"scope": "a b"}, 369 | requiredScopes: []string{"a", "b"}, 370 | expected: true, 371 | }, 372 | { 373 | name: "any_simple_fail", 374 | scopesKey: "scope", 375 | claims: map[string]interface{}{"scope": "a b"}, 376 | requiredScopes: []string{"c"}, 377 | expected: false, 378 | }, 379 | { 380 | name: "any_missingone_success", 381 | scopesKey: "scope", 382 | claims: map[string]interface{}{"scope": "a"}, 383 | requiredScopes: []string{"a", "b"}, 384 | expected: true, 385 | }, 386 | { 387 | name: "any_one_simple_success", 388 | scopesKey: "scope", 389 | claims: map[string]interface{}{"scope": "a b"}, 390 | requiredScopes: []string{"b"}, 391 | expected: true, 392 | }, 393 | { 394 | name: "any_no_req_scopes_success", 395 | scopesKey: "scope", 396 | claims: map[string]interface{}{"scope": "a b"}, 397 | requiredScopes: []string{}, 398 | expected: true, 399 | }, 400 | { 401 | name: "any_struct_success", 402 | scopesKey: "data.scope", 403 | claims: map[string]interface{}{"data": map[string]interface{}{"scope": "a"}}, 404 | requiredScopes: []string{"a", "b"}, 405 | expected: true, 406 | }, 407 | { 408 | name: "any_deep_struct_success", 409 | scopesKey: "data.data.data.data.data.data.data.scope", 410 | claims: map[string]interface{}{ 411 | "data": map[string]interface{}{ 412 | "data": map[string]interface{}{ 413 | "data": map[string]interface{}{ 414 | "data": map[string]interface{}{ 415 | "data": map[string]interface{}{ 416 | "data": map[string]interface{}{ 417 | "data": map[string]interface{}{ 418 | "scope": "a", 419 | }, 420 | }, 421 | }, 422 | }, 423 | }, 424 | }, 425 | }, 426 | }, 427 | requiredScopes: []string{"a", "b"}, 428 | expected: true, 429 | }, 430 | } { 431 | t.Run(v.name, func(t *testing.T) { 432 | if res := ScopesAnyMatcher(v.scopesKey, v.claims, v.requiredScopes); res != v.expected { 433 | t.Errorf("'%s' have %v, want %v", v.name, res, v.expected) 434 | } 435 | }) 436 | } 437 | } 438 | 439 | func TestCalculateHeadersToPropagate(t *testing.T) { 440 | for i, tc := range []struct { 441 | cfg [][]string 442 | claims map[string]interface{} 443 | expected map[string]string 444 | }{ 445 | { 446 | cfg: [][]string{ 447 | {"a", "x-a"}, 448 | {"b", "x-b"}, 449 | {"c", "x-c"}, 450 | {"d.d", "x-d"}, 451 | {"d.d.c", "x-e"}, 452 | {"d.f", "x-f"}, 453 | }, 454 | claims: map[string]interface{}{ 455 | "a": 1, 456 | "b": "foo", 457 | "c": []interface{}{"one", "two"}, 458 | "d": map[string]interface{}{ 459 | "a": 1, 460 | "b": "foo", 461 | "c": []interface{}{"one", "two"}, 462 | "d": map[string]interface{}{ 463 | "a": 1, 464 | "b": "foo", 465 | "c": []interface{}{"one", "two"}, 466 | }, 467 | }, 468 | }, 469 | expected: map[string]string{ 470 | "x-a": "1", 471 | "x-b": "foo", 472 | "x-c": "one,two", 473 | "x-d": `{"a":1,"b":"foo","c":["one","two"]}`, 474 | "x-e": "one,two", 475 | "x-f": "", 476 | }, 477 | }, 478 | } { 479 | res, err := CalculateHeadersToPropagate(tc.cfg, tc.claims) 480 | if err != nil { 481 | t.Errorf("tc-%d: unexpected error: %v", i, err) 482 | continue 483 | } 484 | 485 | if !reflect.DeepEqual(tc.expected, res) { 486 | t.Errorf("tc-%d: got: %v want: %v", i, res, tc.expected) 487 | } 488 | } 489 | } 490 | 491 | func TestUnmarshalDataTypesGetClaim(t *testing.T) { 492 | var c Claims 493 | json.Unmarshal([]byte(`{ 494 | "t0_int": 42, 495 | "t1_int": 0, 496 | "t2_int": -42, 497 | "t3_float": -42.42, 498 | "t4_string": "string val", 499 | "t5_string": "d0052a8b-6b35-4cb4-af69-b95e241e7208", 500 | "t6_array": ["item 1", "item-2", 1, -2, 2.99, -3.01], 501 | "t7_big_int": 1000001, 502 | "t8_float_round": 4.000001, 503 | "t9_float_round": 4.0000001, 504 | "t10_timestamp": 1651529725, 505 | "t11_array": [] 506 | }`), &c) 507 | 508 | for i, tc := range []struct { 509 | key string 510 | expected string 511 | }{ 512 | { 513 | key: "t0_int", 514 | expected: "42", 515 | }, 516 | { 517 | key: "t1_int", 518 | expected: "0", 519 | }, 520 | { 521 | key: "t2_int", 522 | expected: "-42", 523 | }, 524 | { 525 | key: "t3_float", 526 | expected: "-42.420000", 527 | }, 528 | { 529 | key: "t4_string", 530 | expected: "string val", 531 | }, 532 | { 533 | key: "t5_string", 534 | expected: "d0052a8b-6b35-4cb4-af69-b95e241e7208", 535 | }, 536 | { 537 | key: "t6_array", 538 | expected: "item 1,item-2,1,-2,2.99,-3.01", 539 | }, 540 | { 541 | key: "t7_big_int", 542 | expected: "1000001", 543 | }, 544 | { 545 | key: "t8_float_round", 546 | expected: "4.000001", 547 | }, 548 | { 549 | key: "t9_float_round", 550 | expected: "4", 551 | }, 552 | { 553 | key: "t10_timestamp", 554 | expected: "1651529725", 555 | }, 556 | { 557 | key: "t11_array", 558 | expected: "", 559 | }, 560 | } { 561 | t.Run(tc.key, func(t *testing.T) { 562 | res, ok := c.Get(tc.key) 563 | if !ok || !reflect.DeepEqual(tc.expected, res) { 564 | t.Errorf("Test %d - Claim %s: unexpected value: %v", i, tc.key, res) 565 | } 566 | }) 567 | } 568 | } 569 | -------------------------------------------------------------------------------- /jwk.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/sha256" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "encoding/base64" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "log" 14 | "net" 15 | "net/http" 16 | "os" 17 | "runtime" 18 | "sync" 19 | "time" 20 | 21 | jose "github.com/go-jose/go-jose/v3" 22 | auth0 "github.com/krakend/go-auth0/v2" 23 | "github.com/luraproject/lura/v2/core" 24 | 25 | "github.com/krakendio/krakend-jose/v2/secrets" 26 | ) 27 | 28 | type SecretProviderConfig struct { 29 | URI string 30 | CacheEnabled bool 31 | CacheDuration uint32 32 | Fingerprints [][]byte 33 | Cs []uint16 34 | LocalCA string 35 | AllowInsecure bool 36 | LocalPath string 37 | SecretURL string 38 | CipherKey []byte 39 | KeyIdentifyStrategy string 40 | UnknownKeysTTL string 41 | } 42 | 43 | var ( 44 | ErrInsecureJWKSource = errors.New("JWK client is using an insecure connection to the JWK service") 45 | ErrPinnedKeyNotFound = errors.New("JWK client did not find a pinned key") 46 | 47 | cacheWorkers = runtime.GOMAXPROCS(-1) 48 | cacheSemaphore = make(chan struct{}, cacheWorkers) 49 | cacheOnce = new(sync.Once) 50 | ) 51 | 52 | func SecretProvider(cfg SecretProviderConfig, te auth0.RequestTokenExtractor) (*JWKClient, error) { 53 | opts, err := newJWKClientOptions(cfg) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | if !cfg.CacheEnabled { 59 | if cfg.LocalPath == "" { 60 | return NewJWKClientWithCache(opts, te, NewMemoryKeyCacher(0, 0, opts.KeyIdentifyStrategy)), nil 61 | } 62 | return newLocalSecretProvider(opts, cfg, te) 63 | } 64 | 65 | if cfg.LocalPath != "" { 66 | return nil, fmt.Errorf("cache could not be used with jwk_local_path") 67 | } 68 | var cacheDuration time.Duration 69 | cacheDuration = time.Duration(cfg.CacheDuration) * time.Second 70 | // Set default duration to 15 minute 71 | if cacheDuration == 0 { 72 | cacheDuration = 15 * time.Minute 73 | } 74 | 75 | // init the semaphore 76 | cacheOnce.Do(func() { 77 | for i := 0; i < cacheWorkers; i++ { 78 | cacheSemaphore <- struct{}{} 79 | } 80 | }) 81 | 82 | client := NewJWKClientWithCache( 83 | opts, 84 | te, 85 | NewGlobalMemoryKeyCacher(cacheDuration, auth0.MaxCacheSizeNoCheck, opts.KeyIdentifyStrategy), 86 | ) 87 | 88 | // request an unexistent key in order to cache all the actual ones 89 | <-cacheSemaphore 90 | go func() { 91 | client.GetKey("unknown") 92 | cacheSemaphore <- struct{}{} 93 | }() 94 | 95 | return client, nil 96 | } 97 | 98 | func newLocalSecretProvider(opts JWKClientOptions, cfg SecretProviderConfig, te auth0.RequestTokenExtractor) (*JWKClient, error) { 99 | data, err := os.ReadFile(cfg.LocalPath) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | if cfg.SecretURL != "" { 105 | ctx := context.Background() 106 | sk, err := secrets.New(ctx, cfg.SecretURL) 107 | if err != nil { 108 | return nil, err 109 | } 110 | data, err = sk.Decrypt(ctx, data, cfg.CipherKey) 111 | if err != nil { 112 | return nil, err 113 | } 114 | sk.Close() 115 | } 116 | 117 | keyCacher, err := NewFileKeyCacher(data, opts.KeyIdentifyStrategy) 118 | if err != nil { 119 | return nil, err 120 | } 121 | return NewJWKClientWithCache(opts, te, keyCacher), nil 122 | } 123 | 124 | func NewFileKeyCacher(data []byte, keyIdentifyStrategy string) (*FileKeyCacher, error) { 125 | keys := jose.JSONWebKeySet{} 126 | if err := json.Unmarshal(data, &keys); err != nil { 127 | return nil, err 128 | } 129 | keyMap := map[string]*jose.JSONWebKey{} 130 | keyIDGetter := KeyIDGetterFactory(keyIdentifyStrategy) 131 | for _, k := range keys.Keys { 132 | keyToStore := k 133 | keyMap[keyIDGetter.Get(&keyToStore)] = &keyToStore 134 | } 135 | return &FileKeyCacher{keys: keyMap}, nil 136 | } 137 | 138 | type FileKeyCacher struct { 139 | keys map[string]*jose.JSONWebKey 140 | } 141 | 142 | func (f *FileKeyCacher) Get(keyID string) (*jose.JSONWebKey, error) { 143 | v, ok := f.keys[keyID] 144 | if !ok { 145 | return nil, fmt.Errorf("key '%s' not found in the key set", keyID) 146 | } 147 | return v, nil 148 | } 149 | 150 | func (f *FileKeyCacher) Add(keyID string, _ []jose.JSONWebKey) (*jose.JSONWebKey, error) { 151 | return f.keys[keyID], nil 152 | } 153 | 154 | func newJWKClientOptions(cfg SecretProviderConfig) (JWKClientOptions, error) { 155 | if len(cfg.Cs) == 0 { 156 | cfg.Cs = DefaultEnabledCipherSuites 157 | } 158 | 159 | rootCAs, _ := x509.SystemCertPool() 160 | if rootCAs == nil { 161 | rootCAs = x509.NewCertPool() 162 | } 163 | 164 | if cfg.LocalCA != "" { 165 | certs, err := os.ReadFile(cfg.LocalCA) 166 | if err != nil { 167 | return JWKClientOptions{}, fmt.Errorf("failed to append %q to RootCAs: %v", cfg.LocalCA, err) 168 | } 169 | rootCAs.AppendCertsFromPEM(certs) 170 | } 171 | 172 | tlsConfig := &tls.Config{ 173 | CipherSuites: cfg.Cs, 174 | MinVersion: tls.VersionTLS12, 175 | InsecureSkipVerify: cfg.AllowInsecure, // skipcq: GSC-G402 176 | RootCAs: rootCAs, 177 | } 178 | dialer := NewDialer(cfg, tlsConfig) 179 | 180 | transport := krakendTransport{ 181 | Transport: &http.Transport{ 182 | Proxy: http.ProxyFromEnvironment, 183 | DialContext: dialer.DialContext, 184 | MaxIdleConns: 10, 185 | IdleConnTimeout: 90 * time.Second, 186 | TLSHandshakeTimeout: 10 * time.Second, 187 | ExpectContinueTimeout: 1 * time.Second, 188 | TLSClientConfig: tlsConfig, 189 | }, 190 | } 191 | 192 | if len(cfg.Fingerprints) > 0 { 193 | transport.DialTLSContext = dialer.DialTLSContext 194 | } 195 | 196 | return JWKClientOptions{ 197 | JWKClientOptions: auth0.JWKClientOptions{ 198 | URI: cfg.URI, 199 | Client: &http.Client{ 200 | Transport: transport, 201 | }, 202 | }, 203 | KeyIdentifyStrategy: cfg.KeyIdentifyStrategy, 204 | UnknownKeysTTL: cfg.UnknownKeysTTL, 205 | }, nil 206 | } 207 | 208 | type krakendTransport struct { 209 | *http.Transport 210 | } 211 | 212 | func (k krakendTransport) RoundTrip(req *http.Request) (*http.Response, error) { 213 | req.Header.Set("User-Agent", core.KrakendUserAgent) 214 | return k.Transport.RoundTrip(req) 215 | } 216 | 217 | func DecodeFingerprints(in []string) ([][]byte, error) { 218 | out := make([][]byte, len(in)) 219 | for i, f := range in { 220 | r, err := base64.URLEncoding.DecodeString(f) 221 | if err != nil { 222 | return out, fmt.Errorf("decoding fingerprint #%d: %s", i, err.Error()) 223 | } 224 | out[i] = r 225 | } 226 | return out, nil 227 | } 228 | 229 | func NewDialer(cfg SecretProviderConfig, tlsConfig *tls.Config) *Dialer { 230 | return &Dialer{ 231 | dialer: &tls.Dialer{ 232 | NetDialer: &net.Dialer{ 233 | Timeout: 30 * time.Second, 234 | KeepAlive: 30 * time.Second, 235 | DualStack: true, 236 | }, 237 | Config: tlsConfig, 238 | }, 239 | fingerprints: cfg.Fingerprints, 240 | } 241 | } 242 | 243 | type Dialer struct { 244 | dialer *tls.Dialer 245 | fingerprints [][]byte 246 | } 247 | 248 | func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 249 | return d.dialer.NetDialer.DialContext(ctx, network, address) 250 | } 251 | 252 | func (d *Dialer) DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) { 253 | conn, err := d.dialer.DialContext(ctx, network, addr) 254 | if err != nil { 255 | return nil, err 256 | } 257 | c, ok := conn.(*tls.Conn) 258 | if !ok { 259 | return conn, errors.New("wrong connection type") 260 | } 261 | connstate := c.ConnectionState() 262 | keyPinValid := false 263 | for _, peercert := range connstate.PeerCertificates { 264 | der, err := x509.MarshalPKIXPublicKey(peercert.PublicKey) 265 | hash := sha256.Sum256(der) 266 | if err != nil { 267 | log.Fatal(err) 268 | } 269 | for _, fingerprint := range d.fingerprints { 270 | if bytes.Equal(hash[0:], fingerprint) { 271 | keyPinValid = true 272 | break 273 | } 274 | } 275 | } 276 | if !keyPinValid { 277 | return nil, ErrPinnedKeyNotFound 278 | } 279 | return c, nil 280 | } 281 | 282 | // DefaultEnabledCipherSuites is a collection of secure cipher suites to use 283 | var DefaultEnabledCipherSuites = []uint16{ 284 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 285 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 286 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 287 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 288 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 289 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 290 | // TLS 1.3 cipher suites. 291 | tls.TLS_AES_128_GCM_SHA256, 292 | tls.TLS_AES_256_GCM_SHA384, 293 | tls.TLS_CHACHA20_POLY1305_SHA256, 294 | } 295 | -------------------------------------------------------------------------------- /jwk_client.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "time" 7 | 8 | "github.com/go-jose/go-jose/v3" 9 | "github.com/go-jose/go-jose/v3/jwt" 10 | "github.com/krakend/go-auth0/v2" 11 | ) 12 | 13 | // TokenIDGetter extracts the keyID from the JSON web token 14 | type TokenIDGetter interface { 15 | Get(*jwt.JSONWebToken) string 16 | } 17 | 18 | // TokenKeyIDGetterFunc function conforming 19 | // to the TokenIDGetter interface. 20 | type TokenKeyIDGetterFunc func(*jwt.JSONWebToken) string 21 | 22 | // Extract calls f(r) 23 | func (f TokenKeyIDGetterFunc) Get(token *jwt.JSONWebToken) string { 24 | return f(token) 25 | } 26 | 27 | // DefaultTokenKeyIDGetter returns the default kid as the JSONWebKey key id 28 | func DefaultTokenKeyIDGetter(token *jwt.JSONWebToken) string { 29 | return token.Headers[0].KeyID 30 | } 31 | 32 | // X5TTokenKeyIDGetter extracts the key id from the jSONWebToken as the x5t 33 | func X5TTokenKeyIDGetter(token *jwt.JSONWebToken) string { 34 | x5t, ok := token.Headers[0].ExtraHeaders["x5t"].(string) 35 | if !ok { 36 | return token.Headers[0].KeyID 37 | } 38 | return x5t 39 | } 40 | 41 | // X5TS256TokenKeyIDGetter extracts the key id from the jSONWebToken as the x5t#S256 42 | func X5TS256TokenKeyIDGetter(token *jwt.JSONWebToken) string { 43 | x5t, ok := token.Headers[0].ExtraHeaders["x5t#S256"].(string) 44 | if !ok { 45 | return token.Headers[0].KeyID 46 | } 47 | return x5t 48 | } 49 | 50 | // CompoundX5TTokenKeyIDGetter extracts the key id from the jSONWebToken as a compound string of the kid and x5t 51 | func CompoundX5TTokenKeyIDGetter(token *jwt.JSONWebToken) string { 52 | return token.Headers[0].KeyID + X5TTokenKeyIDGetter(token) 53 | } 54 | 55 | // TokenIDGetterFactory returns the TokenIDGetter from the keyIdentifyStrategy configuration string 56 | func TokenIDGetterFactory(keyIdentifyStrategy string) TokenIDGetter { 57 | supportedKeyIdentifyStrategy := map[string]TokenKeyIDGetterFunc{ 58 | "kid": DefaultTokenKeyIDGetter, 59 | "x5t": X5TTokenKeyIDGetter, 60 | "x5t#S256": X5TS256TokenKeyIDGetter, 61 | "kid_x5t": CompoundX5TTokenKeyIDGetter, 62 | } 63 | 64 | if tokenGetter, ok := supportedKeyIdentifyStrategy[keyIdentifyStrategy]; ok { 65 | return tokenGetter 66 | } 67 | return TokenKeyIDGetterFunc(DefaultTokenKeyIDGetter) 68 | } 69 | 70 | type JWKClientOptions struct { 71 | auth0.JWKClientOptions 72 | KeyIdentifyStrategy string 73 | UnknownKeysTTL string 74 | } 75 | 76 | type JWKClient struct { 77 | *auth0.JWKClient 78 | extractor auth0.RequestTokenExtractor 79 | tokenIDGetter TokenIDGetter 80 | misses missTracker 81 | } 82 | 83 | // NewJWKClientWithCache creates a new JWKClient instance from the provided options and custom extractor and keycacher. 84 | // Passing nil to keyCacher will create a persistent key cacher. 85 | // the extractor is also saved in the extended JWKClient. 86 | func NewJWKClientWithCache(options JWKClientOptions, extractor auth0.RequestTokenExtractor, keyCacher auth0.KeyCacher) *JWKClient { 87 | c := &JWKClient{ 88 | JWKClient: auth0.NewJWKClientWithCache(options.JWKClientOptions, extractor, keyCacher), 89 | extractor: extractor, 90 | tokenIDGetter: TokenIDGetterFactory(options.KeyIdentifyStrategy), 91 | misses: noTracker, 92 | } 93 | 94 | if ttl, err := time.ParseDuration(options.UnknownKeysTTL); err == nil && ttl >= time.Second { 95 | c.misses = &memoryMissTracker{ 96 | keys: []unknownKey{}, 97 | mu: new(sync.Mutex), 98 | ttl: ttl, 99 | } 100 | } 101 | 102 | return c 103 | } 104 | 105 | // GetSecret implements the GetSecret method of the SecretProvider interface. 106 | func (j *JWKClient) GetSecret(r *http.Request) (interface{}, error) { 107 | token, err := j.extractor.Extract(r) 108 | if err != nil { 109 | return nil, err 110 | } 111 | return j.SecretFromToken(token) 112 | } 113 | 114 | // SecretFromToken implements the GetSecret method of the SecretProvider interface. 115 | func (j *JWKClient) SecretFromToken(token *jwt.JSONWebToken) (interface{}, error) { 116 | if len(token.Headers) < 1 { 117 | return nil, auth0.ErrNoJWTHeaders 118 | } 119 | keyID := j.tokenIDGetter.Get(token) 120 | return j.GetKey(keyID) 121 | } 122 | 123 | // GetKey wraps the internal key getter so it can manage the misses and avoid smashing the JWK 124 | // provider looking for unknown keys 125 | func (j *JWKClient) GetKey(keyID string) (jose.JSONWebKey, error) { 126 | if j.misses.Exists(keyID) { 127 | return jose.JSONWebKey{}, ErrNoKeyFound 128 | } 129 | 130 | k, err := j.JWKClient.GetKey(keyID) 131 | if err != nil { 132 | j.misses.Add(keyID) 133 | } 134 | return k, err 135 | } 136 | 137 | // missTracker is an interface defining the required signatures for tracking 138 | // keys missing from the received jwk 139 | type missTracker interface { 140 | Exists(string) bool 141 | Add(string) 142 | } 143 | 144 | // noopMissTracker is a missTracker that does nothing and always allows the client 145 | // to contact the jwk provider 146 | type noopMissTracker struct{} 147 | 148 | func (noopMissTracker) Exists(_ string) bool { return false } 149 | func (noopMissTracker) Add(_ string) {} 150 | 151 | var noTracker = noopMissTracker{} 152 | 153 | // memoryMissTracker is a missTracker that keeps a list of missed keys in the last TTL period. 154 | // When the Exists method is called, it maintain the size of the list, removing all the entries 155 | // stored for more than the defined TTL. 156 | type memoryMissTracker struct { 157 | keys []unknownKey 158 | mu *sync.Mutex 159 | ttl time.Duration 160 | } 161 | 162 | type unknownKey struct { 163 | name string 164 | time time.Time 165 | } 166 | 167 | // Exists looks for the key in the list and removes all evicted entries found before the required one. If the required is evicted, 168 | // it removes it and returns false, so the client can try to fetch it again. 169 | func (u *memoryMissTracker) Exists(key string) bool { 170 | u.mu.Lock() 171 | defer u.mu.Unlock() 172 | 173 | now := time.Now() 174 | cutPosition := -1 175 | var found bool 176 | 177 | for i, uk := range u.keys { 178 | evicted := now.Sub(uk.time) >= u.ttl 179 | if evicted { 180 | cutPosition = i 181 | } 182 | if uk.name == key { 183 | found = !evicted 184 | break 185 | } 186 | } 187 | 188 | if cutPosition == -1 { 189 | return found 190 | } 191 | 192 | if len(u.keys) > cutPosition+1 { 193 | u.keys = u.keys[cutPosition+1:] 194 | } else { 195 | u.keys = []unknownKey{} 196 | } 197 | 198 | return found 199 | } 200 | 201 | // Add appends a key and a timestamp to the end of the list of keys 202 | func (u *memoryMissTracker) Add(key string) { 203 | u.mu.Lock() 204 | u.keys = append(u.keys, unknownKey{name: key, time: time.Now()}) 205 | u.mu.Unlock() 206 | } 207 | -------------------------------------------------------------------------------- /jwk_client_test.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "sync" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | 11 | "github.com/krakend/go-auth0/v2" 12 | "github.com/luraproject/lura/v2/config" 13 | "github.com/luraproject/lura/v2/logging" 14 | ) 15 | 16 | func TestJWKClient_globalCache(t *testing.T) { 17 | jwk := []byte(`{ "keys": [{ 18 | "kty": "RSA", 19 | "e": "AQAB", 20 | "use": "sig", 21 | "kid": "8-2-2PBmlHKMo5tizxp-uw9pFrQQamfa1M1ZYMrAFZI", 22 | "alg": "RS256", 23 | "n": "n6p2fLU7PLwMvJ-xeukn-f5wrAdyZ0ZaFa6kanQzVBofacLs2l4FVe6_bcjw4VGWM2Ct3WgelZQUYVkFbqePODpMnV0lV8U4hxbIpMEJOJqY3tK48_PBIdEkl02DN8LaucK1Y7GpOlUZFrWAOM68TyWJTjkyc-yx0ibu2MFaGQoXacV7239Yei_x68iGBpQa2f9SYv8U5nJINdI1CuyccQp991qeskJATgn-UVqQfOfHDsUA2qud2yNOf5QKkvqqPEH_IXuTtPcf_yzVuco9rhhUW8q5bC4R0BxjCv9w4b-Q_UKjKEXQK5UlAuiWqWgmQbQO9Ne94EDFpjlkCtil2Q" 24 | }]}`) 25 | 26 | var count uint64 27 | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 28 | // Content-Type defined in https://datatracker.ietf.org/doc/html/rfc7517#section-8.5.1 29 | w.Header().Add("Content-Type", "application/jwk-set+json") 30 | atomic.AddUint64(&count, 1) 31 | w.Write(jwk) 32 | })) 33 | 34 | defer backend.Close() 35 | opts := JWKClientOptions{ 36 | JWKClientOptions: auth0.JWKClientOptions{ 37 | URI: backend.URL, 38 | }, 39 | } 40 | te := auth0.FromMultiple( 41 | auth0.RequestTokenExtractorFunc(auth0.FromHeader), 42 | ) 43 | cfg := config.ExtraConfig{ 44 | ValidatorNamespace: map[string]interface{}{ 45 | "shared_cache_duration": 3, 46 | }, 47 | } 48 | if err := SetGlobalCacher(logging.NoOp, cfg); err != nil { 49 | t.Error(err) 50 | return 51 | } 52 | for i := 0; i < 10; i++ { 53 | client := NewJWKClientWithCache( 54 | opts, 55 | te, 56 | NewGlobalMemoryKeyCacher(1*time.Second, auth0.MaxCacheSizeNoCheck, opts.KeyIdentifyStrategy), 57 | ) 58 | if _, err := client.GetKey("8-2-2PBmlHKMo5tizxp-uw9pFrQQamfa1M1ZYMrAFZI"); err != nil { 59 | t.Error(err) 60 | return 61 | } 62 | } 63 | if count != 1 { 64 | t.Errorf("invalid count %d", count) 65 | return 66 | } 67 | <-time.After(4 * time.Second) 68 | for i := 0; i < 10; i++ { 69 | client := NewJWKClientWithCache( 70 | opts, 71 | te, 72 | NewGlobalMemoryKeyCacher(1*time.Second, auth0.MaxCacheSizeNoCheck, opts.KeyIdentifyStrategy), 73 | ) 74 | if _, err := client.GetKey("8-2-2PBmlHKMo5tizxp-uw9pFrQQamfa1M1ZYMrAFZI"); err != nil { 75 | t.Error(err) 76 | return 77 | } 78 | } 79 | if count != 2 { 80 | t.Errorf("invalid count %d", count) 81 | } 82 | } 83 | 84 | func Test_memoryMissTracker(t *testing.T) { 85 | now := time.Now() 86 | uks := &memoryMissTracker{ 87 | mu: new(sync.Mutex), 88 | keys: []unknownKey{ 89 | { 90 | name: "key1", 91 | time: now.Add(-time.Hour), 92 | }, 93 | { 94 | name: "key2", 95 | time: now.Add(-2 * time.Minute), 96 | }, 97 | { 98 | name: "key3", 99 | time: now.Add(-time.Second), 100 | }, 101 | { 102 | name: "key4", 103 | time: now.Add(-time.Millisecond), 104 | }, 105 | }, 106 | ttl: time.Minute, 107 | } 108 | 109 | if uks.Exists("key1") { 110 | t.Errorf("key1 should not be present in list of misses %+v", uks) 111 | } 112 | 113 | if len(uks.keys) != 3 { 114 | t.Errorf("wrong size %+v", uks) 115 | } 116 | 117 | if !uks.Exists("key3") { 118 | t.Errorf("key3 should be present in list of misses %+v", uks) 119 | } 120 | 121 | if uks.Exists("key2") { 122 | t.Errorf("key2 should not be present in list of misses %+v", uks) 123 | } 124 | 125 | if len(uks.keys) != 2 { 126 | t.Errorf("wrong size %+v", uks) 127 | } 128 | 129 | if uks.Exists("key1") { 130 | t.Errorf("key1 should not be present in list of misses %+v", uks) 131 | } 132 | 133 | if !uks.Exists("key4") { 134 | t.Errorf("key4 should be present in list of misses %+v", uks) 135 | } 136 | 137 | if !uks.Exists("key3") { 138 | t.Errorf("key3 should be present in list of misses %+v", uks) 139 | } 140 | 141 | if len(uks.keys) != 2 { 142 | t.Errorf("wrong size %+v", uks) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /jwk_example_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package jose 5 | 6 | import "fmt" 7 | 8 | func Example_Auth0Integration() { 9 | fs, _ := DecodeFingerprints([]string{"--MBgDH5WGvL9Bcn5Be30cRcL0f5O-NyoXuWtQdX1aI="}) 10 | cfg := SecretProviderConfig{ 11 | URI: "https://albert-test.auth0.com/.well-known/jwks.json", 12 | Fingerprints: fs, 13 | } 14 | client, _ := SecretProvider(cfg, nil) 15 | 16 | k, err := client.GetKey("MDNGMjU2M0U3RERFQUEwOUUzQUMwQ0NBN0Y1RUY0OEIxNTRDM0IxMw") 17 | fmt.Println("err:", err) 18 | fmt.Println("is public:", k.IsPublic()) 19 | fmt.Println("alg:", k.Algorithm) 20 | fmt.Println("id:", k.KeyID) 21 | // Output: 22 | // err: 23 | // is public: true 24 | // alg: RS256 25 | // id: MDNGMjU2M0U3RERFQUEwOUUzQUMwQ0NBN0Y1RUY0OEIxNTRDM0IxMw 26 | } 27 | 28 | func Example_Auth0Integration_badFingerprint() { 29 | cfg := SecretProviderConfig{ 30 | URI: "https://albert-test.auth0.com/.well-known/jwks.json", 31 | Fingerprints: [][]byte{make([]byte, 32)}, 32 | } 33 | client, _ := SecretProvider(cfg, nil) 34 | 35 | _, err := client.GetKey("MDNGMjU2M0U3RERFQUEwOUUzQUMwQ0NBN0Y1RUY0OEIxNTRDM0IxMw") 36 | fmt.Println("err:", err) 37 | // Output: 38 | // err: Get "https://albert-test.auth0.com/.well-known/jwks.json": JWK client did not find a pinned key 39 | } 40 | -------------------------------------------------------------------------------- /jwk_test.go: -------------------------------------------------------------------------------- 1 | //go:generate go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,localhost --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h 2 | package jose 3 | 4 | import ( 5 | "context" 6 | "crypto/rand" 7 | "crypto/tls" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "sync/atomic" 12 | "testing" 13 | "time" 14 | 15 | "github.com/krakendio/krakend-jose/v2/secrets" 16 | "github.com/luraproject/lura/v2/core" 17 | ) 18 | 19 | func TestJWK(t *testing.T) { 20 | cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") 21 | if err != nil { 22 | t.Error(err) 23 | return 24 | } 25 | 26 | for _, tc := range []struct { 27 | Name string 28 | Alg string 29 | ID []string 30 | }{ 31 | { 32 | Name: "public", 33 | ID: []string{"2011-04-29"}, 34 | Alg: "RS256", 35 | }, 36 | { 37 | Name: "public", 38 | ID: []string{"1"}, 39 | }, 40 | { 41 | Name: "private", 42 | ID: []string{"2011-04-29"}, 43 | Alg: "RS256", 44 | }, 45 | { 46 | Name: "private", 47 | ID: []string{"1"}, 48 | }, 49 | { 50 | Name: "symmetric", 51 | ID: []string{"sim2"}, 52 | Alg: "HS256", 53 | }, 54 | } { 55 | server := httptest.NewUnstartedServer(jwkEndpoint(tc.Name)) 56 | server.TLS = &tls.Config{ 57 | Certificates: []tls.Certificate{cert}, 58 | MinVersion: tls.VersionTLS13, 59 | } 60 | server.StartTLS() 61 | 62 | secretProvidr, err := SecretProvider(SecretProviderConfig{URI: server.URL, LocalCA: "cert.pem"}, nil) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | for _, k := range tc.ID { 67 | key, err := secretProvidr.GetKey(k) 68 | if err != nil { 69 | t.Errorf("[%s] extracting the key %s: %s", tc.Name, k, err.Error()) 70 | } 71 | if key.Algorithm != tc.Alg { 72 | t.Errorf("wrong alg. have: %s, want: %s", key.Algorithm, tc.Alg) 73 | } 74 | } 75 | server.Close() 76 | } 77 | } 78 | 79 | func TestJWK_file(t *testing.T) { 80 | for _, tc := range []struct { 81 | Name string 82 | Alg string 83 | ID string 84 | }{ 85 | { 86 | Name: "public", 87 | ID: "2011-04-29", 88 | Alg: "RS256", 89 | }, 90 | { 91 | Name: "public", 92 | ID: "1", 93 | }, 94 | { 95 | Name: "private", 96 | ID: "2011-04-29", 97 | Alg: "RS256", 98 | }, 99 | { 100 | Name: "private", 101 | ID: "1", 102 | }, 103 | { 104 | Name: "symmetric", 105 | ID: "sim2", 106 | Alg: "HS256", 107 | }, 108 | } { 109 | secretProvidr, err := SecretProvider( 110 | SecretProviderConfig{ 111 | URI: "", 112 | AllowInsecure: true, 113 | LocalPath: "./fixtures/" + tc.Name + ".json", 114 | }, 115 | nil, 116 | ) 117 | if err != nil { 118 | t.Error(err) 119 | } 120 | key, err := secretProvidr.GetKey(tc.ID) 121 | if err != nil { 122 | t.Errorf("[%s] extracting the key %s: %s", tc.Name, tc.ID, err.Error()) 123 | } 124 | if key.Algorithm != tc.Alg { 125 | t.Errorf("wrong alg. have: %s, want: %s", key.Algorithm, tc.Alg) 126 | } 127 | } 128 | } 129 | 130 | func TestJWK_cyperfile(t *testing.T) { 131 | ctx, cancel := context.WithCancel(context.Background()) 132 | defer cancel() 133 | 134 | url := "base64key://smGbjm71Nxd1Ig5FS0wj9SlbzAIrnolCz9bQQ6uAhl4=" 135 | 136 | cypher, err := secrets.New(ctx, url) 137 | if err != nil { 138 | t.Error(err) 139 | return 140 | } 141 | defer cypher.Close() 142 | 143 | plainKey := make([]byte, 32) 144 | rand.Read(plainKey) 145 | 146 | cypherKey, err := cypher.EncryptKey(ctx, plainKey) 147 | if err != nil { 148 | t.Error(err) 149 | return 150 | } 151 | 152 | b, _ := os.ReadFile("./fixtures/private.json") 153 | cypherText, err := cypher.Encrypt(ctx, b, cypherKey) 154 | if err != nil { 155 | t.Error(err) 156 | return 157 | } 158 | os.WriteFile("./fixtures/private.txt", cypherText, 0600) 159 | defer os.Remove("./fixtures/private.txt") 160 | 161 | for k, tc := range []struct { 162 | Alg string 163 | ID string 164 | }{ 165 | { 166 | ID: "2011-04-29", 167 | Alg: "RS256", 168 | }, 169 | { 170 | ID: "1", 171 | }, 172 | } { 173 | secretProvidr, err := SecretProvider( 174 | SecretProviderConfig{ 175 | URI: "", 176 | AllowInsecure: true, 177 | LocalPath: "./fixtures/private.txt", 178 | CipherKey: cypherKey, 179 | SecretURL: url, 180 | }, 181 | nil, 182 | ) 183 | if err != nil { 184 | t.Error(err) 185 | } 186 | key, err := secretProvidr.GetKey(tc.ID) 187 | if err != nil { 188 | t.Errorf("[%d] extracting the key %s: %s", k, tc.ID, err.Error()) 189 | } 190 | if key.Algorithm != tc.Alg { 191 | t.Errorf("wrong alg. have: %s, want: %s", key.Algorithm, tc.Alg) 192 | } 193 | } 194 | } 195 | 196 | func TestJWK_cache(t *testing.T) { 197 | cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") 198 | if err != nil { 199 | t.Error(err) 200 | return 201 | } 202 | 203 | for _, tc := range []struct { 204 | Name string 205 | Alg string 206 | ID []string 207 | }{ 208 | { 209 | Name: "public", 210 | ID: []string{"2011-04-29"}, 211 | Alg: "RS256", 212 | }, 213 | { 214 | Name: "public", 215 | ID: []string{"1"}, 216 | }, 217 | { 218 | Name: "private", 219 | ID: []string{"2011-04-29"}, 220 | Alg: "RS256", 221 | }, 222 | { 223 | Name: "private", 224 | ID: []string{"1"}, 225 | }, 226 | { 227 | Name: "symmetric", 228 | ID: []string{"sim2"}, 229 | Alg: "HS256", 230 | }, 231 | } { 232 | var hits uint32 233 | server := httptest.NewUnstartedServer(jwkEndpointWithCounter(tc.Name, &hits)) 234 | server.TLS = &tls.Config{ 235 | Certificates: []tls.Certificate{cert}, 236 | MinVersion: tls.VersionTLS13, 237 | } 238 | server.StartTLS() 239 | 240 | cfg := SecretProviderConfig{ 241 | URI: server.URL, 242 | LocalCA: "cert.pem", 243 | CacheEnabled: true, 244 | } 245 | 246 | secretProvidr, err := SecretProvider(cfg, nil) 247 | if err != nil { 248 | t.Error(err) 249 | } 250 | 251 | // give some time to the concurrent cache warm up to complete 252 | <-time.After(100 * time.Millisecond) 253 | 254 | if hits != 1 { 255 | t.Errorf("wrong initial number of hits to the jwk endpoint: %d", hits) 256 | } 257 | 258 | for i := 0; i < 10; i++ { 259 | for _, k := range tc.ID { 260 | key, err := secretProvidr.GetKey(k) 261 | if err != nil { 262 | t.Errorf("[%s] extracting the key %s: %s", tc.Name, k, err.Error()) 263 | } 264 | if key.Algorithm != tc.Alg { 265 | t.Errorf("wrong alg. have: %s, want: %s", key.Algorithm, tc.Alg) 266 | } 267 | } 268 | } 269 | server.Close() 270 | 271 | if hits != 1 { 272 | t.Errorf("wrong number of hits to the jwk endpoint: %d", hits) 273 | } 274 | } 275 | } 276 | 277 | func TestDialer_DialTLS_ko(t *testing.T) { 278 | d := NewDialer(SecretProviderConfig{}, nil) 279 | c, err := d.DialTLSContext(context.Background(), "\t", "addr") 280 | if err == nil { 281 | t.Error(err) 282 | } 283 | if c != nil { 284 | t.Errorf("unexpected connection: %v", c) 285 | } 286 | } 287 | 288 | func Test_decodeFingerprints(t *testing.T) { 289 | _, err := DecodeFingerprints([]string{"not_encoded_message"}) 290 | if err == nil { 291 | t.Error(err) 292 | } 293 | } 294 | 295 | func TestNewFileKeyCacher(t *testing.T) { 296 | for _, tc := range []struct { 297 | Name string 298 | Alg string 299 | ID string 300 | }{ 301 | { 302 | Name: "public", 303 | ID: "2011-04-29", 304 | Alg: "RS256", 305 | }, 306 | { 307 | Name: "public", 308 | ID: "1", 309 | }, 310 | { 311 | Name: "private", 312 | ID: "2011-04-29", 313 | Alg: "RS256", 314 | }, 315 | { 316 | Name: "private", 317 | ID: "1", 318 | }, 319 | { 320 | Name: "symmetric", 321 | ID: "sim2", 322 | Alg: "HS256", 323 | }, 324 | } { 325 | b, err := os.ReadFile("./fixtures/" + tc.Name + ".json") 326 | if err != nil { 327 | t.Error(err) 328 | } 329 | kc, err := NewFileKeyCacher(b, "") 330 | if err != nil { 331 | t.Error(err) 332 | } 333 | if _, err := kc.Get(tc.ID); err != nil { 334 | t.Error(err) 335 | } 336 | } 337 | } 338 | 339 | func TestNewFileKeyCacher_unknownKey(t *testing.T) { 340 | b, err := os.ReadFile("./fixtures/symmetric.json") 341 | if err != nil { 342 | t.Error(err) 343 | } 344 | kc, err := NewFileKeyCacher(b, "") 345 | if err != nil { 346 | t.Error(err) 347 | } 348 | v, err := kc.Get("unknown") 349 | if err == nil { 350 | t.Error("error expected") 351 | } else if e := err.Error(); e != "key 'unknown' not found in the key set" { 352 | t.Error("unexpected error:", e) 353 | } 354 | if v != nil { 355 | t.Error("nil value expected") 356 | } 357 | } 358 | 359 | func jwkEndpoint(name string) http.HandlerFunc { 360 | data, err := os.ReadFile("./fixtures/" + name + ".json") 361 | return func(rw http.ResponseWriter, req *http.Request) { 362 | if err != nil { 363 | rw.WriteHeader(500) 364 | return 365 | } 366 | if ua := req.Header.Get("User-Agent"); ua != core.KrakendUserAgent { 367 | rw.WriteHeader(500) 368 | return 369 | } 370 | rw.Header().Set("Content-Type", "application/json") 371 | rw.Write(data) 372 | } 373 | } 374 | 375 | func jwkEndpointWithCounter(name string, hits *uint32) http.HandlerFunc { 376 | data, err := os.ReadFile("./fixtures/" + name + ".json") 377 | return func(rw http.ResponseWriter, _ *http.Request) { 378 | if err != nil { 379 | rw.WriteHeader(500) 380 | return 381 | } 382 | rw.Header().Set("Content-Type", "application/json") 383 | rw.Write(data) 384 | atomic.AddUint32(hits, 1) 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /jws.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/textproto" 8 | "strings" 9 | 10 | jose "github.com/go-jose/go-jose/v3" 11 | "github.com/krakend/go-auth0/v2" 12 | "github.com/luraproject/lura/v2/config" 13 | ) 14 | 15 | const ( 16 | ValidatorNamespace = "github.com/devopsfaith/krakend-jose/validator" 17 | SignerNamespace = "github.com/devopsfaith/krakend-jose/signer" 18 | defaultRolesKey = "roles" 19 | ) 20 | 21 | type SignatureConfig struct { 22 | Alg string `json:"alg"` 23 | AuthHeaderName string `json:"auth_header_name,omitempty"` 24 | URI string `json:"jwk_url"` 25 | CacheEnabled bool `json:"cache,omitempty"` 26 | CacheDuration uint32 `json:"cache_duration,omitempty"` 27 | Issuer string `json:"issuer,omitempty"` 28 | Audience []string `json:"audience,omitempty"` 29 | Roles []string `json:"roles,omitempty"` 30 | PropagateClaimsToHeader [][]string `json:"propagate_claims,omitempty"` 31 | RolesKey string `json:"roles_key,omitempty"` 32 | RolesKeyIsNested bool `json:"roles_key_is_nested,omitempty"` 33 | CookieKey string `json:"cookie_key,omitempty"` 34 | CipherSuites []uint16 `json:"cipher_suites,omitempty"` 35 | DisableJWKSecurity bool `json:"disable_jwk_security"` 36 | Fingerprints []string `json:"jwk_fingerprints,omitempty"` 37 | LocalCA string `json:"jwk_local_ca,omitempty"` 38 | LocalPath string `json:"jwk_local_path,omitempty"` 39 | SecretURL string `json:"secret_url,omitempty"` 40 | CipherKey []byte `json:"cypher_key,omitempty"` 41 | Scopes []string `json:"scopes,omitempty"` 42 | ScopesKey string `json:"scopes_key,omitempty"` 43 | ScopesMatcher string `json:"scopes_matcher,omitempty"` 44 | KeyIdentifyStrategy string `json:"key_identify_strategy"` 45 | OperationDebug bool `json:"operation_debug,omitempty"` 46 | Leeway string `json:"leeway"` 47 | UnknownKeysTTL string `json:"failed_jwk_key_cooldown"` 48 | } 49 | 50 | type SignerConfig struct { 51 | Alg string `json:"alg"` 52 | KeyID string `json:"kid"` 53 | Type string `json:"typ"` 54 | URI string `json:"jwk_url"` 55 | FullSerialization bool `json:"full,omitempty"` 56 | KeysToSign []string `json:"keys_to_sign,omitempty"` 57 | CipherSuites []uint16 `json:"cipher_suites,omitempty"` 58 | DisableJWKSecurity bool `json:"disable_jwk_security"` 59 | Fingerprints []string `json:"jwk_fingerprints,omitempty"` 60 | LocalCA string `json:"jwk_local_ca,omitempty"` 61 | LocalPath string `json:"jwk_local_path,omitempty"` 62 | SecretURL string `json:"secret_url,omitempty"` 63 | CipherKey []byte `json:"cypher_key,omitempty"` 64 | } 65 | 66 | var ( 67 | ErrNoValidatorCfg = errors.New("no validator config") 68 | ErrNoSignerCfg = errors.New("no signer config") 69 | ) 70 | 71 | func GetSignatureConfig(cfg *config.EndpointConfig) (*SignatureConfig, error) { 72 | tmp, ok := cfg.ExtraConfig[ValidatorNamespace] 73 | if !ok { 74 | return nil, ErrNoValidatorCfg 75 | } 76 | data, _ := json.Marshal(tmp) 77 | res := new(SignatureConfig) 78 | if err := json.Unmarshal(data, res); err != nil { 79 | return nil, err 80 | } 81 | 82 | if res.RolesKey == "" { 83 | res.RolesKey = defaultRolesKey 84 | } 85 | if !strings.HasPrefix(res.URI, "https://") && !res.DisableJWKSecurity { 86 | return res, ErrInsecureJWKSource 87 | } 88 | if res.AuthHeaderName != "" { 89 | res.AuthHeaderName = textproto.CanonicalMIMEHeaderKey(res.AuthHeaderName) 90 | } 91 | 92 | return res, nil 93 | } 94 | 95 | func getSignerConfig(cfg *config.EndpointConfig) (*SignerConfig, error) { 96 | tmp, ok := cfg.ExtraConfig[SignerNamespace] 97 | if !ok { 98 | return nil, ErrNoSignerCfg 99 | } 100 | data, _ := json.Marshal(tmp) 101 | res := new(SignerConfig) 102 | if err := json.Unmarshal(data, res); err != nil { 103 | return nil, err 104 | } 105 | if !strings.HasPrefix(res.URI, "https://") && !res.DisableJWKSecurity { 106 | return res, ErrInsecureJWKSource 107 | } 108 | return res, nil 109 | } 110 | 111 | func NewSigner(cfg *config.EndpointConfig, te auth0.RequestTokenExtractor) (*SignerConfig, Signer, error) { 112 | signerCfg, err := getSignerConfig(cfg) 113 | if err != nil { 114 | return signerCfg, nopSigner, err 115 | } 116 | 117 | decodedFs, err := DecodeFingerprints(signerCfg.Fingerprints) 118 | if err != nil { 119 | return signerCfg, nopSigner, err 120 | } 121 | 122 | spcfg := SecretProviderConfig{ 123 | URI: signerCfg.URI, 124 | Cs: signerCfg.CipherSuites, 125 | Fingerprints: decodedFs, 126 | LocalCA: signerCfg.LocalCA, 127 | AllowInsecure: signerCfg.DisableJWKSecurity, 128 | LocalPath: signerCfg.LocalPath, 129 | SecretURL: signerCfg.SecretURL, 130 | CipherKey: signerCfg.CipherKey, 131 | } 132 | 133 | sp, err := SecretProvider(spcfg, te) 134 | if err != nil { 135 | return signerCfg, nopSigner, err 136 | } 137 | key, err := sp.GetKey(signerCfg.KeyID) 138 | if err != nil { 139 | return signerCfg, nopSigner, err 140 | } 141 | // if key.IsPublic() { 142 | // // TODO: we should not sign with a public key 143 | // } 144 | signingKey := jose.SigningKey{ 145 | Key: key.Key, 146 | Algorithm: jose.SignatureAlgorithm(signerCfg.Alg), 147 | } 148 | opts := &jose.SignerOptions{ 149 | ExtraHeaders: map[jose.HeaderKey]interface{}{ 150 | jose.HeaderKey("kid"): key.KeyID, 151 | jose.HeaderKey("typ"): "JWT", 152 | }, 153 | } 154 | if signerCfg.Type != "" { 155 | opts.ExtraHeaders[jose.HeaderKey("typ")] = signerCfg.Type 156 | } 157 | s, err := jose.NewSigner(signingKey, opts) 158 | if err != nil { 159 | return signerCfg, nopSigner, err 160 | } 161 | 162 | if signerCfg.FullSerialization { 163 | return signerCfg, fullSerializeSigner{signer{s}}.Sign, nil 164 | } 165 | return signerCfg, compactSerializeSigner{signer{s}}.Sign, nil 166 | } 167 | 168 | type Signer func(interface{}) (string, error) 169 | 170 | func nopSigner(_ interface{}) (string, error) { return "", nil } 171 | 172 | type signer struct { 173 | signer jose.Signer 174 | } 175 | 176 | func (s signer) sign(v interface{}) (*jose.JSONWebSignature, error) { 177 | data, err := json.Marshal(v) 178 | if err != nil { 179 | return nil, fmt.Errorf("unable to serialize payload: %s", err.Error()) 180 | } 181 | return s.signer.Sign(data) 182 | } 183 | 184 | type fullSerializeSigner struct { 185 | signer 186 | } 187 | 188 | func (f fullSerializeSigner) Sign(v interface{}) (string, error) { 189 | obj, err := f.sign(v) 190 | if err != nil { 191 | return "", fmt.Errorf("unable to sign payload: %s", err.Error()) 192 | } 193 | return obj.FullSerialize(), nil 194 | } 195 | 196 | type compactSerializeSigner struct { 197 | signer 198 | } 199 | 200 | func (c compactSerializeSigner) Sign(v interface{}) (string, error) { 201 | obj, err := c.sign(v) 202 | if err != nil { 203 | return "", fmt.Errorf("unable to sign payload: %s", err.Error()) 204 | } 205 | return obj.CompactSerialize() 206 | } 207 | -------------------------------------------------------------------------------- /jws_test.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | "time" 7 | 8 | "github.com/go-jose/go-jose/v3" 9 | "github.com/luraproject/lura/v2/config" 10 | ) 11 | 12 | func Test_getSignatureConfig(t *testing.T) { 13 | server := httptest.NewServer(jwkEndpoint("private")) 14 | defer server.Close() 15 | 16 | scfg, err := GetSignatureConfig(newVerifierEndpointCfg("RS256", server.URL, []string{})) 17 | if err != nil { 18 | t.Error(err.Error()) 19 | return 20 | } 21 | 22 | if scfg.Issuer != "http://example.com" { 23 | t.Errorf("unexpected issuer: %s", scfg.Issuer) 24 | } 25 | 26 | if scfg.Audience[0] != "http://api.example.com" { 27 | t.Errorf("unexpected audience: %v", scfg.Audience) 28 | } 29 | } 30 | 31 | func Test_getSignatureConfig_unsecure(t *testing.T) { 32 | cfg := &config.EndpointConfig{ 33 | Timeout: time.Second, 34 | Endpoint: "/private", 35 | Backend: []*config.Backend{ 36 | { 37 | URLPattern: "/", 38 | Host: []string{"http://example.com/"}, 39 | Timeout: time.Second, 40 | }, 41 | }, 42 | ExtraConfig: config.ExtraConfig{ 43 | ValidatorNamespace: map[string]interface{}{ 44 | "alg": "RS256", 45 | "jwk_url": "http://jwk.example.com", 46 | "audience": []string{"http://api.example.com"}, 47 | "issuer": "http://example.com", 48 | "roles": []string{}, 49 | "cache": false, 50 | }, 51 | }, 52 | } 53 | 54 | _, err := GetSignatureConfig(cfg) 55 | if err != ErrInsecureJWKSource { 56 | t.Errorf("unexpected error: %v", err) 57 | } 58 | } 59 | 60 | func Test_getSignatureConfig_wrongStruct(t *testing.T) { 61 | cfg := &config.EndpointConfig{ 62 | Timeout: time.Second, 63 | Endpoint: "/private", 64 | Backend: []*config.Backend{ 65 | { 66 | URLPattern: "/", 67 | Host: []string{"http://example.com/"}, 68 | Timeout: time.Second, 69 | }, 70 | }, 71 | ExtraConfig: config.ExtraConfig{ 72 | ValidatorNamespace: true, 73 | }, 74 | } 75 | 76 | _, err := GetSignatureConfig(cfg) 77 | if err == nil || err.Error() != "json: cannot unmarshal bool into Go value of type jose.SignatureConfig" { 78 | t.Errorf("unexpected error: %v", err) 79 | } 80 | } 81 | 82 | func Test_newSigner(t *testing.T) { 83 | server := httptest.NewServer(jwkEndpoint("private")) 84 | defer server.Close() 85 | 86 | _, signer, err := NewSigner(newSignerEndpointCfg("RS256", "2011-04-29", server.URL), nil) 87 | if err != nil { 88 | t.Error(err.Error()) 89 | return 90 | } 91 | 92 | msg, err := signer(map[string]interface{}{ 93 | "aud": "http://api.example.com", 94 | "iss": "http://example.com", 95 | "sub": "1234567890qwertyuio", 96 | "jti": "mnb23vcsrt756yuiomnbvcx98ertyuiop", 97 | }) 98 | if err != nil { 99 | t.Error(err.Error()) 100 | return 101 | } 102 | 103 | expected := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cDovL2V4YW1wbGUuY29tIiwianRpIjoibW5iMjN2Y3NydDc1Nnl1aW9tbmJ2Y3g5OGVydHl1aW9wIiwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.n7R8h1gNVOxKQI05xt2TBGIjTvw-TwNa4zAj4lqFowIy9mqpFkyeZg7PrBKA-I3E4TKkXCjO5z4ibMEl28Jwpda1zEZKaojvcFs17MCR4YrlbszMY649qr8F0goRKo9SJmZFkkH4dNZp5oBD8uo6Q28V5uPYXMKGMbnOjModRnpznid-Qfiash3G9ktUC5fhkxev-uAb03c7Q3msJvkCxHpJdLSeJKHxfzAZQWr6i-8CoCZpggtTc6gILZ7dNpAsLHS2hElGFelCBlONEum2ihIjF7dEYGQDlvKR-WGEPsQgRglOKohIbKM9ewQQ0mWYyLoCpY0bKn6IEAIpIvAzcQ" 104 | if msg != expected { 105 | t.Errorf("unexpected signed payload: %s", msg) 106 | } 107 | } 108 | 109 | func Test_newSigner_unsecure(t *testing.T) { 110 | cfg := &config.EndpointConfig{ 111 | Timeout: time.Second, 112 | Endpoint: "/token", 113 | Method: "POST", 114 | Backend: []*config.Backend{ 115 | { 116 | URLPattern: "/token", 117 | Host: []string{"http://example.com/"}, 118 | Timeout: time.Second, 119 | }, 120 | }, 121 | ExtraConfig: config.ExtraConfig{ 122 | SignerNamespace: map[string]interface{}{ 123 | "alg": "RS256", 124 | "kid": "2011-04-29", 125 | "jwk_url": "http://jwk.example.com", 126 | "keys_to_sign": []string{"access_token", "refresh_token"}, 127 | }, 128 | }, 129 | } 130 | _, _, err := NewSigner(cfg, nil) 131 | if err != ErrInsecureJWKSource { 132 | t.Errorf("unexpected error: %v", err) 133 | } 134 | } 135 | 136 | func Test_newSigner_wrongStruct(t *testing.T) { 137 | cfg := &config.EndpointConfig{ 138 | Timeout: time.Second, 139 | Endpoint: "/token", 140 | Method: "POST", 141 | Backend: []*config.Backend{ 142 | { 143 | URLPattern: "/token", 144 | Host: []string{"http://example.com/"}, 145 | Timeout: time.Second, 146 | }, 147 | }, 148 | ExtraConfig: config.ExtraConfig{ 149 | SignerNamespace: true, 150 | }, 151 | } 152 | _, _, err := NewSigner(cfg, nil) 153 | if err == nil || err.Error() != "json: cannot unmarshal bool into Go value of type jose.SignerConfig" { 154 | t.Errorf("unexpected error: %v", err) 155 | } 156 | } 157 | 158 | func Test_newSigner_unknownKey(t *testing.T) { 159 | server := httptest.NewServer(jwkEndpoint("private")) 160 | defer server.Close() 161 | 162 | _, _, err := NewSigner(newSignerEndpointCfg("RS256", "unknown key", server.URL), nil) 163 | if err == nil || err.Error() != "no Keys have been found" { 164 | t.Errorf("unexpected error: %v", err) 165 | } 166 | } 167 | 168 | func Test_RSAPrivateSigner(t *testing.T) { 169 | testPrivateSigner( 170 | t, 171 | "private", 172 | "2011-04-29", 173 | `{"payload":"eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cDovL2V4YW1wbGUuY29tIiwianRpIjoibW5iMjN2Y3NydDc1Nnl1aW9tbmJ2Y3g5OGVydHl1aW9wIiwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9","protected":"eyJhbGciOiJSUzI1NiJ9","signature":"Cz7OEXmH6CsjFYFnGyrGMe7QsjrTk-QLTfP4VL6CZVpKKeVYKSI0NlquzlEGgwY3pujhdpQGVV2md3MvrccY6-a7-C8nRjyv4TnYkAk0lQcdmaG4hd38SwG0jZ6LpzgyL5l51txQATnayZgbRuUVzco-AZTPfTw9xS15CDPtFjoQHAKe9w9kvCGR6RmyOP29-YgqAk20hVqUj5EiFD_q-m2lTGYIAAYiWqYon661Ep9vfRNO1acq9ch_7qe1UBSmWu1BGiN2u7sq0rlkJ0Z4WKQL914eEiVBLRAUEsYpV-W-OBKmM3NMsm_2Ems0CcCaax0OrS0nHViuI4ZeT_molw"}`, 174 | "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cDovL2V4YW1wbGUuY29tIiwianRpIjoibW5iMjN2Y3NydDc1Nnl1aW9tbmJ2Y3g5OGVydHl1aW9wIiwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.Cz7OEXmH6CsjFYFnGyrGMe7QsjrTk-QLTfP4VL6CZVpKKeVYKSI0NlquzlEGgwY3pujhdpQGVV2md3MvrccY6-a7-C8nRjyv4TnYkAk0lQcdmaG4hd38SwG0jZ6LpzgyL5l51txQATnayZgbRuUVzco-AZTPfTw9xS15CDPtFjoQHAKe9w9kvCGR6RmyOP29-YgqAk20hVqUj5EiFD_q-m2lTGYIAAYiWqYon661Ep9vfRNO1acq9ch_7qe1UBSmWu1BGiN2u7sq0rlkJ0Z4WKQL914eEiVBLRAUEsYpV-W-OBKmM3NMsm_2Ems0CcCaax0OrS0nHViuI4ZeT_molw", 175 | ) 176 | } 177 | 178 | func Test_HSAPrivateSigner(t *testing.T) { 179 | testPrivateSigner( 180 | t, 181 | "symmetric", 182 | "sim2", 183 | `{"payload":"eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cDovL2V4YW1wbGUuY29tIiwianRpIjoibW5iMjN2Y3NydDc1Nnl1aW9tbmJ2Y3g5OGVydHl1aW9wIiwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9","protected":"eyJhbGciOiJIUzI1NiJ9","signature":"2eGKzqRiIJE5TJ4WcgnmopwhUczIdTFuQkp9ZVuFyUk"}`, 184 | "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cDovL2V4YW1wbGUuY29tIiwianRpIjoibW5iMjN2Y3NydDc1Nnl1aW9tbmJ2Y3g5OGVydHl1aW9wIiwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.2eGKzqRiIJE5TJ4WcgnmopwhUczIdTFuQkp9ZVuFyUk", 185 | ) 186 | } 187 | 188 | func testPrivateSigner(t *testing.T, keyType, keyName, full, compact string) { 189 | server := httptest.NewServer(jwkEndpoint(keyType)) 190 | defer server.Close() 191 | 192 | sp, err := SecretProvider(SecretProviderConfig{URI: server.URL}, nil) 193 | if err != nil { 194 | t.Error(err) 195 | return 196 | } 197 | key, err := sp.GetKey(keyName) 198 | if err != nil { 199 | t.Errorf("getting the key: %s", err.Error()) 200 | return 201 | } 202 | 203 | signingKey := jose.SigningKey{ 204 | Key: key.Key, 205 | Algorithm: jose.SignatureAlgorithm(key.Algorithm), 206 | } 207 | s, err := jose.NewSigner(signingKey, nil) 208 | if err != nil { 209 | t.Errorf("building the signer: %s", err.Error()) 210 | return 211 | } 212 | 213 | payload := map[string]interface{}{ 214 | "aud": "http://api.example.com", 215 | "iss": "http://example.com", 216 | "sub": "1234567890qwertyuio", 217 | "jti": "mnb23vcsrt756yuiomnbvcx98ertyuiop", 218 | } 219 | for _, tc := range []struct { 220 | Name string 221 | Signer Signer 222 | Expected string 223 | }{ 224 | { 225 | Name: keyType + "-full", 226 | Signer: fullSerializeSigner{signer{s}}.Sign, 227 | Expected: full, 228 | }, 229 | { 230 | Name: keyType + "-compact", 231 | Signer: compactSerializeSigner{signer{s}}.Sign, 232 | Expected: compact, 233 | }, 234 | } { 235 | data, err := tc.Signer(payload) 236 | if err != nil { 237 | t.Errorf("[%s] signing the payload: %s", tc.Name, err.Error()) 238 | return 239 | } 240 | if data != tc.Expected { 241 | t.Errorf("[%s] unexpected signed payload: %s", tc.Name, data) 242 | } 243 | } 244 | } 245 | 246 | func newSignerEndpointCfg(alg, ID, URL string) *config.EndpointConfig { 247 | return &config.EndpointConfig{ 248 | Timeout: time.Second, 249 | Endpoint: "/token", 250 | Method: "POST", 251 | Backend: []*config.Backend{ 252 | { 253 | URLPattern: "/token", 254 | Host: []string{"http://example.com/"}, 255 | Timeout: time.Second, 256 | }, 257 | }, 258 | ExtraConfig: config.ExtraConfig{ 259 | SignerNamespace: map[string]interface{}{ 260 | "alg": alg, 261 | "kid": ID, 262 | "jwk_url": URL, 263 | "keys_to_sign": []string{"access_token", "refresh_token"}, 264 | "disable_jwk_security": true, 265 | "cache": true, 266 | "cacheDuration": time.Minute, 267 | }, 268 | }, 269 | } 270 | } 271 | 272 | func newVerifierEndpointCfg(alg, URL string, roles []string) *config.EndpointConfig { 273 | return &config.EndpointConfig{ 274 | Timeout: time.Second, 275 | Endpoint: "/private", 276 | Backend: []*config.Backend{ 277 | { 278 | URLPattern: "/", 279 | Host: []string{"http://example.com/"}, 280 | Timeout: time.Second, 281 | }, 282 | }, 283 | ExtraConfig: config.ExtraConfig{ 284 | ValidatorNamespace: map[string]interface{}{ 285 | "alg": alg, 286 | "jwk_url": URL, 287 | "audience": []string{"http://api.example.com"}, 288 | "issuer": "http://example.com", 289 | "roles": roles, 290 | "disable_jwk_security": true, 291 | "cache": true, 292 | }, 293 | }, 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /key_cacher.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | b64 "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "sync" 8 | "time" 9 | 10 | "github.com/go-jose/go-jose/v3" 11 | "github.com/luraproject/lura/v2/config" 12 | "github.com/luraproject/lura/v2/logging" 13 | ) 14 | 15 | var ( 16 | ErrNoKeyFound = errors.New("no Keys have been found") 17 | ErrKeyExpired = errors.New("key exists but is expired") 18 | defaultGlobalCacheMaxAge uint32 = 900 19 | defaultStrategy = "kid" 20 | 21 | // Configuring with MaxKeyAgeNoCheck will skip key expiry check 22 | MaxKeyAgeNoCheck = time.Duration(-1) 23 | globalKeyCacher = map[string]GlobalCacher{} 24 | globalKeyCacherOnce = new(sync.Once) 25 | ) 26 | 27 | type GlobalCacher struct { 28 | kc KeyCacher 29 | mu *sync.RWMutex 30 | } 31 | 32 | func SetGlobalCacher(l logging.Logger, cfg config.ExtraConfig) error { 33 | scfg, err := configGetter(l, cfg) 34 | if err != nil { 35 | return err 36 | } 37 | duration := time.Duration(scfg.CacheDuration) * time.Second 38 | globalKeyCacherOnce.Do(func() { 39 | globalKeyCacher = map[string]GlobalCacher{ 40 | "kid": {kc: NewMemoryKeyCacher(duration, -1, "kid"), mu: new(sync.RWMutex)}, 41 | "x5t": {kc: NewMemoryKeyCacher(duration, -1, "x5t"), mu: new(sync.RWMutex)}, 42 | "x5t#S256": {kc: NewMemoryKeyCacher(duration, -1, "x5t#S256"), mu: new(sync.RWMutex)}, 43 | "kid_x5t": {kc: NewMemoryKeyCacher(duration, -1, "kid_x5t"), mu: new(sync.RWMutex)}, 44 | } 45 | }) 46 | return nil 47 | } 48 | 49 | type serviceConfig struct { 50 | CacheDuration uint32 `json:"shared_cache_duration"` 51 | } 52 | 53 | func configGetter(l logging.Logger, cfg config.ExtraConfig) (serviceConfig, error) { 54 | scfg := serviceConfig{} 55 | e, ok := cfg[ValidatorNamespace].(map[string]interface{}) 56 | if !ok { 57 | return scfg, ErrNoValidatorCfg 58 | } 59 | tmp, err := json.Marshal(e) 60 | if err != nil { 61 | return scfg, err 62 | } 63 | if err := json.Unmarshal(tmp, &scfg); err != nil { 64 | return scfg, err 65 | } 66 | if scfg.CacheDuration == 0 { 67 | scfg.CacheDuration = defaultGlobalCacheMaxAge 68 | l.Info("[SERVICE: JOSE] Empty shared_cache_duration, using default (15m)") 69 | } 70 | return scfg, nil 71 | } 72 | 73 | // KeyIDGetter extracts a key id from a JSONWebKey 74 | type KeyIDGetter interface { 75 | Get(*jose.JSONWebKey) string 76 | } 77 | 78 | // KeyIDGetterFunc function conforming to the KeyIDGetter interface. 79 | type KeyIDGetterFunc func(*jose.JSONWebKey) string 80 | 81 | // Get calls f(r) 82 | func (f KeyIDGetterFunc) Get(key *jose.JSONWebKey) string { 83 | return f(key) 84 | } 85 | 86 | // DefaultKeyIDGetter returns the default kid as JSONWebKey key id 87 | func DefaultKeyIDGetter(key *jose.JSONWebKey) string { 88 | return key.KeyID 89 | } 90 | 91 | // X5TKeyIDGetter extracts the key id from the jSONWebKey as the x5t 92 | func X5TKeyIDGetter(key *jose.JSONWebKey) string { 93 | return b64.RawURLEncoding.EncodeToString(key.CertificateThumbprintSHA1) 94 | } 95 | 96 | // X5TS256KeyIDGetter extracts the key id from the jSONWebKey as the x5t#S256 97 | func X5TS256KeyIDGetter(key *jose.JSONWebKey) string { 98 | return b64.RawURLEncoding.EncodeToString(key.CertificateThumbprintSHA256) 99 | } 100 | 101 | // CompoundX5TKeyIDGetter extracts the key id from the jSONWebKey as the a compound string of the kid and the x5t 102 | func CompoundX5TKeyIDGetter(key *jose.JSONWebKey) string { 103 | return key.KeyID + X5TKeyIDGetter(key) 104 | } 105 | 106 | func KeyIDGetterFactory(keyIdentifyStrategy string) KeyIDGetter { 107 | supportedKeyIdentifyStrategy := map[string]KeyIDGetterFunc{ 108 | "kid": DefaultKeyIDGetter, 109 | "x5t": X5TKeyIDGetter, 110 | "x5t#S256": X5TS256KeyIDGetter, 111 | "kid_x5t": CompoundX5TKeyIDGetter, 112 | } 113 | 114 | if keyGetter, ok := supportedKeyIdentifyStrategy[keyIdentifyStrategy]; ok { 115 | return keyGetter 116 | } 117 | return KeyIDGetterFunc(DefaultKeyIDGetter) 118 | } 119 | 120 | type KeyCacher interface { 121 | Get(keyID string) (*jose.JSONWebKey, error) 122 | Add(keyID string, webKeys []jose.JSONWebKey) (*jose.JSONWebKey, error) 123 | } 124 | 125 | type MemoryKeyCacher struct { 126 | entries map[string]keyCacherEntry 127 | maxKeyAge time.Duration 128 | maxCacheSize int 129 | keyIDGetter KeyIDGetter 130 | mu *sync.RWMutex 131 | } 132 | 133 | type keyCacherEntry struct { 134 | addedAt time.Time 135 | jose.JSONWebKey 136 | } 137 | 138 | type GMemoryKeyCacher struct { 139 | *MemoryKeyCacher 140 | Global GlobalCacher 141 | } 142 | 143 | func (gkc *GMemoryKeyCacher) Add(keyID string, downloadedKeys []jose.JSONWebKey) (*jose.JSONWebKey, error) { 144 | if gkc.Global.kc != nil { 145 | gkc.Global.mu.Lock() 146 | gkc.Global.kc.Add(keyID, downloadedKeys) 147 | gkc.Global.mu.Unlock() 148 | } 149 | 150 | return gkc.MemoryKeyCacher.Add(keyID, downloadedKeys) 151 | } 152 | 153 | // Get obtains a key from the cache, and checks if the key is expired 154 | func (gkc *GMemoryKeyCacher) Get(keyID string) (*jose.JSONWebKey, error) { 155 | k, err := gkc.MemoryKeyCacher.Get(keyID) 156 | if err == nil || gkc.Global.kc == nil { 157 | return k, err 158 | } 159 | 160 | gkc.Global.mu.RLock() 161 | v, err := gkc.Global.kc.Get(keyID) 162 | gkc.Global.mu.RUnlock() 163 | if err == nil { 164 | gkc.MemoryKeyCacher.Add(keyID, []jose.JSONWebKey{*v}) 165 | } 166 | return v, err 167 | } 168 | 169 | // NewMemoryKeyCacher creates a new Keycacher interface with option 170 | // to set max age of cached keys and max size of the cache. 171 | func NewMemoryKeyCacher(maxKeyAge time.Duration, maxCacheSize int, keyIdentifyStrategy string) *MemoryKeyCacher { 172 | return &MemoryKeyCacher{ 173 | entries: map[string]keyCacherEntry{}, 174 | maxKeyAge: maxKeyAge, 175 | maxCacheSize: maxCacheSize, 176 | keyIDGetter: KeyIDGetterFactory(keyIdentifyStrategy), 177 | mu: new(sync.RWMutex), 178 | } 179 | } 180 | 181 | func NewGlobalMemoryKeyCacher(maxKeyAge time.Duration, maxCacheSize int, keyIdentifyStrategy string) *GMemoryKeyCacher { 182 | kc := &GMemoryKeyCacher{ 183 | MemoryKeyCacher: NewMemoryKeyCacher(maxKeyAge, maxCacheSize, keyIdentifyStrategy), 184 | Global: GlobalCacher{}, 185 | } 186 | if keyIdentifyStrategy == "" { 187 | keyIdentifyStrategy = defaultStrategy 188 | } 189 | if len(globalKeyCacher) > 0 { 190 | g := globalKeyCacher[keyIdentifyStrategy] 191 | kc.Global = g 192 | } 193 | return kc 194 | } 195 | 196 | // Get obtains a key from the cache, and checks if the key is expired 197 | func (mkc *MemoryKeyCacher) Get(keyID string) (*jose.JSONWebKey, error) { 198 | mkc.mu.RLock() 199 | searchKey, ok := mkc.entries[keyID] 200 | mkc.mu.RUnlock() 201 | if ok { 202 | if mkc.maxKeyAge == MaxKeyAgeNoCheck || !mkc.keyIsExpired(keyID) { 203 | return &searchKey.JSONWebKey, nil 204 | } 205 | return nil, ErrKeyExpired 206 | } 207 | return nil, ErrNoKeyFound 208 | } 209 | 210 | // Add adds a key into the cache and handles overflow 211 | func (mkc *MemoryKeyCacher) Add(keyID string, downloadedKeys []jose.JSONWebKey) (*jose.JSONWebKey, error) { 212 | var addingKey jose.JSONWebKey 213 | var addingKeyID string 214 | for i := range downloadedKeys { 215 | cacheKey := mkc.keyIDGetter.Get(&downloadedKeys[i]) 216 | if cacheKey == keyID { 217 | addingKey = downloadedKeys[i] 218 | addingKeyID = cacheKey 219 | } 220 | if mkc.maxCacheSize == -1 { 221 | mkc.mu.Lock() 222 | mkc.entries[cacheKey] = keyCacherEntry{ 223 | addedAt: time.Now(), 224 | JSONWebKey: downloadedKeys[i], 225 | } 226 | mkc.mu.Unlock() 227 | } 228 | } 229 | if addingKey.Key != nil { 230 | if mkc.maxCacheSize != -1 { 231 | mkc.mu.Lock() 232 | mkc.entries[addingKeyID] = keyCacherEntry{ 233 | addedAt: time.Now(), 234 | JSONWebKey: addingKey, 235 | } 236 | mkc.mu.Unlock() 237 | mkc.handleOverflow() 238 | } 239 | return &addingKey, nil 240 | } 241 | return nil, ErrNoKeyFound 242 | } 243 | 244 | // keyIsExpired deletes the key from cache if it is expired 245 | func (mkc *MemoryKeyCacher) keyIsExpired(keyID string) bool { 246 | mkc.mu.RLock() 247 | entry := mkc.entries[keyID].addedAt.Add(mkc.maxKeyAge) 248 | mkc.mu.RUnlock() 249 | 250 | if time.Now().After(entry) { 251 | mkc.mu.Lock() 252 | delete(mkc.entries, keyID) 253 | mkc.mu.Unlock() 254 | return true 255 | } 256 | return false 257 | } 258 | 259 | // handleOverflow deletes the oldest key from the cache if overflowed 260 | func (mkc *MemoryKeyCacher) handleOverflow() { 261 | if mkc.maxCacheSize < len(mkc.entries) { 262 | mkc.mu.RLock() 263 | var oldestEntryKeyID string 264 | latestAddedTime := time.Now() 265 | for entryKeyID := range mkc.entries { 266 | if mkc.entries[entryKeyID].addedAt.Before(latestAddedTime) { 267 | latestAddedTime = mkc.entries[entryKeyID].addedAt 268 | oldestEntryKeyID = entryKeyID 269 | } 270 | } 271 | mkc.mu.RUnlock() 272 | 273 | mkc.mu.Lock() 274 | delete(mkc.entries, oldestEntryKeyID) 275 | mkc.mu.Unlock() 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /mux/jose.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/go-jose/go-jose/v3/jwt" 12 | "github.com/krakend/go-auth0/v2" 13 | krakendjose "github.com/krakendio/krakend-jose/v2" 14 | "github.com/luraproject/lura/v2/config" 15 | "github.com/luraproject/lura/v2/logging" 16 | "github.com/luraproject/lura/v2/proxy" 17 | muxlura "github.com/luraproject/lura/v2/router/mux" 18 | ) 19 | 20 | func HandlerFactory(hf muxlura.HandlerFactory, paramExtractor muxlura.ParamExtractor, logger logging.Logger, rejecterF krakendjose.RejecterFactory) muxlura.HandlerFactory { 21 | return TokenSignatureValidator(TokenSigner(hf, paramExtractor, logger), logger, rejecterF) 22 | } 23 | 24 | func TokenSigner(hf muxlura.HandlerFactory, paramExtractor muxlura.ParamExtractor, logger logging.Logger) muxlura.HandlerFactory { 25 | return func(cfg *config.EndpointConfig, prxy proxy.Proxy) http.HandlerFunc { 26 | signerCfg, signer, err := krakendjose.NewSigner(cfg, nil) 27 | if err == krakendjose.ErrNoSignerCfg { 28 | logger.Info("JOSE: signer disabled for the endpoint", cfg.Endpoint) 29 | return hf(cfg, prxy) 30 | } 31 | if err != nil { 32 | logger.Error("JOSE: unable to create the signer for the endpoint", cfg.Endpoint) 33 | logger.Error(err.Error()) 34 | return hf(cfg, prxy) 35 | } 36 | 37 | logger.Info("JOSE: signer enabled for the endpoint", cfg.Endpoint) 38 | 39 | return func(w http.ResponseWriter, r *http.Request) { 40 | proxyReq := muxlura.NewRequestBuilder(paramExtractor)(r, cfg.QueryString, cfg.HeadersToPass) 41 | ctx, cancel := context.WithTimeout(r.Context(), cfg.Timeout) 42 | defer cancel() 43 | 44 | response, err := prxy(ctx, proxyReq) 45 | if err != nil { 46 | logger.Error("proxy response error:", err.Error()) 47 | http.Error(w, "", http.StatusBadRequest) 48 | return 49 | } 50 | 51 | if response == nil { 52 | http.Error(w, "", http.StatusBadRequest) 53 | return 54 | } 55 | 56 | if err := krakendjose.SignFields(signerCfg.KeysToSign, signer, response); err != nil { 57 | logger.Error(err.Error()) 58 | http.Error(w, "", http.StatusBadRequest) 59 | return 60 | } 61 | 62 | for k, v := range response.Metadata.Headers { 63 | w.Header().Set(k, v[0]) 64 | } 65 | 66 | err = jsonRender(w, response) 67 | if err != nil { 68 | logger.Error("render answer error:", err.Error()) 69 | } 70 | } 71 | } 72 | } 73 | 74 | var emptyResponse = []byte("{}") 75 | 76 | func jsonRender(w http.ResponseWriter, response *proxy.Response) error { 77 | w.Header().Set("Content-Type", "application/json") 78 | 79 | if response == nil { 80 | _, err := w.Write(emptyResponse) 81 | return err 82 | } 83 | 84 | w.WriteHeader(response.Metadata.StatusCode) 85 | 86 | js, err := json.Marshal(response.Data) 87 | if err != nil { 88 | http.Error(w, err.Error(), http.StatusInternalServerError) 89 | return err 90 | } 91 | _, err = w.Write(js) 92 | return err 93 | } 94 | 95 | func TokenSignatureValidator(hf muxlura.HandlerFactory, logger logging.Logger, rejecterF krakendjose.RejecterFactory) muxlura.HandlerFactory { 96 | return func(cfg *config.EndpointConfig, prxy proxy.Proxy) http.HandlerFunc { 97 | if rejecterF == nil { 98 | rejecterF = new(krakendjose.NopRejecterFactory) 99 | } 100 | rejecter := rejecterF.New(logger, cfg) 101 | 102 | handler := hf(cfg, prxy) 103 | signatureConfig, err := krakendjose.GetSignatureConfig(cfg) 104 | if err == krakendjose.ErrNoValidatorCfg { 105 | logger.Info("JOSE: validator disabled for the endpoint", cfg.Endpoint) 106 | return handler 107 | } 108 | if err != nil { 109 | logger.Warning(fmt.Sprintf("JOSE: validator for %s: %s", cfg.Endpoint, err.Error())) 110 | return handler 111 | } 112 | 113 | validator, err := krakendjose.NewValidator(signatureConfig, FromCookie, FromHeader) 114 | if err != nil { 115 | log.Fatalf("%s: %s", cfg.Endpoint, err.Error()) 116 | } 117 | 118 | var aclCheck func(string, map[string]interface{}, []string) bool 119 | 120 | if signatureConfig.RolesKeyIsNested && strings.Contains(signatureConfig.RolesKey, ".") && signatureConfig.RolesKey[:4] != "http" { 121 | aclCheck = krakendjose.CanAccessNested 122 | } else { 123 | aclCheck = krakendjose.CanAccess 124 | } 125 | 126 | var scopesMatcher func(string, map[string]interface{}, []string) bool 127 | 128 | if len(signatureConfig.Scopes) > 0 && signatureConfig.ScopesKey != "" { 129 | if signatureConfig.ScopesMatcher == "all" { 130 | scopesMatcher = krakendjose.ScopesAllMatcher 131 | } else { 132 | scopesMatcher = krakendjose.ScopesAnyMatcher 133 | } 134 | } else { 135 | scopesMatcher = krakendjose.ScopesDefaultMatcher 136 | } 137 | 138 | logger.Info("JOSE: validator enabled for the endpoint", cfg.Endpoint) 139 | 140 | return func(w http.ResponseWriter, r *http.Request) { 141 | token, err := validator.ValidateRequest(r) 142 | if err != nil { 143 | http.Error(w, err.Error(), http.StatusUnauthorized) 144 | return 145 | } 146 | 147 | claims := map[string]interface{}{} 148 | err = validator.Claims(r, token, &claims) 149 | if err != nil { 150 | http.Error(w, err.Error(), http.StatusUnauthorized) 151 | return 152 | } 153 | 154 | if rejecter.Reject(claims) { 155 | http.Error(w, "", http.StatusUnauthorized) 156 | return 157 | } 158 | 159 | if !aclCheck(signatureConfig.RolesKey, claims, signatureConfig.Roles) { 160 | http.Error(w, "", http.StatusForbidden) 161 | return 162 | } 163 | 164 | if !scopesMatcher(signatureConfig.ScopesKey, claims, signatureConfig.Scopes) { 165 | http.Error(w, "", http.StatusForbidden) 166 | return 167 | } 168 | 169 | propagateHeaders(cfg, signatureConfig.PropagateClaimsToHeader, claims, r, logger) 170 | 171 | handler(w, r) 172 | } 173 | } 174 | } 175 | 176 | func FromCookie(key string) func(r *http.Request) (*jwt.JSONWebToken, error) { 177 | if key == "" { 178 | key = "access_token" 179 | } 180 | return func(r *http.Request) (*jwt.JSONWebToken, error) { 181 | cookie, err := r.Cookie(key) 182 | if err != nil { 183 | return nil, auth0.ErrTokenNotFound 184 | } 185 | return jwt.ParseSigned(cookie.Value) 186 | } 187 | } 188 | 189 | func FromHeader(header string) func(r *http.Request) (*jwt.JSONWebToken, error) { 190 | if header == "" { 191 | header = "Authorization" 192 | } 193 | return func(r *http.Request) (*jwt.JSONWebToken, error) { 194 | raw := r.Header.Get(header) 195 | if len(raw) > 7 && strings.EqualFold(raw[0:7], "BEARER ") { 196 | raw = raw[7:] 197 | } 198 | if raw == "" { 199 | return nil, auth0.ErrTokenNotFound 200 | } 201 | return jwt.ParseSigned(raw) 202 | } 203 | } 204 | 205 | func propagateHeaders(cfg *config.EndpointConfig, propagationCfg [][]string, claims map[string]interface{}, r *http.Request, logger logging.Logger) { 206 | if len(propagationCfg) > 0 { 207 | headersToPropagate, err := krakendjose.CalculateHeadersToPropagate(propagationCfg, claims) 208 | if err != nil { 209 | logger.Warning(fmt.Sprintf("JOSE: header propagations error for %s: %s", cfg.Endpoint, err.Error())) 210 | } 211 | for k, v := range headersToPropagate { 212 | // Set header value - replaces existing one 213 | r.Header.Set(k, v) 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /mux/jose_example_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "time" 12 | 13 | krakendjose "github.com/krakendio/krakend-jose/v2" 14 | "github.com/luraproject/lura/v2/config" 15 | "github.com/luraproject/lura/v2/logging" 16 | "github.com/luraproject/lura/v2/proxy" 17 | muxlura "github.com/luraproject/lura/v2/router/mux" 18 | ) 19 | 20 | func Example_RS256() { 21 | privateServer := httptest.NewServer(jwkEndpoint("private")) 22 | defer privateServer.Close() 23 | publicServer := httptest.NewServer(jwkEndpoint("public")) 24 | defer publicServer.Close() 25 | 26 | runValidationCycle( 27 | newSignerEndpointCfg("RS256", "2011-04-29", privateServer.URL), 28 | newVerifierEndpointCfg("RS256", publicServer.URL, []string{"role_a"}), 29 | ) 30 | 31 | // output: 32 | // token request 33 | // 201 34 | // {"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.u1fK05FpXctB-VkhhT3xu2WSIkEr1_VM71ald-yeKTesxhxg68TsHFEOBCgoXPuCviOP8QnUKNuVSeyMJh9z3nnrfQIjo9VZ2yicZu6ImYptSQ2DJbR80GDSPp-H7KnjaR9AAY0HZ0M-KUTaHdLABZFr307nkOeaJn_5jMpav7pqa7nrU3sI1CLX5pYVTggG6t7Zoqj2ebzzqdRxQEtdmZkD_NfH-3w3t-H0ylVdeBnPh-RvlspxC_mJzyUIJ0BwPlZpabppHm1ISySa4kwnwxEYnux0oZcb3PSoOZZZA467JySZ69PRlenNPdfGPL6E3uL1nqPHcxhte7ikSG4Q6Q","exp":2051882755,"refresh_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uMTI4NzZidmN4OThlcnR5dWlvcCIsInN1YiI6IjEyMzQ1Njc4OTBxd2VydHl1aW8ifQ.jwmNRj7gRcAgeeG9WqB2I8mqRVFZtw3uw5uSBJD8MmCVGRGPJ83ytEbqF3A-ya9IbdL5lJZ5LDUhwkO9xnkLZPBClDYBP81h0ZU7KR3vJnH9ZNkgpUiu1XLfkpJ6tSuZXPLj5-Lxymr3Mf8PdWey5YjEfk6mN_xfBHZR_XZbwsVbiv_nWhp-qeltPkXraShEpsDFFfzjRFrGprFi1S00OFDBcObbmXtZ8GTyJgSN8vO_rU-vkt6no1phKHzuyaS5D6GdjrxDXv7pHYL-OWifBiElMs09PAd16rZy3-qSIDZS7vHo724cG9UYMgxSE86PvjGP_dOJCOf64p_wPkkBRw"} 35 | // map[Content-Type:[application/json]] 36 | // unauthorized request 37 | // 401 38 | // authorized request 39 | // 200 40 | // {} 41 | // application/json 42 | // dummy request 43 | // 200 44 | // {} 45 | // application/json 46 | } 47 | 48 | func Example_HS256() { 49 | server := httptest.NewServer(jwkEndpoint("symmetric")) 50 | defer server.Close() 51 | 52 | runValidationCycle( 53 | newSignerEndpointCfg("HS256", "sim2", server.URL), 54 | newVerifierEndpointCfg("HS256", server.URL, []string{"role_a"}), 55 | ) 56 | 57 | // output: 58 | // token request 59 | // 201 60 | // {"access_token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InNpbTIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.xG6O62h475Y-EknyLFerPOUX6ATKCoIYEq4QsQsuw-Q","exp":2051882755,"refresh_token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InNpbTIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uMTI4NzZidmN4OThlcnR5dWlvcCIsInN1YiI6IjEyMzQ1Njc4OTBxd2VydHl1aW8ifQ.8rd0w9_H7Z_0J37nKvqQNwJnP25VrQcVAAa5sc3Fsw0"} 61 | // map[Content-Type:[application/json]] 62 | // unauthorized request 63 | // 401 64 | // authorized request 65 | // 200 66 | // {} 67 | // application/json 68 | // dummy request 69 | // 200 70 | // {} 71 | // application/json 72 | } 73 | 74 | func Example_HS256_cookie() { 75 | server := httptest.NewServer(jwkEndpoint("symmetric")) 76 | defer server.Close() 77 | 78 | sCfg := newSignerEndpointCfg("HS256", "sim2", server.URL) 79 | _, signer, _ := krakendjose.NewSigner(sCfg, nil) 80 | verifierCfg := newVerifierEndpointCfg("HS256", server.URL, []string{"role_a"}) 81 | 82 | externalTokenIssuer := func(rw http.ResponseWriter, _ *http.Request) { 83 | resp, _ := tokenIssuer(context.Background(), new(proxy.Request)) 84 | data, ok := resp.Data["access_token"] 85 | if !ok { 86 | rw.WriteHeader(http.StatusBadRequest) 87 | return 88 | } 89 | token, _ := signer(data) 90 | cookie := &http.Cookie{ 91 | Name: "access_token", 92 | Value: token, 93 | Expires: time.Now().Add(time.Hour), 94 | } 95 | http.SetCookie(rw, cookie) 96 | } 97 | 98 | loginRequest, _ := http.NewRequest("GET", "/", new(bytes.Buffer)) 99 | w := httptest.NewRecorder() 100 | externalTokenIssuer(w, loginRequest) 101 | 102 | buf := new(bytes.Buffer) 103 | logger, _ := logging.NewLogger("DEBUG", os.Stderr, "") 104 | hf := HandlerFactory(muxlura.EndpointHandler, dummyParamsExtractor, logger, nil) 105 | 106 | engine := http.ServeMux{} 107 | 108 | engine.Handle(verifierCfg.Endpoint, hf(verifierCfg, proxy.NoopProxy)) 109 | 110 | request, _ := http.NewRequest("GET", verifierCfg.Endpoint, new(bytes.Buffer)) 111 | if len(w.Result().Cookies()) == 0 { 112 | fmt.Println("unexpected number of cookies") 113 | return 114 | } 115 | request.AddCookie(w.Result().Cookies()[0]) 116 | 117 | w = httptest.NewRecorder() 118 | engine.ServeHTTP(w, request) 119 | 120 | fmt.Println(w.Result().StatusCode) 121 | fmt.Println(w.Body.String()) 122 | fmt.Println(w.Result().Header.Get("Content-Type")) 123 | 124 | fmt.Println(buf.String()) 125 | 126 | // output: 127 | // 200 128 | // {} 129 | // application/json 130 | } 131 | 132 | func runValidationCycle(signerEndpointCfg, validatorEndpointCfg *config.EndpointConfig) { 133 | buf := new(bytes.Buffer) 134 | logger, _ := logging.NewLogger("DEBUG", os.Stderr, "") 135 | hf := HandlerFactory(muxlura.EndpointHandler, dummyParamsExtractor, logger, nil) 136 | 137 | engine := http.ServeMux{} 138 | 139 | engine.Handle(validatorEndpointCfg.Endpoint, hf(validatorEndpointCfg, proxy.NoopProxy)) 140 | engine.Handle(signerEndpointCfg.Endpoint, hf(signerEndpointCfg, tokenIssuer)) 141 | engine.Handle("/", hf(&config.EndpointConfig{ 142 | Timeout: time.Second, 143 | Endpoint: "/private", 144 | Method: "GET", 145 | Backend: []*config.Backend{ 146 | { 147 | URLPattern: "/", 148 | Host: []string{"http://example.com/"}, 149 | Timeout: time.Second, 150 | }, 151 | }, 152 | }, proxy.NoopProxy)) 153 | 154 | fmt.Println("token request") 155 | req := httptest.NewRequest("POST", signerEndpointCfg.Endpoint, new(bytes.Buffer)) 156 | 157 | w := httptest.NewRecorder() 158 | engine.ServeHTTP(w, req) 159 | 160 | fmt.Println(w.Result().StatusCode) 161 | fmt.Println(w.Body.String()) 162 | fmt.Println(w.Result().Header) 163 | 164 | responseData := struct { 165 | AccessToken string `json:"access_token"` 166 | RefreshToken string `json:"refresh_token"` 167 | Expiration int `json:"exp"` 168 | }{} 169 | json.Unmarshal(w.Body.Bytes(), &responseData) 170 | 171 | fmt.Println("unauthorized request") 172 | req = httptest.NewRequest("GET", validatorEndpointCfg.Endpoint, new(bytes.Buffer)) 173 | w = httptest.NewRecorder() 174 | engine.ServeHTTP(w, req) 175 | 176 | fmt.Println(w.Result().StatusCode) 177 | 178 | fmt.Println("authorized request") 179 | req = httptest.NewRequest("GET", validatorEndpointCfg.Endpoint, new(bytes.Buffer)) 180 | req.Header.Set("Authorization", "BEARER "+responseData.AccessToken) 181 | w = httptest.NewRecorder() 182 | engine.ServeHTTP(w, req) 183 | 184 | fmt.Println(w.Result().StatusCode) 185 | fmt.Println(w.Body.String()) 186 | fmt.Println(w.Result().Header.Get("Content-Type")) 187 | 188 | fmt.Println("dummy request") 189 | req = httptest.NewRequest("GET", "/", new(bytes.Buffer)) 190 | w = httptest.NewRecorder() 191 | engine.ServeHTTP(w, req) 192 | 193 | fmt.Println(w.Result().StatusCode) 194 | fmt.Println(w.Body.String()) 195 | fmt.Println(w.Result().Header.Get("Content-Type")) 196 | 197 | fmt.Println(buf.String()) 198 | } 199 | 200 | func tokenIssuer(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 201 | return &proxy.Response{ 202 | Data: map[string]interface{}{ 203 | "access_token": map[string]interface{}{ 204 | "aud": "http://api.example.com", 205 | "iss": "http://example.com", 206 | "sub": "1234567890qwertyuio", 207 | "jti": "mnb23vcsrt756yuiomnbvcx98ertyuiop", 208 | "roles": []string{"role_a", "role_b"}, 209 | "exp": 2051882755, 210 | }, 211 | "refresh_token": map[string]interface{}{ 212 | "aud": "http://api.example.com", 213 | "iss": "http://example.com", 214 | "sub": "1234567890qwertyuio", 215 | "jti": "mnb23vcsrt756yuiomn12876bvcx98ertyuiop", 216 | "exp": 2051882755, 217 | }, 218 | "exp": 2051882755, 219 | }, 220 | Metadata: proxy.Metadata{ 221 | StatusCode: 201, 222 | }, 223 | IsComplete: true, 224 | }, nil 225 | } 226 | 227 | func newSignerEndpointCfg(alg, ID, URL string) *config.EndpointConfig { 228 | return &config.EndpointConfig{ 229 | Timeout: time.Second, 230 | Endpoint: "/token", 231 | Method: "POST", 232 | Backend: []*config.Backend{ 233 | { 234 | URLPattern: "/token", 235 | Host: []string{"http://example.com/"}, 236 | Timeout: time.Second, 237 | }, 238 | }, 239 | ExtraConfig: config.ExtraConfig{ 240 | krakendjose.SignerNamespace: map[string]interface{}{ 241 | "alg": alg, 242 | "kid": ID, 243 | "jwk_url": URL, 244 | "keys_to_sign": []string{"access_token", "refresh_token"}, 245 | "disable_jwk_security": true, 246 | "cache": true, 247 | }, 248 | }, 249 | } 250 | } 251 | 252 | func newVerifierEndpointCfg(alg, URL string, roles []string) *config.EndpointConfig { 253 | return &config.EndpointConfig{ 254 | Timeout: time.Second, 255 | Endpoint: "/private", 256 | Method: "GET", 257 | Backend: []*config.Backend{ 258 | { 259 | URLPattern: "/", 260 | Host: []string{"http://example.com/"}, 261 | Timeout: time.Second, 262 | }, 263 | }, 264 | ExtraConfig: config.ExtraConfig{ 265 | krakendjose.ValidatorNamespace: map[string]interface{}{ 266 | "alg": alg, 267 | "jwk_url": URL, 268 | "audience": []string{"http://api.example.com"}, 269 | "issuer": "http://example.com", 270 | "roles": roles, 271 | "propagate_claims": [][]string{{"jti", "x-krakend-jti"}, {"sub", "x-krakend-sub"}, {"nonexistent", "x-krakend-ne"}, {"sub", "x-krakend-replace"}}, 272 | "disable_jwk_security": true, 273 | "cache": true, 274 | }, 275 | }, 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /mux/jose_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | jose "github.com/krakendio/krakend-jose/v2" 13 | "github.com/luraproject/lura/v2/logging" 14 | "github.com/luraproject/lura/v2/proxy" 15 | muxlura "github.com/luraproject/lura/v2/router/mux" 16 | ) 17 | 18 | func TestTokenSignatureValidator(t *testing.T) { 19 | server := httptest.NewServer(jwkEndpoint("public")) 20 | defer server.Close() 21 | 22 | validatorEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{"role_a"}) 23 | 24 | forbidenEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{"role_c"}) 25 | forbidenEndpointCfg.Endpoint = "/forbiden" 26 | 27 | registeredEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{}) 28 | registeredEndpointCfg.Endpoint = "/registered" 29 | 30 | propagateHeadersEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{}) 31 | propagateHeadersEndpointCfg.Endpoint = "/propagateheaders" 32 | 33 | token := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.u1fK05FpXctB-VkhhT3xu2WSIkEr1_VM71ald-yeKTesxhxg68TsHFEOBCgoXPuCviOP8QnUKNuVSeyMJh9z3nnrfQIjo9VZ2yicZu6ImYptSQ2DJbR80GDSPp-H7KnjaR9AAY0HZ0M-KUTaHdLABZFr307nkOeaJn_5jMpav7pqa7nrU3sI1CLX5pYVTggG6t7Zoqj2ebzzqdRxQEtdmZkD_NfH-3w3t-H0ylVdeBnPh-RvlspxC_mJzyUIJ0BwPlZpabppHm1ISySa4kwnwxEYnux0oZcb3PSoOZZZA467JySZ69PRlenNPdfGPL6E3uL1nqPHcxhte7ikSG4Q6Q" 34 | 35 | dummyProxy := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 36 | return &proxy.Response{ 37 | Data: map[string]interface{}{ 38 | "aaaa": map[string]interface{}{ 39 | "foo": "a", 40 | "bar": "b", 41 | }, 42 | "bbbb": true, 43 | "cccc": 1234567890, 44 | }, 45 | IsComplete: true, 46 | Metadata: proxy.Metadata{ 47 | StatusCode: 200, 48 | }, 49 | }, nil 50 | } 51 | 52 | buf := new(bytes.Buffer) 53 | logger, _ := logging.NewLogger("DEBUG", buf, "") 54 | hf := HandlerFactory(muxlura.EndpointHandler, dummyParamsExtractor, logger, nil) 55 | 56 | engine := muxlura.DefaultEngine() 57 | 58 | engine.Handle(validatorEndpointCfg.Endpoint, "GET", hf(validatorEndpointCfg, dummyProxy)) 59 | engine.Handle(forbidenEndpointCfg.Endpoint, "GET", hf(forbidenEndpointCfg, dummyProxy)) 60 | engine.Handle(registeredEndpointCfg.Endpoint, "GET", hf(registeredEndpointCfg, dummyProxy)) 61 | engine.Handle(propagateHeadersEndpointCfg.Endpoint, "GET", hf(propagateHeadersEndpointCfg, dummyProxy)) 62 | 63 | req := httptest.NewRequest("GET", forbidenEndpointCfg.Endpoint, new(bytes.Buffer)) 64 | 65 | w := httptest.NewRecorder() 66 | engine.ServeHTTP(w, req) 67 | 68 | if w.Code != http.StatusUnauthorized { 69 | t.Errorf("unexpected status code: %d", w.Code) 70 | } 71 | if body := w.Body.String(); body != "Token not found\n" { 72 | t.Errorf("unexpected body: '%s'", body) 73 | } 74 | 75 | req = httptest.NewRequest("GET", validatorEndpointCfg.Endpoint, new(bytes.Buffer)) 76 | req.Header.Set("Authorization", "BEARER "+token) 77 | 78 | w = httptest.NewRecorder() 79 | engine.ServeHTTP(w, req) 80 | 81 | if w.Code != 200 { 82 | t.Errorf("unexpected status code: %d", w.Code) 83 | } 84 | if body := w.Body.String(); body != `{"aaaa":{"bar":"b","foo":"a"},"bbbb":true,"cccc":1234567890}` { 85 | t.Errorf("unexpected body: %s", body) 86 | } 87 | 88 | if log := buf.String(); !strings.Contains(log, "INFO: JOSE: signer disabled for the endpoint /private") { 89 | t.Error(log) 90 | } 91 | 92 | req = httptest.NewRequest("GET", forbidenEndpointCfg.Endpoint, new(bytes.Buffer)) 93 | req.Header.Set("Authorization", "BEARER "+token) 94 | 95 | w = httptest.NewRecorder() 96 | engine.ServeHTTP(w, req) 97 | 98 | if w.Code != http.StatusForbidden { 99 | t.Errorf("unexpected status code: %d", w.Code) 100 | } 101 | if body := w.Body.String(); body != "\n" { 102 | t.Errorf("unexpected body: %s", body) 103 | } 104 | 105 | req = httptest.NewRequest("GET", registeredEndpointCfg.Endpoint, new(bytes.Buffer)) 106 | req.Header.Set("Authorization", "BEARER "+token) 107 | 108 | w = httptest.NewRecorder() 109 | engine.ServeHTTP(w, req) 110 | 111 | if w.Code != http.StatusOK { 112 | t.Errorf("unexpected status code: %d", w.Code) 113 | } 114 | if body := w.Body.String(); body != `{"aaaa":{"bar":"b","foo":"a"},"bbbb":true,"cccc":1234567890}` { 115 | t.Errorf("unexpected body: %s", body) 116 | } 117 | 118 | req = httptest.NewRequest("GET", propagateHeadersEndpointCfg.Endpoint, new(bytes.Buffer)) 119 | req.Header.Set("Authorization", "BEARER "+token) 120 | // Check header-overwrite: it must be overwritten by a claim in the JWT! 121 | req.Header.Set("x-krakend-replace", "abc") 122 | 123 | w = httptest.NewRecorder() 124 | engine.ServeHTTP(w, req) 125 | 126 | if req.Header.Get("x-krakend-jti") == "" { 127 | t.Error("JWT claim not propagated to header: jti") 128 | } else if req.Header.Get("x-krakend-jti") != "mnb23vcsrt756yuiomnbvcx98ertyuiop" { 129 | t.Errorf("wrong JWT claim propagated for 'jti': %v", req.Header.Get("x-krakend-jti")) 130 | } 131 | 132 | // Check that existing header values are overwritten 133 | if req.Header.Get("x-krakend-replace") == "abc" { 134 | t.Error("JWT claim not propagated to x-krakend-replace header: sub") 135 | } else if req.Header.Get("x-krakend-replace") != "1234567890qwertyuio" { 136 | t.Errorf("wrong JWT claim propagated for 'sub': %v", req.Header.Get("x-krakend-replace")) 137 | } 138 | 139 | if req.Header.Get("x-krakend-sub") == "" { 140 | t.Error("JWT claim not propagated to header: sub") 141 | } else if req.Header.Get("x-krakend-sub") != "1234567890qwertyuio" { 142 | t.Errorf("wrong JWT claim propagated for 'sub': %v", req.Header.Get("x-krakend-sub")) 143 | } 144 | 145 | if req.Header.Get("x-krakend-ne") != "" { 146 | t.Error("JWT claim propagated, although it shouldn't: nonexistent") 147 | } 148 | 149 | if w.Code != http.StatusOK { 150 | t.Errorf("unexpected status code: %d", w.Code) 151 | } 152 | if body := w.Body.String(); body != `{"aaaa":{"bar":"b","foo":"a"},"bbbb":true,"cccc":1234567890}` { 153 | t.Errorf("unexpected body: %s", body) 154 | } 155 | } 156 | 157 | func TestCustomHeaderName(t *testing.T) { 158 | server := httptest.NewServer(jwkEndpoint("public")) 159 | defer server.Close() 160 | 161 | nonDefaultAuthHeaderEndpointCfg := newVerifierEndpointCfg("RS256", server.URL, []string{}) 162 | nonDefaultAuthHeaderEndpointCfg.Endpoint = "/custom-header" 163 | nonDefaultAuthHeaderEndpointCfg.ExtraConfig[jose.ValidatorNamespace].(map[string]interface{})["auth_header_name"] = "X-Custom-Auth" 164 | 165 | token := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMTEtMDQtMjkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoyMDUxODgyNzU1LCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJqdGkiOiJtbmIyM3Zjc3J0NzU2eXVpb21uYnZjeDk4ZXJ0eXVpb3AiLCJyb2xlcyI6WyJyb2xlX2EiLCJyb2xlX2IiXSwic3ViIjoiMTIzNDU2Nzg5MHF3ZXJ0eXVpbyJ9.u1fK05FpXctB-VkhhT3xu2WSIkEr1_VM71ald-yeKTesxhxg68TsHFEOBCgoXPuCviOP8QnUKNuVSeyMJh9z3nnrfQIjo9VZ2yicZu6ImYptSQ2DJbR80GDSPp-H7KnjaR9AAY0HZ0M-KUTaHdLABZFr307nkOeaJn_5jMpav7pqa7nrU3sI1CLX5pYVTggG6t7Zoqj2ebzzqdRxQEtdmZkD_NfH-3w3t-H0ylVdeBnPh-RvlspxC_mJzyUIJ0BwPlZpabppHm1ISySa4kwnwxEYnux0oZcb3PSoOZZZA467JySZ69PRlenNPdfGPL6E3uL1nqPHcxhte7ikSG4Q6Q" 166 | 167 | dummyProxy := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 168 | return &proxy.Response{ 169 | Data: map[string]interface{}{ 170 | "aaaa": map[string]interface{}{ 171 | "foo": "a", 172 | "bar": "b", 173 | }, 174 | "bbbb": true, 175 | "cccc": 1234567890, 176 | }, 177 | IsComplete: true, 178 | Metadata: proxy.Metadata{ 179 | StatusCode: 200, 180 | }, 181 | }, nil 182 | } 183 | 184 | buf := new(bytes.Buffer) 185 | logger, _ := logging.NewLogger("DEBUG", buf, "") 186 | hf := HandlerFactory(muxlura.EndpointHandler, dummyParamsExtractor, logger, nil) 187 | 188 | engine := muxlura.DefaultEngine() 189 | 190 | engine.Handle(nonDefaultAuthHeaderEndpointCfg.Endpoint, "GET", hf(nonDefaultAuthHeaderEndpointCfg, dummyProxy)) 191 | 192 | req := httptest.NewRequest("GET", nonDefaultAuthHeaderEndpointCfg.Endpoint, new(bytes.Buffer)) 193 | req.Header.Set("X-Custom-Auth", "BEARER "+token) 194 | 195 | w := httptest.NewRecorder() 196 | engine.ServeHTTP(w, req) 197 | 198 | if w.Code != http.StatusOK { 199 | t.Errorf("unexpected status code: %d", w.Code) 200 | } 201 | if body := w.Body.String(); body != "{\"aaaa\":{\"bar\":\"b\",\"foo\":\"a\"},\"bbbb\":true,\"cccc\":1234567890}" { 202 | t.Errorf("unexpected body: %s", body) 203 | } 204 | 205 | req = httptest.NewRequest("GET", nonDefaultAuthHeaderEndpointCfg.Endpoint, new(bytes.Buffer)) 206 | req.Header.Set("Authorization", "BEARER "+token) 207 | 208 | w = httptest.NewRecorder() 209 | engine.ServeHTTP(w, req) 210 | 211 | if w.Code != http.StatusUnauthorized { 212 | t.Errorf("unexpected status code: %d", w.Code) 213 | } 214 | 215 | req = httptest.NewRequest("GET", nonDefaultAuthHeaderEndpointCfg.Endpoint, new(bytes.Buffer)) 216 | 217 | w = httptest.NewRecorder() 218 | engine.ServeHTTP(w, req) 219 | 220 | if w.Code != http.StatusUnauthorized { 221 | t.Errorf("unexpected status code: %d", w.Code) 222 | } 223 | } 224 | 225 | func jwkEndpoint(name string) http.HandlerFunc { 226 | data, err := ioutil.ReadFile("../fixtures/" + name + ".json") 227 | return func(rw http.ResponseWriter, _ *http.Request) { 228 | if err != nil { 229 | rw.WriteHeader(500) 230 | return 231 | } 232 | rw.Header().Set("Content-Type", "application/json") 233 | rw.Write(data) 234 | } 235 | } 236 | 237 | func dummyParamsExtractor(_ *http.Request) map[string]string { 238 | return map[string]string{} 239 | } 240 | -------------------------------------------------------------------------------- /rejecter.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "github.com/luraproject/lura/v2/config" 5 | "github.com/luraproject/lura/v2/logging" 6 | ) 7 | 8 | // Rejecter defines the interface for the components responsible for rejecting tokens. 9 | type Rejecter interface { 10 | Reject(map[string]interface{}) bool 11 | } 12 | 13 | // RejecterFunc is an adapter to use functions as rejecters 14 | type RejecterFunc func(map[string]interface{}) bool 15 | 16 | // Reject calls r(v) 17 | func (r RejecterFunc) Reject(v map[string]interface{}) bool { return r(v) } 18 | 19 | // FixedRejecter is a rejecter that always returns the same bool response 20 | type FixedRejecter bool 21 | 22 | // Reject returns f 23 | func (f FixedRejecter) Reject(_ map[string]interface{}) bool { return bool(f) } 24 | 25 | // RejecterFactory is a builder for rejecters 26 | type RejecterFactory interface { 27 | New(logging.Logger, *config.EndpointConfig) Rejecter 28 | } 29 | 30 | // RejecterFactoryFunc is an adapter to use a function as rejecter factory 31 | type RejecterFactoryFunc func(logging.Logger, *config.EndpointConfig) Rejecter 32 | 33 | // New calls f(l, cfg) 34 | func (f RejecterFactoryFunc) New(l logging.Logger, cfg *config.EndpointConfig) Rejecter { 35 | return f(l, cfg) 36 | } 37 | 38 | // NopRejecterFactory is a factory returning rejecters accepting all the tokens 39 | type NopRejecterFactory struct{} 40 | 41 | // New returns a fixed rejecter that accepts all the tokens 42 | func (NopRejecterFactory) New(_ logging.Logger, _ *config.EndpointConfig) Rejecter { 43 | return FixedRejecter(false) 44 | } 45 | 46 | // ChainedRejecterFactory returns rejecters chaining every rejecter contained in tne collection 47 | type ChainedRejecterFactory []RejecterFactory 48 | 49 | // New returns a chainned rejected that evaluates all the rejecters until v is rejected or the chain 50 | // is finished 51 | func (c ChainedRejecterFactory) New(l logging.Logger, cfg *config.EndpointConfig) Rejecter { 52 | rejecters := []Rejecter{} 53 | for _, rf := range c { 54 | rejecters = append(rejecters, rf.New(l, cfg)) 55 | } 56 | return RejecterFunc(func(v map[string]interface{}) bool { 57 | for _, r := range rejecters { 58 | if r.Reject(v) { 59 | return true 60 | } 61 | } 62 | return false 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /rejecter_test.go: -------------------------------------------------------------------------------- 1 | package jose 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/luraproject/lura/v2/config" 7 | "github.com/luraproject/lura/v2/logging" 8 | ) 9 | 10 | func TestChainedRejecterFactory(t *testing.T) { 11 | rf := ChainedRejecterFactory([]RejecterFactory{ 12 | NopRejecterFactory{}, 13 | RejecterFactoryFunc(func(_ logging.Logger, _ *config.EndpointConfig) Rejecter { 14 | return RejecterFunc(func(in map[string]interface{}) bool { 15 | v, ok := in["key"].(int) 16 | return ok && v == 42 17 | }) 18 | }), 19 | }) 20 | 21 | rejecter := rf.New(nil, nil) 22 | 23 | for _, tc := range []struct { 24 | name string 25 | in map[string]interface{} 26 | expected bool 27 | }{ 28 | { 29 | name: "empty", 30 | in: map[string]interface{}{}, 31 | }, 32 | { 33 | name: "reject", 34 | in: map[string]interface{}{"key": 42}, 35 | expected: true, 36 | }, 37 | { 38 | name: "pass-1", 39 | in: map[string]interface{}{"key": "42"}, 40 | }, 41 | { 42 | name: "pass-2", 43 | in: map[string]interface{}{"key": 9876}, 44 | }, 45 | } { 46 | if v := rejecter.Reject(tc.in); tc.expected != v { 47 | t.Errorf("unexpected result for %s. have %v, want %v", tc.name, v, tc.expected) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /secrets/cypher.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "context" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/md5" // skipcq: GSC-G501 8 | "crypto/rand" 9 | "encoding/hex" 10 | "io" 11 | 12 | "gocloud.dev/secrets" 13 | _ "gocloud.dev/secrets/awskms" 14 | _ "gocloud.dev/secrets/azurekeyvault" 15 | _ "gocloud.dev/secrets/gcpkms" 16 | _ "gocloud.dev/secrets/hashivault" 17 | _ "gocloud.dev/secrets/localsecrets" 18 | ) 19 | 20 | // OpenCensusViews are predefined views for OpenCensus metrics. 21 | // The views include counts and latency distributions for API method calls. 22 | var OpenCensusViews = secrets.OpenCensusViews 23 | 24 | // New returns a Cypher wrapping a secrets.Keeper accesing the secret stored at the given 25 | // url. The url depends on the secrets driver required (awskms, azurekeyvault, gcpkms, 26 | // hashivault and localsecrets). 27 | // See the URLOpener documentation in gocloud.dev/secrets driver subpackages for 28 | // details on supported URL formats, and https://gocloud.dev/concepts/urls 29 | // for more information. 30 | func New(ctx context.Context, url string) (*Cypher, error) { 31 | k, err := secrets.OpenKeeper(ctx, url) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return &Cypher{keeper: k}, nil 36 | } 37 | 38 | // Cypher is a structure able to encrypt and decrypt messages with an encrypted key. 39 | // Before encrypting or decrypting the message, the encrypted key is decrypted with the 40 | // help of the wrapped secrets.Keeper 41 | type Cypher struct { 42 | keeper *secrets.Keeper 43 | } 44 | 45 | // Encrypt encrypts a plain text using a encrypted key, returning a cipher message. Before using the given key, 46 | // it decrypts the key with the secrets.Keeper 47 | func (c *Cypher) Encrypt(ctx context.Context, plainText, cipheredKey []byte) ([]byte, error) { 48 | plainKey, err := c.keeper.Decrypt(ctx, cipheredKey) 49 | if err != nil { 50 | return []byte{}, err 51 | } 52 | return Encrypt(plainText, plainKey) 53 | } 54 | 55 | // Decrypt decrypts an encrypted text using a encrypted key, returning a plain message. Before using the given 56 | // key, it decrypts the key with the secrets.Keeper 57 | func (c *Cypher) Decrypt(ctx context.Context, cipherText, cipheredKey []byte) ([]byte, error) { 58 | plainKey, err := c.keeper.Decrypt(ctx, cipheredKey) 59 | if err != nil { 60 | return []byte{}, err 61 | } 62 | return Decrypt(cipherText, plainKey) 63 | } 64 | 65 | // EncryptKey encrypts the given plain key with the secrets.Keeper 66 | func (c *Cypher) EncryptKey(ctx context.Context, plainKey []byte) ([]byte, error) { 67 | return c.keeper.Encrypt(ctx, plainKey) 68 | } 69 | 70 | // Close releases any resources used for the Cypher 71 | func (c *Cypher) Close() { 72 | c.keeper.Close() 73 | } 74 | 75 | func createHash(key []byte) string { 76 | hasher := md5.New() // skipcq: GO-S1023, GSC-G401 77 | hasher.Write(key) 78 | return hex.EncodeToString(hasher.Sum(nil)) 79 | } 80 | 81 | // Encrypt encrypts the received data with a passphrase using AES GCM 82 | func Encrypt(data, passphrase []byte) ([]byte, error) { 83 | block, _ := aes.NewCipher([]byte(createHash(passphrase))) 84 | gcm, err := cipher.NewGCM(block) 85 | if err != nil { 86 | return []byte{}, err 87 | } 88 | nonce := make([]byte, gcm.NonceSize()) 89 | if _, err = io.ReadFull(rand.Reader, nonce); err != nil { 90 | return []byte{}, err 91 | } 92 | ciphertext := gcm.Seal(nonce, nonce, data, nil) 93 | return ciphertext, nil 94 | } 95 | 96 | // Decrypt decrypts the received data with a passphrase using AES GCM 97 | func Decrypt(data, passphrase []byte) ([]byte, error) { 98 | block, err := aes.NewCipher([]byte(createHash(passphrase))) 99 | if err != nil { 100 | return []byte{}, err 101 | } 102 | gcm, err := cipher.NewGCM(block) 103 | if err != nil { 104 | return []byte{}, err 105 | } 106 | nonceSize := gcm.NonceSize() 107 | nonce, ciphertext := data[:nonceSize], data[nonceSize:] 108 | plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) 109 | if err != nil { 110 | return []byte{}, err 111 | } 112 | return plaintext, nil 113 | } 114 | -------------------------------------------------------------------------------- /secrets/cypher_example_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "fmt" 8 | ) 9 | 10 | func Example() { 11 | ctx, cancel := context.WithCancel(context.Background()) 12 | defer cancel() 13 | 14 | // use "smGbjm71Nxd1Ig5FS0wj9SlbzAIrnolCz9bQQ6uAhl4=" as secret 15 | c, err := New(ctx, "base64key://smGbjm71Nxd1Ig5FS0wj9SlbzAIrnolCz9bQQ6uAhl4=") 16 | if err != nil { 17 | fmt.Println(err) 18 | return 19 | } 20 | defer c.Close() 21 | 22 | plainKey := make([]byte, 32) 23 | rand.Read(plainKey) 24 | 25 | cypherKey, err := c.EncryptKey(ctx, plainKey) 26 | if err != nil { 27 | fmt.Println(err) 28 | return 29 | } 30 | 31 | plainText := "asdfghjklñqwertyuiozxcvbnm," 32 | 33 | cypherText, err := c.Encrypt(ctx, []byte(plainText), cypherKey) 34 | if err != nil { 35 | fmt.Println(err) 36 | return 37 | } 38 | 39 | result, err := c.Decrypt(ctx, cypherText, cypherKey) 40 | if err != nil { 41 | fmt.Println(err) 42 | return 43 | } 44 | 45 | if r := string(result); r != plainText { 46 | fmt.Printf("unexpected result: %s", r) 47 | } 48 | 49 | // output: 50 | } 51 | 52 | func ExampleEncrypt() { 53 | msg := "zxcvbnmasdfghjklqwertyuiop1234567890" 54 | passphrase := "some secret" 55 | 56 | cypherMsg, err := Encrypt([]byte(msg), []byte(passphrase)) 57 | if err != nil { 58 | fmt.Println(err) 59 | return 60 | } 61 | 62 | cypherMsg2, err2 := Encrypt([]byte(msg), []byte(passphrase)) 63 | if err2 != nil { 64 | fmt.Println(err2) 65 | return 66 | } 67 | 68 | if bytes.Equal(cypherMsg, cypherMsg2) { 69 | fmt.Println("two executions with the same input shall not generate the same output") 70 | } 71 | 72 | // output: 73 | } 74 | 75 | func ExampleDecrypt() { 76 | msg := "zxcvbnmasdfghjklqwertyuiop1234567890" 77 | passphrase := "some secret" 78 | 79 | cypherMsg, err := Encrypt([]byte(msg), []byte(passphrase)) 80 | if err != nil { 81 | fmt.Println(err) 82 | return 83 | } 84 | 85 | cypherMsg2, err2 := Encrypt([]byte(msg), []byte(passphrase)) 86 | if err2 != nil { 87 | fmt.Println(err2) 88 | return 89 | } 90 | 91 | if bytes.Equal(cypherMsg, cypherMsg2) { 92 | fmt.Println("two executions with the same input shall not generate the same output") 93 | return 94 | } 95 | 96 | res1, err3 := Decrypt(cypherMsg, []byte(passphrase)) 97 | if err != nil { 98 | fmt.Println(err3) 99 | return 100 | } 101 | 102 | res2, err4 := Decrypt(cypherMsg2, []byte(passphrase)) 103 | if err != nil { 104 | fmt.Println(err4) 105 | return 106 | } 107 | 108 | if !bytes.Equal(res1, res2) { 109 | fmt.Println("results are different:", string(res1), string(res2)) 110 | return 111 | } 112 | 113 | // output: 114 | } 115 | -------------------------------------------------------------------------------- /secrets/cypher_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "testing" 7 | ) 8 | 9 | func TestNew(t *testing.T) { 10 | ctx, cancel := context.WithCancel(context.Background()) 11 | defer cancel() 12 | 13 | c, err := New(ctx, "base64key://") 14 | if err != nil { 15 | t.Error(err) 16 | return 17 | } 18 | 19 | plainKey := make([]byte, 32) 20 | rand.Read(plainKey) 21 | 22 | cypherKey, err := c.EncryptKey(ctx, plainKey) 23 | if err != nil { 24 | t.Error(err) 25 | return 26 | } 27 | 28 | plainText := "asdfghjklñqwertyuiozxcvbnm," 29 | 30 | cypherText, err := c.Encrypt(ctx, []byte(plainText), cypherKey) 31 | if err != nil { 32 | t.Error(err) 33 | return 34 | } 35 | 36 | result, err := c.Decrypt(ctx, cypherText, cypherKey) 37 | if err != nil { 38 | t.Error(err) 39 | return 40 | } 41 | 42 | if r := string(result); r != plainText { 43 | t.Errorf("unexpected result: %s", r) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/integration_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/gin-gonic/gin" 13 | krakendjose "github.com/krakendio/krakend-jose/v2" 14 | jose "github.com/krakendio/krakend-jose/v2/gin" 15 | "github.com/luraproject/lura/v2/config" 16 | "github.com/luraproject/lura/v2/logging" 17 | "github.com/luraproject/lura/v2/proxy" 18 | ginlura "github.com/luraproject/lura/v2/router/gin" 19 | ) 20 | 21 | func TestJoseMw(t *testing.T) { 22 | hf := ginlura.HandlerFactory(func(_ *config.EndpointConfig, _ proxy.Proxy) gin.HandlerFunc { 23 | return func(c *gin.Context) { 24 | t.Error("this handler should not be executed") 25 | } 26 | }) 27 | 28 | buf := bytes.NewBuffer([]byte{}) 29 | logger, _ := logging.NewLogger("DEBUG", buf, "") 30 | 31 | hf = jose.HandlerFactory(hf, logger, new(krakendjose.NopRejecterFactory)) 32 | 33 | signerProxy := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 34 | return &proxy.Response{ 35 | IsComplete: true, 36 | Data: map[string]interface{}{ 37 | "access_token": map[string]interface{}{ 38 | "aud": "http://api.example.com", 39 | "iss": "https://krakend.io", 40 | "sub": "1234567890qwertyuio", 41 | "jti": "mnb23vcsrt756yuiomnbvcx98ertyuiop", 42 | "roles": []interface{}{"role_a", "role_b"}, 43 | "exp": 1735689600, 44 | }, 45 | "refresh_token": map[string]interface{}{ 46 | "aud": "http://api.example.com", 47 | "iss": "https://krakend.io", 48 | "sub": "1234567890qwertyuio", 49 | "jti": "mnb23vcsrt756yuiomn12876bvcx98ertyuiop", 50 | "exp": 1735689600, 51 | }, 52 | "exp": 1735689600, 53 | }, 54 | }, nil 55 | } 56 | 57 | signerCfg := &config.EndpointConfig{ 58 | Endpoint: "/token/asymmetric/file", 59 | Backend: []*config.Backend{ 60 | { 61 | URLPattern: "/token.json", 62 | }, 63 | }, 64 | ExtraConfig: map[string]interface{}{ 65 | "github.com/devopsfaith/krakend-jose/signer": map[string]interface{}{ 66 | "alg": "RS256", 67 | "kid": "2011-04-29", 68 | "keys-to-sign": []interface{}{"access_token", "refresh_token"}, 69 | "jwk_local_path": "../fixtures/private.json", 70 | "disable_jwk_security": true, 71 | }, 72 | }, 73 | } 74 | 75 | gin.SetMode(gin.TestMode) 76 | e := gin.New() 77 | e.GET(signerCfg.Endpoint, hf(signerCfg, signerProxy)) 78 | 79 | w := httptest.NewRecorder() 80 | req, _ := http.NewRequest("GET", signerCfg.Endpoint, nil) 81 | 82 | e.ServeHTTP(w, req) 83 | 84 | resp := w.Result() 85 | 86 | if resp.StatusCode != 200 { 87 | t.Errorf("unexpected status: %d", resp.StatusCode) 88 | } 89 | body, _ := ioutil.ReadAll(resp.Body) 90 | resp.Body.Close() 91 | 92 | fmt.Println(string(body)) 93 | fmt.Println(buf.String()) 94 | } 95 | --------------------------------------------------------------------------------