├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── build-and-test.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── jwt.publication.gradle.kts ├── examples └── ktor-server │ ├── build.gradle.kts │ └── src │ └── commonMain │ └── kotlin │ └── com │ └── appstractive │ └── Application.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jwt-ecdsa ├── README.md ├── api │ ├── android │ │ └── jwt-ecdsa-kt.api │ └── jvm │ │ └── jwt-ecdsa-kt.api ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── appstractive │ │ └── jwt │ │ └── signatures │ │ └── ECDSA.kt │ └── commonTest │ └── kotlin │ └── com │ └── appstractive │ └── jwt │ └── signatures │ └── JwtVerifierTests.kt ├── jwt-hmac ├── README.md ├── api │ ├── android │ │ └── jwt-hmac-kt.api │ └── jvm │ │ └── jwt-hmac-kt.api ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── appstractive │ │ └── jwt │ │ └── signatures │ │ └── HMAC.kt │ └── commonTest │ └── kotlin │ └── com │ └── appstractive │ └── jwt │ └── signatures │ └── JwtVerifierTests.kt ├── jwt-jwks ├── README.md ├── api │ ├── android │ │ └── jwt-jwks-kt.api │ └── jvm │ │ └── jwt-jwks-kt.api ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── appstractive │ │ └── jwt │ │ ├── Common.kt │ │ ├── JSONWebKey.kt │ │ ├── JSONWebKeyExt.kt │ │ └── Jwks.kt │ └── commonTest │ └── kotlin │ └── jwt │ └── JwksVerifierTests.kt ├── jwt-rsa ├── README.md ├── api │ ├── android │ │ └── jwt-rsa-kt.api │ └── jvm │ │ └── jwt-rsa-kt.api ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── appstractive │ │ └── jwt │ │ └── signatures │ │ ├── PKCS1.kt │ │ ├── PSS.kt │ │ └── RSA.kt │ └── commonTest │ └── kotlin │ └── com │ └── appstractive │ └── jwt │ └── signatures │ └── JwtVerifierTests.kt ├── jwt ├── api │ ├── android │ │ └── jwt-kt.api │ └── jvm │ │ └── jwt-kt.api ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── appstractive │ │ └── jwt │ │ ├── Claims.kt │ │ ├── Header.kt │ │ ├── JWT.kt │ │ ├── Parser.kt │ │ ├── Signing.kt │ │ ├── Verification.kt │ │ └── utils │ │ ├── Base64Utils.kt │ │ ├── ClaimsExt.kt │ │ └── Serialization.kt │ └── commonTest │ └── kotlin │ └── com │ └── appstractive │ └── jwt │ ├── Common.kt │ ├── JwtParserTests.kt │ └── JwtVerifierTests.kt ├── ktor-server-auth-jwt ├── README.md ├── api │ ├── android │ │ └── ktor-server-auth-jwt.api │ └── jvm │ │ └── ktor-server-auth-jwt.api ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── io │ └── ktor │ └── server │ └── auth │ └── jwt │ ├── JWTAuth.kt │ ├── JWTAuthSchemes.kt │ └── JWTUtils.kt └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | # This .editorconfig section approximates ktfmt's formatting rules. You can include it in an 2 | # existing .editorconfig file or use it standalone by copying it to /.editorconfig 3 | # and making sure your editor is set to read settings from .editorconfig files. 4 | # 5 | # It includes editor-specific config options for IntelliJ IDEA. 6 | # 7 | # If any option is wrong, PR are welcome 8 | 9 | [{*.kt,*.kts}] 10 | indent_style = space 11 | insert_final_newline = true 12 | max_line_length = 160 13 | indent_size = 2 14 | ij_continuation_indent_size = 4 15 | ij_java_names_count_to_use_import_on_demand = 9999 16 | ij_kotlin_align_in_columns_case_branch = false 17 | ij_kotlin_align_multiline_binary_operation = false 18 | ij_kotlin_align_multiline_extends_list = false 19 | ij_kotlin_align_multiline_method_parentheses = false 20 | ij_kotlin_align_multiline_parameters = true 21 | ij_kotlin_align_multiline_parameters_in_calls = false 22 | ij_kotlin_allow_trailing_comma = true 23 | ij_kotlin_allow_trailing_comma_on_call_site = true 24 | ij_kotlin_assignment_wrap = normal 25 | ij_kotlin_blank_lines_after_class_header = 0 26 | ij_kotlin_blank_lines_around_block_when_branches = 0 27 | ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 28 | ij_kotlin_block_comment_at_first_column = true 29 | ij_kotlin_call_parameters_new_line_after_left_paren = true 30 | ij_kotlin_call_parameters_right_paren_on_new_line = false 31 | ij_kotlin_call_parameters_wrap = on_every_item 32 | ij_kotlin_catch_on_new_line = false 33 | ij_kotlin_class_annotation_wrap = split_into_lines 34 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL 35 | ij_kotlin_continuation_indent_for_chained_calls = true 36 | ij_kotlin_continuation_indent_for_expression_bodies = true 37 | ij_kotlin_continuation_indent_in_argument_lists = true 38 | ij_kotlin_continuation_indent_in_elvis = false 39 | ij_kotlin_continuation_indent_in_if_conditions = false 40 | ij_kotlin_continuation_indent_in_parameter_lists = false 41 | ij_kotlin_continuation_indent_in_supertype_lists = false 42 | ij_kotlin_else_on_new_line = false 43 | ij_kotlin_enum_constants_wrap = off 44 | ij_kotlin_extends_list_wrap = normal 45 | ij_kotlin_field_annotation_wrap = off 46 | ij_kotlin_finally_on_new_line = false 47 | ij_kotlin_if_rparen_on_new_line = false 48 | ij_kotlin_import_nested_classes = false 49 | ij_kotlin_imports_layout = * 50 | ij_kotlin_insert_whitespaces_in_simple_one_line_method = true 51 | ij_kotlin_keep_blank_lines_before_right_brace = 2 52 | ij_kotlin_keep_blank_lines_in_code = 2 53 | ij_kotlin_keep_blank_lines_in_declarations = 2 54 | ij_kotlin_keep_first_column_comment = true 55 | ij_kotlin_keep_indents_on_empty_lines = false 56 | ij_kotlin_keep_line_breaks = true 57 | ij_kotlin_lbrace_on_next_line = false 58 | ij_kotlin_line_comment_add_space = false 59 | ij_kotlin_line_comment_at_first_column = true 60 | ij_kotlin_method_annotation_wrap = split_into_lines 61 | ij_kotlin_method_call_chain_wrap = normal 62 | ij_kotlin_method_parameters_new_line_after_left_paren = true 63 | ij_kotlin_method_parameters_right_paren_on_new_line = true 64 | ij_kotlin_method_parameters_wrap = on_every_item 65 | ij_kotlin_name_count_to_use_star_import = 9999 66 | ij_kotlin_name_count_to_use_star_import_for_members = 9999 67 | ij_kotlin_parameter_annotation_wrap = off 68 | ij_kotlin_space_after_comma = true 69 | ij_kotlin_space_after_extend_colon = true 70 | ij_kotlin_space_after_type_colon = true 71 | ij_kotlin_space_before_catch_parentheses = true 72 | ij_kotlin_space_before_comma = false 73 | ij_kotlin_space_before_extend_colon = true 74 | ij_kotlin_space_before_for_parentheses = true 75 | ij_kotlin_space_before_if_parentheses = true 76 | ij_kotlin_space_before_lambda_arrow = true 77 | ij_kotlin_space_before_type_colon = false 78 | ij_kotlin_space_before_when_parentheses = true 79 | ij_kotlin_space_before_while_parentheses = true 80 | ij_kotlin_spaces_around_additive_operators = true 81 | ij_kotlin_spaces_around_assignment_operators = true 82 | ij_kotlin_spaces_around_equality_operators = true 83 | ij_kotlin_spaces_around_function_type_arrow = true 84 | ij_kotlin_spaces_around_logical_operators = true 85 | ij_kotlin_spaces_around_multiplicative_operators = true 86 | ij_kotlin_spaces_around_range = false 87 | ij_kotlin_spaces_around_relational_operators = true 88 | ij_kotlin_spaces_around_unary_operator = false 89 | ij_kotlin_spaces_around_when_arrow = true 90 | ij_kotlin_variable_annotation_wrap = off 91 | ij_kotlin_while_on_new_line = false 92 | ij_kotlin_wrap_elvis_expressions = 1 93 | ij_kotlin_wrap_expression_body_functions = 1 94 | ij_kotlin_wrap_first_method_in_call_chain = false 95 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "master" ] 7 | pull_request: 8 | branches: [ "master" ] 9 | 10 | jobs: 11 | jwt: 12 | 13 | runs-on: [ self-hosted, macOS ] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: set up JDK 17 19 | uses: actions/setup-java@v4 20 | with: 21 | java-version: '17' 22 | distribution: 'temurin' 23 | #cache: gradle 24 | 25 | - name: Grant execute permission for gradlew 26 | run: chmod +x gradlew 27 | 28 | - name: Setup Android SDK 29 | uses: android-actions/setup-android@v3 30 | with: 31 | packages: 'platform-tools' 32 | 33 | - name: Build 34 | run: ./gradlew :jwt-kt:build 35 | 36 | jwt-hmac: 37 | 38 | runs-on: [ self-hosted, macOS ] 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - name: set up JDK 17 44 | uses: actions/setup-java@v4 45 | with: 46 | java-version: '17' 47 | distribution: 'temurin' 48 | #cache: gradle 49 | 50 | - name: Grant execute permission for gradlew 51 | run: chmod +x gradlew 52 | 53 | - name: Setup Android SDK 54 | uses: android-actions/setup-android@v3 55 | with: 56 | packages: 'platform-tools' 57 | 58 | - name: Build 59 | run: ./gradlew :jwt-hmac-kt:build 60 | 61 | jwt-rsa: 62 | 63 | runs-on: [ self-hosted, macOS ] 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | 68 | - name: set up JDK 17 69 | uses: actions/setup-java@v4 70 | with: 71 | java-version: '17' 72 | distribution: 'temurin' 73 | #cache: gradle 74 | 75 | - name: Grant execute permission for gradlew 76 | run: chmod +x gradlew 77 | 78 | - name: Setup Android SDK 79 | uses: android-actions/setup-android@v3 80 | with: 81 | packages: 'platform-tools' 82 | 83 | - name: Build 84 | run: ./gradlew :jwt-rsa-kt:build 85 | 86 | jwt-ecdsa: 87 | 88 | runs-on: [ self-hosted, macOS ] 89 | 90 | steps: 91 | - uses: actions/checkout@v4 92 | 93 | - name: set up JDK 17 94 | uses: actions/setup-java@v4 95 | with: 96 | java-version: '17' 97 | distribution: 'temurin' 98 | #cache: gradle 99 | 100 | - name: Grant execute permission for gradlew 101 | run: chmod +x gradlew 102 | 103 | - name: Setup Android SDK 104 | uses: android-actions/setup-android@v3 105 | with: 106 | packages: 'platform-tools' 107 | 108 | - name: Build 109 | run: ./gradlew :jwt-ecdsa-kt:build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .kotlin 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### IntelliJ IDEA ### 9 | .idea 10 | *.iws 11 | *.iml 12 | *.ipr 13 | out/ 14 | !**/src/main/**/out/ 15 | !**/src/test/**/out/ 16 | 17 | ### Eclipse ### 18 | .apt_generated 19 | .classpath 20 | .factorypath 21 | .project 22 | .settings 23 | .springBeans 24 | .sts4-cache 25 | bin/ 26 | !**/src/main/**/bin/ 27 | !**/src/test/**/bin/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Mac OS ### 40 | .DS_Store 41 | 42 | key.txt 43 | *.podspec 44 | local.properties -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JWT Kotlin Multiplatform 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.appstractive/jwt-kt?label=Maven%20Central)](https://central.sonatype.com/artifact/com.appstractive/jwt-kt) 4 | 5 | ![badge][badge-android] 6 | ![badge][badge-apple] 7 | ![badge][badge-jvm] 8 | ![badge][badge-js] 9 | ![badge][badge-win] 10 | ![badge][badge-linux] 11 | 12 | A kotlin multiplatform library for creating, parsing, signing and verifying JWTs. 13 | 14 | ## Supported Algorithms 15 | 16 | - HS256 17 | - HS384 18 | - HS512 19 | - RS256 20 | - RS384 21 | - RS512 22 | - PS256 23 | - PS384 24 | - PS512 25 | - ES256 26 | - ES384 27 | - ES512 28 | 29 | ## Usage 30 | 31 | ### Installation 32 | 33 | Gradle: 34 | 35 | ``` 36 | commonMain.dependencies { 37 | implementation("com.appstractive:jwt-kt:1.1.0") 38 | } 39 | ``` 40 | 41 | ### Create JWT 42 | 43 | ```kotlin 44 | val jwt: UnsignedJWT = jwt { 45 | claims { 46 | issuer = "example.com" 47 | subject = "me" 48 | audience = "api.example.com" 49 | expires(Clock.System.now() + 24.hours) 50 | notBefore() 51 | issuedAt() 52 | id = "123456" 53 | 54 | claim("double", 1.1) 55 | claim("long", 1L) 56 | claim("bool", true) 57 | claim("string", "test") 58 | claim("null", null) 59 | objectClaim("object") { put("key", JsonPrimitive("value")) } 60 | arrayClaim("list") { 61 | add(JsonPrimitive(0)) 62 | add(JsonPrimitive(1)) 63 | add(JsonPrimitive(2)) 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ### Parse JWT 70 | 71 | ```kotlin 72 | val jwt = JWT.from("eyJraWQiOiJzLWRmZmZkMDJlLTlhNDItNDQzMC1hNT...") 73 | 74 | val userId = jwt.subject 75 | ``` 76 | 77 | ### Sign JWT 78 | 79 | For specific algorithms see: 80 | 81 | - [HMAC](jwt-hmac/README.md) 82 | - [RSA](jwt-rsa/README.md) 83 | - [ECDSA](jwt-ecdsa/README.md) 84 | 85 | ### Verify JWT 86 | 87 | ```kotlin 88 | val jwt = JWT.from("eyJraWQiOiJzLWRmZmZkMDJlLTlhNDItNDQzMC1hNT...") 89 | 90 | val isValid = jwt.verify { 91 | // verify issuer 92 | issuer("example.com") 93 | // verify audience 94 | audience("api.example.com") 95 | // verify expiration 96 | expiresAt() 97 | // verify not before time 98 | notBefore() 99 | } 100 | ``` 101 | 102 | For signature verification see: 103 | 104 | - [HMAC](jwt-hmac/README.md) 105 | - [RSA](jwt-rsa/README.md) 106 | - [ECDSA](jwt-ecdsa/README.md) 107 | 108 | ## License 109 | 110 | ``` 111 | Copyright 2024 Andreas Schulz. 112 | 113 | Licensed under the Apache License, Version 2.0 (the "License"); 114 | you may not use this file except in compliance with the License. 115 | You may obtain a copy of the License at 116 | 117 | http://www.apache.org/licenses/LICENSE-2.0 118 | 119 | Unless required by applicable law or agreed to in writing, software 120 | distributed under the License is distributed on an "AS IS" BASIS, 121 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 122 | See the License for the specific language governing permissions and 123 | limitations under the License. 124 | ``` 125 | 126 | [badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat 127 | 128 | [badge-apple]: http://img.shields.io/badge/platform-apple-111111.svg?style=flat 129 | 130 | [badge-jvm]: http://img.shields.io/badge/platform-jvm-CDCDCD.svg?style=flat 131 | 132 | [badge-js]: http://img.shields.io/badge/platform-js-f7df1e.svg?style=flat 133 | 134 | [badge-win]: http://img.shields.io/badge/platform-win-357EC7.svg?style=flat 135 | 136 | [badge-linux]: http://img.shields.io/badge/platform-linux-CDCDCD.svg?style=flat -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) apply false 5 | alias(libs.plugins.android.library) apply false 6 | alias(libs.plugins.kotlinx.serialization) apply false 7 | alias(libs.plugins.kotlinx.binary.compatibility) apply false 8 | alias(libs.plugins.nexus) 9 | } 10 | 11 | group = "com.appstractive" 12 | 13 | version = "1.1.0" 14 | 15 | val localProps = Properties() 16 | val localPropertiesFile = rootProject.file("local.properties") 17 | 18 | if (localPropertiesFile.exists()) { 19 | localPropertiesFile.inputStream().use { localProps.load(it) } 20 | } 21 | 22 | val signingKey by 23 | extra( 24 | localProps["signing.keyFile"]?.let { rootDir.resolve(it.toString()).readText() } 25 | ?: System.getenv("SIGNING_KEY") 26 | ?: "", 27 | ) 28 | val signingPassword by 29 | extra( 30 | localProps.getOrDefault( 31 | "signing.password", 32 | System.getenv("SIGNING_PASSWORD") ?: "", 33 | ), 34 | ) 35 | val ossrhUsername by 36 | extra( 37 | localProps.getOrDefault( 38 | "ossrhUsername", 39 | System.getenv("OSSRH_USERNAME") ?: "", 40 | ), 41 | ) 42 | val ossrhPassword by 43 | extra( 44 | localProps.getOrDefault( 45 | "ossrhPassword", 46 | System.getenv("OSSRH_PASSWORD") ?: "", 47 | ), 48 | ) 49 | val sonatypeStagingProfileId by 50 | extra( 51 | localProps.getOrDefault( 52 | "sonatypeStagingProfileId", 53 | System.getenv("SONATYPE_STAGING_PROFILE_ID") ?: "", 54 | ), 55 | ) 56 | 57 | nexusPublishing { 58 | this.repositories { 59 | sonatype { 60 | val sonatypeStagingProfileId: String by rootProject.extra 61 | val ossrhUsername: String by rootProject.extra 62 | val ossrhPassword: String by rootProject.extra 63 | 64 | stagingProfileId.set(sonatypeStagingProfileId) 65 | username.set(ossrhUsername) 66 | password.set(ossrhPassword) 67 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 68 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { `kotlin-dsl` } 2 | 3 | dependencies { implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.9.20") } 4 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } 3 | 4 | repositories { 5 | gradlePluginPortal() 6 | mavenCentral() 7 | google() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/jwt.publication.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | signing 3 | `maven-publish` 4 | id("org.jetbrains.dokka") 5 | } 6 | 7 | val javadocJar by 8 | tasks.registering(Jar::class) { 9 | archiveClassifier.set("javadoc") 10 | from(tasks.named("dokkaHtml")) 11 | } 12 | 13 | val publishing = extensions.getByName("publishing") as PublishingExtension 14 | 15 | extensions.configure("publishing") { 16 | publications.withType { 17 | artifact(javadocJar) 18 | 19 | pom { 20 | name.set(project.name) 21 | description.set( 22 | "JWT creating, parsing, signing and verifying implementation for Kotlin Multiplatform") 23 | url.set("https://github.com/Appstractive/jwt-kt") 24 | 25 | scm { 26 | url.set("https://github.com/Appstractive/jwt-kt") 27 | connection.set("scm:git:https://github.com/Appstractive/jwt-kt.git") 28 | developerConnection.set("scm:git:https://github.com/Appstractive/jwt-kt.git") 29 | tag.set("HEAD") 30 | } 31 | 32 | issueManagement { 33 | system.set("GitHub Issues") 34 | url.set("https://github.com/Appstractive/jwt-kt/issues") 35 | } 36 | 37 | developers { 38 | developer { 39 | name.set("Andreas Schulz") 40 | email.set("dev@appstractive.com") 41 | } 42 | } 43 | 44 | licenses { 45 | license { 46 | name.set("The Apache Software License, Version 2.0") 47 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 48 | distribution.set("repo") 49 | comments.set("A business-friendly OSS license") 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | val signingTasks = tasks.withType() 57 | 58 | tasks.withType().configureEach { dependsOn(signingTasks) } 59 | 60 | extensions.configure("signing") { 61 | val signingKey: String by rootProject.extra 62 | val signingPassword: String by rootProject.extra 63 | 64 | useInMemoryPgpKeys(signingKey, signingPassword) 65 | sign(publishing.publications) 66 | } 67 | -------------------------------------------------------------------------------- /examples/ktor-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | application 5 | alias(libs.plugins.multiplatform) 6 | alias(libs.plugins.kotlinx.serialization) 7 | } 8 | 9 | group = "com.appstractive.jwt" 10 | 11 | version = "1.1.0" 12 | 13 | kotlin { 14 | jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } 15 | 16 | linuxX64().apply { 17 | binaries { 18 | executable { 19 | entryPoint = "com.appstractive.main" 20 | } 21 | } 22 | } 23 | 24 | listOf( 25 | macosX64(), 26 | macosArm64(), 27 | ) 28 | .forEach { 29 | it.binaries { 30 | framework { 31 | baseName = "JWT-KT" 32 | isStatic = true 33 | } 34 | 35 | executable { 36 | entryPoint = "com.appstractive.main" 37 | } 38 | } 39 | } 40 | 41 | sourceSets { 42 | commonMain.dependencies { 43 | implementation(projects.jwtHmacKt) 44 | implementation(projects.ktorServerAuthJwt) 45 | 46 | implementation(libs.kotlin.datetime) 47 | 48 | implementation(libs.ktor.serialization.json) 49 | implementation(libs.ktor.server.core) 50 | implementation(libs.ktor.server.cio) 51 | implementation(libs.ktor.server.contentnegotiation) 52 | implementation(libs.ktor.server.auth) 53 | } 54 | 55 | commonTest.dependencies {} 56 | 57 | jvmMain.dependencies {} 58 | 59 | appleMain.dependencies {} 60 | 61 | linuxMain.dependencies {} 62 | 63 | mingwMain.dependencies {} 64 | } 65 | } 66 | 67 | application { mainClass.set("com.appstractive.ApplicationKt") } 68 | -------------------------------------------------------------------------------- /examples/ktor-server/src/commonMain/kotlin/com/appstractive/Application.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive 2 | 3 | import com.appstractive.jwt.expiresAt 4 | import com.appstractive.jwt.jwt 5 | import com.appstractive.jwt.sign 6 | import com.appstractive.jwt.signatures.hs256 7 | import dev.whyoleg.cryptography.random.CryptographyRandom 8 | import io.ktor.http.* 9 | import io.ktor.serialization.kotlinx.json.* 10 | import io.ktor.server.application.* 11 | import io.ktor.server.auth.* 12 | import io.ktor.server.auth.jwt.* 13 | import io.ktor.server.cio.* 14 | import io.ktor.server.engine.* 15 | import io.ktor.server.plugins.contentnegotiation.* 16 | import io.ktor.server.request.* 17 | import io.ktor.server.response.* 18 | import io.ktor.server.routing.* 19 | import kotlin.time.Duration.Companion.minutes 20 | import kotlinx.datetime.Clock 21 | import kotlinx.serialization.Serializable 22 | import kotlinx.serialization.json.jsonPrimitive 23 | 24 | @Serializable data class UserDTO(val username: String, val password: String) 25 | 26 | fun main() { 27 | embeddedServer(CIO, port = 8080) { 28 | install(ContentNegotiation) { json() } 29 | val secret = CryptographyRandom.nextBytes(64) 30 | val issuer = "http://0.0.0.0:8080/" 31 | val audience = "http://0.0.0.0:8080/hello" 32 | val myRealm = "Access to 'hello'" 33 | install(Authentication) { 34 | jwt("auth-jwt") { 35 | realm = myRealm 36 | verifier( 37 | issuer = issuer, 38 | audience = audience, 39 | ) { 40 | hs256 { this.secret = secret } 41 | } 42 | 43 | validate { credential -> 44 | if (credential.claims["username"]?.jsonPrimitive?.content != "") { 45 | JWTPrincipal(credential.claims) 46 | } else { 47 | null 48 | } 49 | } 50 | 51 | challenge { defaultScheme, realm -> 52 | call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired") 53 | } 54 | } 55 | } 56 | 57 | routing { 58 | post("/login") { 59 | val user = call.receive() 60 | // Check username and password 61 | // ... 62 | val token = 63 | jwt { 64 | claims { 65 | this.audience = audience 66 | this.issuer = issuer 67 | claim("username", user.username) 68 | expires() 69 | } 70 | } 71 | .sign { hs256 { this.secret = secret } } 72 | 73 | call.respond(hashMapOf("token" to token.toString())) 74 | } 75 | 76 | authenticate("auth-jwt") { 77 | get("/hello") { 78 | val principal = 79 | call.principal() ?: return@get call.respond(HttpStatusCode.Unauthorized) 80 | val username = principal.claims["username"]?.jsonPrimitive?.content 81 | val expiresIn = principal.claims.expiresAt?.minus(Clock.System.now()) 82 | call.respondText("Hello, $username! Token is expired in ${expiresIn?.inWholeSeconds} s.") 83 | } 84 | } 85 | } 86 | }.start(wait = true) 87 | } 88 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx4096m -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.10" # https://kotlinlang.org/docs/releases.html#release-details 3 | agp = "8.7.3" # https://developer.android.com/studio/releases/gradle-plugin 4 | 5 | compileSdk = "35" 6 | minSdk = "26" 7 | 8 | ktor = "3.0.3" 9 | 10 | kotlin-datetime = "0.6.1" 11 | kotlin-serialization = "1.8.0" 12 | kotlin-coroutines = "1.10.1" 13 | 14 | cryptography = "0.4.0" 15 | 16 | [libraries] 17 | 18 | kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlin-serialization" } 19 | kotlin-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlin-serialization" } 20 | kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlin-datetime" } 21 | kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } 22 | 23 | ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } 24 | 25 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 26 | ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } 27 | ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } 28 | ktor-client-ios = { module = "io.ktor:ktor-client-ios", version.ref = "ktor" } 29 | ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" } 30 | ktor-client-win = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" } 31 | ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } 32 | ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor" } 33 | ktor-client-json = { module = "io.ktor:ktor-client-json", version.ref = "ktor" } 34 | ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } 35 | 36 | ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } 37 | ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" } 38 | ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" } 39 | ktor-server-contentnegotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } 40 | 41 | crypto = { module = "dev.whyoleg.cryptography:cryptography-core", version.ref = "cryptography" } 42 | crypto-jdk = { module = "dev.whyoleg.cryptography:cryptography-provider-jdk", version.ref = "cryptography" } 43 | crypto-apple = { module = "dev.whyoleg.cryptography:cryptography-provider-apple", version.ref = "cryptography" } 44 | crypto-webcrypto = { module = "dev.whyoleg.cryptography:cryptography-provider-webcrypto", version.ref = "cryptography" } 45 | crypto-openssl3 = { module = "dev.whyoleg.cryptography:cryptography-provider-openssl3-prebuilt", version.ref = "cryptography" } 46 | crypto-pem = { module = "dev.whyoleg.cryptography:cryptography-serialization-pem", version.ref = "cryptography" } 47 | crypto-asn = { module = "dev.whyoleg.cryptography:cryptography-serialization-asn1", version.ref = "cryptography" } 48 | 49 | test-kotlin = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 50 | test-kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version = "1.10.1" } 51 | test-ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } 52 | 53 | junit = "junit:junit:4.13.2" 54 | 55 | [plugins] 56 | 57 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 58 | android-library = { id = "com.android.library", version.ref = "agp" } 59 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 60 | nexus = { id = "io.github.gradle-nexus.publish-plugin", version = "1.3.0" } 61 | dokka = { id = "org.jetbrains.dokka", version = "1.9.20" } 62 | kotlinx-binary-compatibility = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.16.3" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appstractive/jwt-kt/b63e3366d67a40c463ec25cb7cb46b7de1d2597e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Aug 12 19:14:14 CEST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jwt-ecdsa/README.md: -------------------------------------------------------------------------------- 1 | # JWT Kotlin Multiplatform - ECDSA 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.appstractive/jwt-ecdsa-kt?label=Maven%20Central)](https://central.sonatype.com/artifact/com.appstractive/jwt-ecdsa-kt) 4 | 5 | ![badge][badge-android] 6 | ![badge][badge-apple] 7 | ![badge][badge-jvm] 8 | ![badge][badge-js] 9 | ![badge][badge-win] 10 | ![badge][badge-linux] 11 | 12 | Sign and verify JWTs using ECDSA algorithm. 13 | 14 | ## Supported Algorithms 15 | 16 | - ES256 17 | - ES384 18 | - ES512 19 | 20 | ## Usage 21 | 22 | ### Installation 23 | 24 | Gradle: 25 | 26 | ``` 27 | commonMain.dependencies { 28 | implementation("com.appstractive:jwt-kt:1.1.0") 29 | implementation("com.appstractive:jwt-ecdsa-kt:1.1.0") 30 | } 31 | ``` 32 | 33 | ### Sign JWT 34 | 35 | ```kotlin 36 | val jwt: UnsignedJWT = jwt { 37 | claims { issuer = "example.com" } 38 | } 39 | val keys = CryptographyProvider.Default.get(ECDSA).keyPairGenerator(curve).generateKey() 40 | val privateKey = keys.privateKey.encodeTo(EC.PrivateKey.Format.PEM) 41 | 42 | val signedJWT = jwt.sign { 43 | es256 { secret = pem(privateKey) } 44 | // or with different hashing 45 | // es384 { secret = pem(privateKey) } 46 | // es512 { secret = pem(privateKey) } 47 | } 48 | ``` 49 | 50 | ### Verify JWT 51 | 52 | ```kotlin 53 | val jwt = JWT.from("eyJraWQiOiJzLWRmZmZkMDJlLTlhNDItNDQzMC1hNT...") 54 | val keys = CryptographyProvider.Default.get(ECDSA).keyPairGenerator(curve).generateKey() 55 | val publicKey = keys.publicKey.encodeTo(EC.PublicKey.Format.PEM) 56 | 57 | val isValid = jwt.verify { 58 | es256 { secret = pem(publicKey) } 59 | // or with different hashing 60 | // es384 { secret = pem(publicKey) } 61 | // es512 { secret = pem(publicKey) } 62 | 63 | // verify issuer 64 | issuer("example.com") 65 | // verify audience 66 | audience("api.example.com") 67 | // verify expiration 68 | expiresAt() 69 | // verify not before time 70 | notBefore() 71 | } 72 | ``` 73 | 74 | ## License 75 | 76 | ``` 77 | Copyright 2024 Andreas Schulz. 78 | 79 | Licensed under the Apache License, Version 2.0 (the "License"); 80 | you may not use this file except in compliance with the License. 81 | You may obtain a copy of the License at 82 | 83 | http://www.apache.org/licenses/LICENSE-2.0 84 | 85 | Unless required by applicable law or agreed to in writing, software 86 | distributed under the License is distributed on an "AS IS" BASIS, 87 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 88 | See the License for the specific language governing permissions and 89 | limitations under the License. 90 | ``` 91 | 92 | [badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat 93 | 94 | [badge-apple]: http://img.shields.io/badge/platform-apple-111111.svg?style=flat 95 | 96 | [badge-jvm]: http://img.shields.io/badge/platform-jvm-CDCDCD.svg?style=flat 97 | 98 | [badge-js]: http://img.shields.io/badge/platform-js-f7df1e.svg?style=flat 99 | 100 | [badge-win]: http://img.shields.io/badge/platform-win-357EC7.svg?style=flat 101 | 102 | [badge-linux]: http://img.shields.io/badge/platform-linux-CDCDCD.svg?style=flat 103 | -------------------------------------------------------------------------------- /jwt-ecdsa/api/android/jwt-ecdsa-kt.api: -------------------------------------------------------------------------------- 1 | public final class com/appstractive/jwt/signatures/ECDSAKt { 2 | public static final fun es256 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 3 | public static final fun es256 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 4 | public static final fun es384 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 5 | public static final fun es384 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 6 | public static final fun es512 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 7 | public static final fun es512 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 8 | } 9 | 10 | public final class com/appstractive/jwt/signatures/ECDSASignerConfig { 11 | public fun ()V 12 | public final fun der-txG9UtI (Ljava/lang/String;Ljava/lang/String;)V 13 | public final fun der-txG9UtI ([BLjava/lang/String;)V 14 | public static synthetic fun der-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSASignerConfig;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V 15 | public static synthetic fun der-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSASignerConfig;[BLjava/lang/String;ILjava/lang/Object;)V 16 | public final fun pem-txG9UtI (Ljava/lang/String;Ljava/lang/String;)V 17 | public final fun pem-txG9UtI ([BLjava/lang/String;)V 18 | public static synthetic fun pem-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSASignerConfig;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V 19 | public static synthetic fun pem-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSASignerConfig;[BLjava/lang/String;ILjava/lang/Object;)V 20 | } 21 | 22 | public final class com/appstractive/jwt/signatures/ECDSAVerifierConfig { 23 | public fun ()V 24 | public final fun der-txG9UtI (Ljava/lang/String;Ljava/lang/String;)V 25 | public final fun der-txG9UtI ([BLjava/lang/String;)V 26 | public static synthetic fun der-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSAVerifierConfig;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V 27 | public static synthetic fun der-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSAVerifierConfig;[BLjava/lang/String;ILjava/lang/Object;)V 28 | public final fun pem-txG9UtI (Ljava/lang/String;Ljava/lang/String;)V 29 | public final fun pem-txG9UtI ([BLjava/lang/String;)V 30 | public static synthetic fun pem-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSAVerifierConfig;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V 31 | public static synthetic fun pem-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSAVerifierConfig;[BLjava/lang/String;ILjava/lang/Object;)V 32 | } 33 | 34 | -------------------------------------------------------------------------------- /jwt-ecdsa/api/jvm/jwt-ecdsa-kt.api: -------------------------------------------------------------------------------- 1 | public final class com/appstractive/jwt/signatures/ECDSAKt { 2 | public static final fun es256 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 3 | public static final fun es256 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 4 | public static final fun es384 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 5 | public static final fun es384 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 6 | public static final fun es512 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 7 | public static final fun es512 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 8 | } 9 | 10 | public final class com/appstractive/jwt/signatures/ECDSASignerConfig { 11 | public fun ()V 12 | public final fun der-txG9UtI (Ljava/lang/String;Ljava/lang/String;)V 13 | public final fun der-txG9UtI ([BLjava/lang/String;)V 14 | public static synthetic fun der-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSASignerConfig;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V 15 | public static synthetic fun der-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSASignerConfig;[BLjava/lang/String;ILjava/lang/Object;)V 16 | public final fun pem-txG9UtI (Ljava/lang/String;Ljava/lang/String;)V 17 | public final fun pem-txG9UtI ([BLjava/lang/String;)V 18 | public static synthetic fun pem-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSASignerConfig;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V 19 | public static synthetic fun pem-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSASignerConfig;[BLjava/lang/String;ILjava/lang/Object;)V 20 | } 21 | 22 | public final class com/appstractive/jwt/signatures/ECDSAVerifierConfig { 23 | public fun ()V 24 | public final fun der-txG9UtI (Ljava/lang/String;Ljava/lang/String;)V 25 | public final fun der-txG9UtI ([BLjava/lang/String;)V 26 | public static synthetic fun der-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSAVerifierConfig;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V 27 | public static synthetic fun der-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSAVerifierConfig;[BLjava/lang/String;ILjava/lang/Object;)V 28 | public final fun pem-txG9UtI (Ljava/lang/String;Ljava/lang/String;)V 29 | public final fun pem-txG9UtI ([BLjava/lang/String;)V 30 | public static synthetic fun pem-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSAVerifierConfig;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V 31 | public static synthetic fun pem-txG9UtI$default (Lcom/appstractive/jwt/signatures/ECDSAVerifierConfig;[BLjava/lang/String;ILjava/lang/Object;)V 32 | } 33 | 34 | -------------------------------------------------------------------------------- /jwt-ecdsa/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 3 | 4 | plugins { 5 | alias(libs.plugins.multiplatform) 6 | alias(libs.plugins.android.library) 7 | alias(libs.plugins.kotlinx.binary.compatibility) 8 | id("jwt.publication") 9 | } 10 | 11 | group = rootProject.group 12 | 13 | version = rootProject.version 14 | 15 | kotlin { 16 | androidTarget { 17 | publishAllLibraryVariants() 18 | compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } 19 | } 20 | 21 | jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } 22 | 23 | mingwX64() 24 | linuxArm64() 25 | linuxX64() 26 | 27 | js() 28 | @OptIn(ExperimentalWasmDsl::class) wasmJs() 29 | 30 | listOf( 31 | iosX64(), 32 | iosArm64(), 33 | iosSimulatorArm64(), 34 | macosX64(), 35 | macosArm64(), 36 | tvosX64(), 37 | tvosArm64(), 38 | tvosSimulatorArm64(), 39 | watchosX64(), 40 | watchosArm64(), 41 | watchosSimulatorArm64(), 42 | ) 43 | .forEach { 44 | it.binaries.framework { 45 | baseName = "JWT-ECDSA-KT" 46 | isStatic = true 47 | } 48 | } 49 | 50 | sourceSets { 51 | commonMain.dependencies { implementation(projects.jwtKt) } 52 | 53 | commonTest.dependencies { 54 | implementation(kotlin("test")) 55 | implementation(libs.test.kotlin.coroutines) 56 | } 57 | 58 | androidMain.dependencies { implementation(libs.crypto.jdk) } 59 | 60 | jvmMain.dependencies { implementation(libs.crypto.jdk) } 61 | 62 | appleMain.dependencies { implementation(libs.crypto.openssl3) } 63 | 64 | linuxMain.dependencies { implementation(libs.crypto.openssl3) } 65 | 66 | mingwMain.dependencies { implementation(libs.crypto.openssl3) } 67 | 68 | jsMain.dependencies { implementation(libs.crypto.webcrypto) } 69 | } 70 | } 71 | 72 | android { 73 | namespace = "com.appstractive.jwt.ecdsa" 74 | compileSdk = libs.versions.compileSdk.get().toInt() 75 | 76 | defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } 77 | sourceSets["main"].apply { manifest.srcFile("src/androidMain/AndroidManifest.xml") } 78 | compileOptions { 79 | sourceCompatibility = JavaVersion.VERSION_17 80 | targetCompatibility = JavaVersion.VERSION_17 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /jwt-ecdsa/src/commonMain/kotlin/com/appstractive/jwt/signatures/ECDSA.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.signatures 2 | 3 | import com.appstractive.jwt.Algorithm 4 | import com.appstractive.jwt.JWT 5 | import com.appstractive.jwt.Signer 6 | import com.appstractive.jwt.SigningAlgorithm 7 | import com.appstractive.jwt.VerificationAlgorithm 8 | import com.appstractive.jwt.Verifier 9 | import com.appstractive.jwt.digest 10 | import dev.whyoleg.cryptography.CryptographyAlgorithmId 11 | import dev.whyoleg.cryptography.CryptographyProvider 12 | import dev.whyoleg.cryptography.algorithms.Digest 13 | import dev.whyoleg.cryptography.algorithms.EC 14 | import dev.whyoleg.cryptography.algorithms.ECDSA 15 | import dev.whyoleg.cryptography.algorithms.ECDSA.SignatureFormat 16 | import dev.whyoleg.cryptography.operations.SignatureGenerator 17 | import dev.whyoleg.cryptography.operations.SignatureVerifier 18 | 19 | internal val provider by lazy { CryptographyProvider.Default } 20 | internal val ecdsa: ECDSA by lazy { provider.get(ECDSA) } 21 | 22 | fun Signer.es256(configure: ECDSASignerConfig.() -> Unit) { 23 | val config = ECDSASignerConfig().apply(configure) 24 | algorithm( 25 | algorithm = ECDSASigner(config = config), 26 | type = Algorithm.ES256, 27 | ) 28 | } 29 | 30 | fun Signer.es384(configure: ECDSASignerConfig.() -> Unit) { 31 | val config = ECDSASignerConfig().apply(configure) 32 | algorithm( 33 | algorithm = ECDSASigner(config = config), 34 | type = Algorithm.ES384, 35 | ) 36 | } 37 | 38 | fun Signer.es512(configure: ECDSASignerConfig.() -> Unit) { 39 | val config = ECDSASignerConfig().apply(configure) 40 | algorithm( 41 | algorithm = ECDSASigner(config = config), 42 | type = Algorithm.ES512, 43 | ) 44 | } 45 | 46 | fun Verifier.es256(configure: ECDSAVerifierConfig.() -> Unit) { 47 | val config = ECDSAVerifierConfig().apply(configure) 48 | algorithm( 49 | type = Algorithm.ES256, 50 | algorithm = ECDSAVerifier(config = config), 51 | ) 52 | } 53 | 54 | fun Verifier.es384(configure: ECDSAVerifierConfig.() -> Unit) { 55 | val config = ECDSAVerifierConfig().apply(configure) 56 | algorithm( 57 | type = Algorithm.ES384, 58 | algorithm = ECDSAVerifier(config = config), 59 | ) 60 | } 61 | 62 | fun Verifier.es512(configure: ECDSAVerifierConfig.() -> Unit) { 63 | val config = ECDSAVerifierConfig().apply(configure) 64 | algorithm( 65 | type = Algorithm.ES512, 66 | algorithm = ECDSAVerifier(config = config), 67 | ) 68 | } 69 | 70 | internal class ECDSASigner( 71 | private val config: ECDSASignerConfig, 72 | ) : SigningAlgorithm { 73 | 74 | override suspend fun generator(digest: CryptographyAlgorithmId): SignatureGenerator { 75 | return ecdsa 76 | .privateKeyDecoder(config.curve) 77 | .decodeFromByteArray(checkNotNull(config.privateKeyFormat), checkNotNull(config.privateKey)) 78 | .signatureGenerator(digest = digest, format = config.signatureFormat) 79 | } 80 | } 81 | 82 | internal class ECDSAVerifier( 83 | private val config: ECDSAVerifierConfig, 84 | ) : VerificationAlgorithm { 85 | override suspend fun verifier(jwt: JWT): SignatureVerifier { 86 | return ecdsa 87 | .publicKeyDecoder(curve = config.curve) 88 | .decodeFromByteArray(checkNotNull(config.publicKeyFormat), checkNotNull(config.publicKey)) 89 | .signatureVerifier(digest = jwt.header.alg.digest, format = config.signatureFormat) 90 | } 91 | } 92 | 93 | class ECDSASignerConfig { 94 | internal var privateKey: ByteArray? = null 95 | internal var privateKeyFormat: EC.PrivateKey.Format? = null 96 | internal var curve: EC.Curve = EC.Curve.P256 97 | internal var signatureFormat: SignatureFormat = SignatureFormat.RAW 98 | 99 | fun pem(key: ByteArray, curve: EC.Curve = EC.Curve.P256) { 100 | privateKey = key 101 | privateKeyFormat = EC.PrivateKey.Format.PEM 102 | this.curve = curve 103 | } 104 | 105 | fun pem(key: String, curve: EC.Curve = EC.Curve.P256) { 106 | privateKey = key.encodeToByteArray() 107 | privateKeyFormat = EC.PrivateKey.Format.PEM 108 | this.curve = curve 109 | } 110 | 111 | fun der(key: ByteArray, curve: EC.Curve = EC.Curve.P256) { 112 | privateKey = key 113 | privateKeyFormat = EC.PrivateKey.Format.DER 114 | this.curve = curve 115 | } 116 | 117 | fun der(key: String, curve: EC.Curve = EC.Curve.P256) { 118 | privateKey = key.encodeToByteArray() 119 | privateKeyFormat = EC.PrivateKey.Format.DER 120 | this.curve = curve 121 | } 122 | } 123 | 124 | class ECDSAVerifierConfig { 125 | internal var publicKey: ByteArray? = null 126 | internal var publicKeyFormat: EC.PublicKey.Format? = null 127 | internal var curve: EC.Curve = EC.Curve.P256 128 | internal var signatureFormat: SignatureFormat = SignatureFormat.RAW 129 | 130 | fun pem(key: ByteArray, curve: EC.Curve = EC.Curve.P256) { 131 | publicKey = key 132 | publicKeyFormat = EC.PublicKey.Format.PEM 133 | this.curve = curve 134 | } 135 | 136 | fun pem(key: String, curve: EC.Curve = EC.Curve.P256) { 137 | publicKey = key.encodeToByteArray() 138 | publicKeyFormat = EC.PublicKey.Format.PEM 139 | this.curve = curve 140 | } 141 | 142 | fun der(key: ByteArray, curve: EC.Curve = EC.Curve.P256) { 143 | publicKey = key 144 | publicKeyFormat = EC.PublicKey.Format.DER 145 | this.curve = curve 146 | } 147 | 148 | fun der(key: String, curve: EC.Curve = EC.Curve.P256) { 149 | publicKey = key.encodeToByteArray() 150 | publicKeyFormat = EC.PublicKey.Format.DER 151 | this.curve = curve 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /jwt-ecdsa/src/commonTest/kotlin/com/appstractive/jwt/signatures/JwtVerifierTests.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.signatures 2 | 3 | import com.appstractive.jwt.Algorithm 4 | import com.appstractive.jwt.JWT 5 | import com.appstractive.jwt.Signer 6 | import com.appstractive.jwt.Verifier 7 | import com.appstractive.jwt.jwt 8 | import com.appstractive.jwt.sign 9 | import com.appstractive.jwt.verify 10 | import dev.whyoleg.cryptography.algorithms.EC 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertFalse 14 | import kotlin.test.assertTrue 15 | import kotlinx.coroutines.test.runTest 16 | 17 | class JwtVerifierTests { 18 | 19 | @Test 20 | fun createJwtES256() = runTest { 21 | val jwt = 22 | createJwtEC( 23 | builder = { es256 { pem(it) } }, 24 | verifier = { es256 { pem(it) } }, 25 | ) 26 | 27 | assertEquals(Algorithm.ES256, jwt.header.alg) 28 | } 29 | 30 | @Test 31 | fun createJwtES384() = runTest { 32 | val jwt = 33 | createJwtEC( 34 | builder = { es384 { pem(it) } }, 35 | verifier = { es384 { pem(it) } }, 36 | ) 37 | 38 | assertEquals(Algorithm.ES384, jwt.header.alg) 39 | } 40 | 41 | @Test 42 | fun createJwtES512() = runTest { 43 | val jwt = 44 | createJwtEC( 45 | builder = { es512 { pem(it) } }, 46 | verifier = { es512 { pem(it) } }, 47 | ) 48 | 49 | assertEquals(Algorithm.ES512, jwt.header.alg) 50 | } 51 | 52 | private suspend fun createJwtEC( 53 | curve: EC.Curve = EC.Curve.P256, 54 | builder: Signer.(ByteArray) -> Unit, 55 | verifier: Verifier.(ByteArray) -> Unit, 56 | ): JWT { 57 | val unsignedJwt = jwt { claims { issuer = "someone" } } 58 | val keys = ecdsa.keyPairGenerator(curve).generateKey() 59 | val privateKey = keys.privateKey.encodeToByteArray(EC.PrivateKey.Format.PEM) 60 | val publicKey = keys.publicKey.encodeToByteArray(EC.PublicKey.Format.PEM) 61 | println(privateKey.decodeToString()) 62 | println(publicKey.decodeToString()) 63 | 64 | val signedJwt = unsignedJwt.sign { builder(privateKey) } 65 | 66 | println(signedJwt.toString()) 67 | 68 | assertTrue(signedJwt.verify { verifier(publicKey) }) 69 | 70 | val wrongKey = 71 | ecdsa.keyPairGenerator(curve).generateKey().publicKey.encodeToByteArray(EC.PublicKey.Format.PEM) 72 | assertFalse(signedJwt.verify { verifier(wrongKey) }) 73 | 74 | return signedJwt 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /jwt-hmac/README.md: -------------------------------------------------------------------------------- 1 | # JWT Kotlin Multiplatform - HMAC 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.appstractive/jwt-hmac-kt?label=Maven%20Central)](https://central.sonatype.com/artifact/com.appstractive/jwt-hmac-kt) 4 | 5 | ![badge][badge-android] 6 | ![badge][badge-apple] 7 | ![badge][badge-jvm] 8 | ![badge][badge-js] 9 | ![badge][badge-win] 10 | ![badge][badge-linux] 11 | 12 | Sign and verify JWTs using HMAC algorithm. 13 | 14 | ## Supported Algorithms 15 | 16 | - HS256 17 | - HS384 18 | - HS512 19 | 20 | ## Usage 21 | 22 | ### Installation 23 | 24 | Gradle: 25 | 26 | ``` 27 | commonMain.dependencies { 28 | implementation("com.appstractive:jwt-kt:1.1.0") 29 | implementation("com.appstractive:jwt-hmac-kt:1.1.0") 30 | } 31 | ``` 32 | 33 | ### Sign JWT 34 | 35 | ```kotlin 36 | val jwt: UnsignedJWT = jwt { 37 | claims { issuer = "example.com" } 38 | } 39 | val mySecret = CryptographyRandom.nextBytes(64) 40 | 41 | val signedJWT = jwt.sign { 42 | hs256 { secret = mySecret } 43 | // or with different hashing 44 | // hs384 { secret = mySecret } 45 | // hs512 { secret = mySecret } 46 | } 47 | ``` 48 | 49 | ### Verify JWT 50 | 51 | ```kotlin 52 | val jwt = JWT.from("eyJraWQiOiJzLWRmZmZkMDJlLTlhNDItNDQzMC1hNT...") 53 | val mySecret = CryptographyRandom.nextBytes(64) 54 | 55 | val isValid = jwt.verify { 56 | hs256 { secret = mySecret } 57 | // or with different hashing 58 | // hs384 { secret = mySecret } 59 | // hs512 { secret = mySecret } 60 | 61 | // verify issuer 62 | issuer("example.com") 63 | // verify audience 64 | audience("api.example.com") 65 | // verify expiration 66 | expiresAt() 67 | // verify not before time 68 | notBefore() 69 | } 70 | ``` 71 | 72 | ## License 73 | 74 | ``` 75 | Copyright 2024 Andreas Schulz. 76 | 77 | Licensed under the Apache License, Version 2.0 (the "License"); 78 | you may not use this file except in compliance with the License. 79 | You may obtain a copy of the License at 80 | 81 | http://www.apache.org/licenses/LICENSE-2.0 82 | 83 | Unless required by applicable law or agreed to in writing, software 84 | distributed under the License is distributed on an "AS IS" BASIS, 85 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 86 | See the License for the specific language governing permissions and 87 | limitations under the License. 88 | ``` 89 | 90 | [badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat 91 | 92 | [badge-apple]: http://img.shields.io/badge/platform-apple-111111.svg?style=flat 93 | 94 | [badge-jvm]: http://img.shields.io/badge/platform-jvm-CDCDCD.svg?style=flat 95 | 96 | [badge-js]: http://img.shields.io/badge/platform-js-f7df1e.svg?style=flat 97 | 98 | [badge-win]: http://img.shields.io/badge/platform-win-357EC7.svg?style=flat 99 | 100 | [badge-linux]: http://img.shields.io/badge/platform-linux-CDCDCD.svg?style=flat 101 | -------------------------------------------------------------------------------- /jwt-hmac/api/android/jwt-hmac-kt.api: -------------------------------------------------------------------------------- 1 | public final class com/appstractive/jwt/signatures/HMACKt { 2 | public static final fun hs256 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 3 | public static final fun hs256 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 4 | public static final fun hs384 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 5 | public static final fun hs384 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 6 | public static final fun hs512 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 7 | public static final fun hs512 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 8 | } 9 | 10 | public final class com/appstractive/jwt/signatures/Hmac : com/appstractive/jwt/SigningAlgorithm, com/appstractive/jwt/VerificationAlgorithm { 11 | public fun (Lcom/appstractive/jwt/signatures/Hmac$Config;)V 12 | public fun generator (Ldev/whyoleg/cryptography/CryptographyAlgorithmId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 13 | public fun sign (Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 14 | public fun verifier (Lcom/appstractive/jwt/JWT;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 15 | public fun verify (Lcom/appstractive/jwt/JWT;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 16 | } 17 | 18 | public final class com/appstractive/jwt/signatures/Hmac$Config { 19 | public fun ()V 20 | public final fun getSecret ()[B 21 | public final fun setSecret ([B)V 22 | } 23 | 24 | -------------------------------------------------------------------------------- /jwt-hmac/api/jvm/jwt-hmac-kt.api: -------------------------------------------------------------------------------- 1 | public final class com/appstractive/jwt/signatures/HMACKt { 2 | public static final fun hs256 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 3 | public static final fun hs256 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 4 | public static final fun hs384 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 5 | public static final fun hs384 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 6 | public static final fun hs512 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 7 | public static final fun hs512 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 8 | } 9 | 10 | public final class com/appstractive/jwt/signatures/Hmac : com/appstractive/jwt/SigningAlgorithm, com/appstractive/jwt/VerificationAlgorithm { 11 | public fun (Lcom/appstractive/jwt/signatures/Hmac$Config;)V 12 | public fun generator (Ldev/whyoleg/cryptography/CryptographyAlgorithmId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 13 | public fun sign (Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 14 | public fun verifier (Lcom/appstractive/jwt/JWT;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 15 | public fun verify (Lcom/appstractive/jwt/JWT;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 16 | } 17 | 18 | public final class com/appstractive/jwt/signatures/Hmac$Config { 19 | public fun ()V 20 | public final fun getSecret ()[B 21 | public final fun setSecret ([B)V 22 | } 23 | 24 | -------------------------------------------------------------------------------- /jwt-hmac/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 3 | 4 | plugins { 5 | alias(libs.plugins.multiplatform) 6 | alias(libs.plugins.android.library) 7 | alias(libs.plugins.kotlinx.binary.compatibility) 8 | id("jwt.publication") 9 | } 10 | 11 | group = rootProject.group 12 | 13 | version = rootProject.version 14 | 15 | kotlin { 16 | androidTarget { 17 | publishAllLibraryVariants() 18 | compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } 19 | } 20 | 21 | jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } 22 | 23 | mingwX64() 24 | linuxArm64() 25 | linuxX64() 26 | 27 | js() 28 | @OptIn(ExperimentalWasmDsl::class) wasmJs() 29 | 30 | listOf( 31 | iosX64(), 32 | iosArm64(), 33 | iosSimulatorArm64(), 34 | macosX64(), 35 | macosArm64(), 36 | tvosX64(), 37 | tvosArm64(), 38 | tvosSimulatorArm64(), 39 | watchosX64(), 40 | watchosArm64(), 41 | watchosSimulatorArm64(), 42 | ) 43 | .forEach { 44 | it.binaries.framework { 45 | baseName = "JWT-HMAC-KT" 46 | isStatic = true 47 | } 48 | } 49 | 50 | sourceSets { 51 | commonMain.dependencies { implementation(projects.jwtKt) } 52 | 53 | commonTest.dependencies { 54 | implementation(kotlin("test")) 55 | implementation(libs.test.kotlin.coroutines) 56 | } 57 | 58 | androidMain.dependencies { implementation(libs.crypto.jdk) } 59 | 60 | jvmMain.dependencies { implementation(libs.crypto.jdk) } 61 | 62 | appleMain.dependencies { implementation(libs.crypto.openssl3) } 63 | 64 | linuxMain.dependencies { implementation(libs.crypto.openssl3) } 65 | 66 | mingwMain.dependencies { implementation(libs.crypto.openssl3) } 67 | 68 | jsMain.dependencies { implementation(libs.crypto.webcrypto) } 69 | } 70 | } 71 | 72 | android { 73 | namespace = "com.appstractive.jwt.hmac" 74 | compileSdk = libs.versions.compileSdk.get().toInt() 75 | 76 | defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } 77 | sourceSets["main"].apply { manifest.srcFile("src/androidMain/AndroidManifest.xml") } 78 | compileOptions { 79 | sourceCompatibility = JavaVersion.VERSION_17 80 | targetCompatibility = JavaVersion.VERSION_17 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /jwt-hmac/src/commonMain/kotlin/com/appstractive/jwt/signatures/HMAC.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.signatures 2 | 3 | import com.appstractive.jwt.Algorithm 4 | import com.appstractive.jwt.JWT 5 | import com.appstractive.jwt.Signer 6 | import com.appstractive.jwt.SigningAlgorithm 7 | import com.appstractive.jwt.VerificationAlgorithm 8 | import com.appstractive.jwt.Verifier 9 | import com.appstractive.jwt.digest 10 | import dev.whyoleg.cryptography.CryptographyAlgorithmId 11 | import dev.whyoleg.cryptography.CryptographyProvider 12 | import dev.whyoleg.cryptography.algorithms.Digest 13 | import dev.whyoleg.cryptography.algorithms.HMAC 14 | import dev.whyoleg.cryptography.operations.SignatureGenerator 15 | import dev.whyoleg.cryptography.operations.SignatureVerifier 16 | 17 | internal val provider by lazy { CryptographyProvider.Default } 18 | internal val hmac by lazy { provider.get(HMAC) } 19 | 20 | fun Signer.hs256(configure: Hmac.Config.() -> Unit) { 21 | val config = Hmac.Config().apply(configure) 22 | algorithm( 23 | algorithm = Hmac(config = config), 24 | type = Algorithm.HS256, 25 | ) 26 | } 27 | 28 | fun Signer.hs384(configure: Hmac.Config.() -> Unit) { 29 | val config = Hmac.Config().apply(configure) 30 | algorithm( 31 | algorithm = Hmac(config = config), 32 | type = Algorithm.HS384, 33 | ) 34 | } 35 | 36 | fun Signer.hs512(configure: Hmac.Config.() -> Unit) { 37 | val config = Hmac.Config().apply(configure) 38 | algorithm( 39 | algorithm = Hmac(config = config), 40 | type = Algorithm.HS512, 41 | ) 42 | } 43 | 44 | fun Verifier.hs256(configure: Hmac.Config.() -> Unit) { 45 | val config = Hmac.Config().apply(configure) 46 | algorithm( 47 | type = Algorithm.HS256, 48 | algorithm = Hmac(config = config), 49 | ) 50 | } 51 | 52 | fun Verifier.hs384(configure: Hmac.Config.() -> Unit) { 53 | val config = Hmac.Config().apply(configure) 54 | algorithm( 55 | type = Algorithm.HS384, 56 | algorithm = Hmac(config = config), 57 | ) 58 | } 59 | 60 | fun Verifier.hs512(configure: Hmac.Config.() -> Unit) { 61 | val config = Hmac.Config().apply(configure) 62 | algorithm( 63 | type = Algorithm.HS512, 64 | algorithm = Hmac(config = config), 65 | ) 66 | } 67 | 68 | class Hmac( 69 | private val config: Config, 70 | ) : SigningAlgorithm, VerificationAlgorithm { 71 | 72 | override suspend fun generator(digest: CryptographyAlgorithmId): SignatureGenerator { 73 | return hmac 74 | .keyDecoder(digest) 75 | .decodeFromByteArray(HMAC.Key.Format.RAW, config.secret) 76 | .signatureGenerator() 77 | } 78 | 79 | override suspend fun verifier(jwt: JWT): SignatureVerifier { 80 | return hmac 81 | .keyDecoder(jwt.header.alg.digest) 82 | .decodeFromByteArray(HMAC.Key.Format.RAW, config.secret) 83 | .signatureVerifier() 84 | } 85 | 86 | class Config { 87 | var secret: ByteArray = ByteArray(0) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /jwt-hmac/src/commonTest/kotlin/com/appstractive/jwt/signatures/JwtVerifierTests.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.signatures 2 | 3 | import com.appstractive.jwt.Algorithm 4 | import com.appstractive.jwt.JWT 5 | import com.appstractive.jwt.Signer 6 | import com.appstractive.jwt.Verifier 7 | import com.appstractive.jwt.jwt 8 | import com.appstractive.jwt.sign 9 | import com.appstractive.jwt.verify 10 | import dev.whyoleg.cryptography.random.CryptographyRandom 11 | import kotlin.io.encoding.Base64.Default.UrlSafe 12 | import kotlin.io.encoding.ExperimentalEncodingApi 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | import kotlin.test.assertFalse 16 | import kotlin.test.assertTrue 17 | import kotlinx.coroutines.test.runTest 18 | 19 | class JwtVerifierTests { 20 | 21 | @Test 22 | fun createJwtHS256() = runTest { 23 | val jwt = 24 | createJwtHmac( 25 | builder = { hs256 { secret = it } }, 26 | verifier = { hs256 { secret = it } }, 27 | ) 28 | 29 | assertEquals(Algorithm.HS256, jwt.header.alg) 30 | } 31 | 32 | @Test 33 | fun createJwtHS384() = runTest { 34 | val jwt = 35 | createJwtHmac( 36 | secret = CryptographyRandom.nextBytes(128), 37 | builder = { hs384 { secret = it } }, 38 | verifier = { hs384 { secret = it } }, 39 | ) 40 | 41 | assertEquals(Algorithm.HS384, jwt.header.alg) 42 | } 43 | 44 | @Test 45 | fun createJwtHS512() = runTest { 46 | val jwt = 47 | createJwtHmac( 48 | secret = CryptographyRandom.nextBytes(128), 49 | builder = { hs512 { secret = it } }, 50 | verifier = { hs512 { secret = it } }, 51 | ) 52 | 53 | assertEquals(Algorithm.HS512, jwt.header.alg) 54 | } 55 | 56 | @OptIn(ExperimentalEncodingApi::class) 57 | private suspend fun createJwtHmac( 58 | secret: ByteArray = CryptographyRandom.nextBytes(64), 59 | builder: Signer.(ByteArray) -> Unit, 60 | verifier: Verifier.(ByteArray) -> Unit, 61 | ): JWT { 62 | val unsignedJwt = jwt { claims { issuer = "someone" } } 63 | 64 | val signedJwt = unsignedJwt.sign { builder(secret) } 65 | 66 | println(UrlSafe.encode(secret).replace("=", "")) 67 | println(signedJwt.toString()) 68 | 69 | assertTrue(signedJwt.verify { verifier(secret) }) 70 | 71 | assertFalse(signedJwt.verify { verifier(CryptographyRandom.nextBytes(secret.size)) }) 72 | 73 | return signedJwt 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /jwt-jwks/README.md: -------------------------------------------------------------------------------- 1 | # JWT Kotlin Multiplatform - JWKS (TODO) 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.appstractive/jwt-jwks-kt?label=Maven%20Central)](https://central.sonatype.com/artifact/com.appstractive/jwt-jwks-kt) 4 | 5 | ![badge][badge-android] 6 | ![badge][badge-apple] 7 | ![badge][badge-jvm] 8 | ![badge][badge-js] 9 | ![badge][badge-win] 10 | ![badge][badge-linux] 11 | 12 | Verify JWTs using JSONWebKeySets. 13 | 14 | # Attention! 15 | 16 | JWKS is not yet supported, because it is not yet supported in cryptography-kotlin for all platforms. See discussion here: whyoleg/cryptography-kotlin#16 17 | 18 | ## Usage 19 | 20 | ### Installation 21 | 22 | Gradle: 23 | 24 | TODO: Not yet published, requires JWK support from kotlin crypto library 25 | 26 | ``` 27 | commonMain.dependencies { 28 | implementation("com.appstractive:jwt-kt:1.1.0") 29 | implementation("com.appstractive:jwt-jwks-kt:1.1.0") 30 | } 31 | ``` 32 | 33 | ### Verify JWT 34 | 35 | ```kotlin 36 | val jwt = JWT.from("eyJraWQiOiJzLWRmZmZkMDJlLTlhNDItNDQzMC1hNT...") 37 | 38 | val isValid = jwt.verify { 39 | jwks { 40 | endpoint = "http://example.com/.well-known/jwt/jwks.json" 41 | cacheDuration = 1.minutes 42 | } 43 | 44 | // verify issuer 45 | issuer("example.com") 46 | // verify audience 47 | audience("api.example.com") 48 | // verify expiration 49 | expiresAt() 50 | // verify not before time 51 | notBefore() 52 | } 53 | ``` 54 | 55 | ## License 56 | 57 | ``` 58 | Copyright 2024 Andreas Schulz. 59 | 60 | Licensed under the Apache License, Version 2.0 (the "License"); 61 | you may not use this file except in compliance with the License. 62 | You may obtain a copy of the License at 63 | 64 | http://www.apache.org/licenses/LICENSE-2.0 65 | 66 | Unless required by applicable law or agreed to in writing, software 67 | distributed under the License is distributed on an "AS IS" BASIS, 68 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 69 | See the License for the specific language governing permissions and 70 | limitations under the License. 71 | ``` 72 | 73 | [badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat 74 | 75 | [badge-apple]: http://img.shields.io/badge/platform-apple-111111.svg?style=flat 76 | 77 | [badge-jvm]: http://img.shields.io/badge/platform-jvm-CDCDCD.svg?style=flat 78 | 79 | [badge-js]: http://img.shields.io/badge/platform-js-f7df1e.svg?style=flat 80 | 81 | [badge-win]: http://img.shields.io/badge/platform-win-357EC7.svg?style=flat 82 | 83 | [badge-linux]: http://img.shields.io/badge/platform-linux-CDCDCD.svg?style=flat 84 | -------------------------------------------------------------------------------- /jwt-jwks/api/android/jwt-jwks-kt.api: -------------------------------------------------------------------------------- 1 | public final class com/appstractive/jwt/Curve : java/lang/Enum { 2 | public static final field P256 Lcom/appstractive/jwt/Curve; 3 | public static final field P384 Lcom/appstractive/jwt/Curve; 4 | public static final field P521 Lcom/appstractive/jwt/Curve; 5 | public static fun getEntries ()Lkotlin/enums/EnumEntries; 6 | public static fun valueOf (Ljava/lang/String;)Lcom/appstractive/jwt/Curve; 7 | public static fun values ()[Lcom/appstractive/jwt/Curve; 8 | } 9 | 10 | public abstract interface class com/appstractive/jwt/JSONWebKey { 11 | public static final field Companion Lcom/appstractive/jwt/JSONWebKey$Companion; 12 | public abstract fun getAlg ()Lcom/appstractive/jwt/Algorithm; 13 | public abstract fun getKid ()Ljava/lang/String; 14 | } 15 | 16 | public final class com/appstractive/jwt/JSONWebKey$Companion { 17 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 18 | } 19 | 20 | public final class com/appstractive/jwt/JSONWebKeyEC : com/appstractive/jwt/JSONWebKey { 21 | public static final field Companion Lcom/appstractive/jwt/JSONWebKeyEC$Companion; 22 | public fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Lcom/appstractive/jwt/Curve;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V 23 | public synthetic fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Lcom/appstractive/jwt/Curve;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 24 | public final fun component1 ()Lcom/appstractive/jwt/Algorithm; 25 | public final fun component2 ()Ljava/lang/String; 26 | public final fun component3 ()Lcom/appstractive/jwt/Curve; 27 | public final fun component4 ()Ljava/lang/String; 28 | public final fun component5 ()Ljava/lang/String; 29 | public final fun component6 ()Ljava/lang/String; 30 | public final fun copy (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Lcom/appstractive/jwt/Curve;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/appstractive/jwt/JSONWebKeyEC; 31 | public static synthetic fun copy$default (Lcom/appstractive/jwt/JSONWebKeyEC;Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Lcom/appstractive/jwt/Curve;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/appstractive/jwt/JSONWebKeyEC; 32 | public fun equals (Ljava/lang/Object;)Z 33 | public fun getAlg ()Lcom/appstractive/jwt/Algorithm; 34 | public final fun getCrv ()Lcom/appstractive/jwt/Curve; 35 | public final fun getD ()Ljava/lang/String; 36 | public fun getKid ()Ljava/lang/String; 37 | public final fun getX ()Ljava/lang/String; 38 | public final fun getY ()Ljava/lang/String; 39 | public fun hashCode ()I 40 | public fun toString ()Ljava/lang/String; 41 | } 42 | 43 | public synthetic class com/appstractive/jwt/JSONWebKeyEC$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 44 | public static final field INSTANCE Lcom/appstractive/jwt/JSONWebKeyEC$$serializer; 45 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 46 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/appstractive/jwt/JSONWebKeyEC; 47 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 48 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 49 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/appstractive/jwt/JSONWebKeyEC;)V 50 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 51 | } 52 | 53 | public final class com/appstractive/jwt/JSONWebKeyEC$Companion { 54 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 55 | } 56 | 57 | public final class com/appstractive/jwt/JSONWebKeyExtKt { 58 | public static final fun getKey (Lcom/appstractive/jwt/JSONWebKeySet;Ljava/lang/String;)Lcom/appstractive/jwt/JSONWebKey; 59 | public static final fun getVerifier (Lcom/appstractive/jwt/JSONWebKey;Lkotlinx/serialization/json/Json;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 60 | public static synthetic fun getVerifier$default (Lcom/appstractive/jwt/JSONWebKey;Lkotlinx/serialization/json/Json;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; 61 | } 62 | 63 | public final class com/appstractive/jwt/JSONWebKeyHMAC : com/appstractive/jwt/JSONWebKey { 64 | public static final field Companion Lcom/appstractive/jwt/JSONWebKeyHMAC$Companion; 65 | public fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;)V 66 | public synthetic fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 67 | public final fun component1 ()Lcom/appstractive/jwt/Algorithm; 68 | public final fun component2 ()Ljava/lang/String; 69 | public final fun component3 ()Ljava/lang/String; 70 | public final fun copy (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;)Lcom/appstractive/jwt/JSONWebKeyHMAC; 71 | public static synthetic fun copy$default (Lcom/appstractive/jwt/JSONWebKeyHMAC;Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/appstractive/jwt/JSONWebKeyHMAC; 72 | public fun equals (Ljava/lang/Object;)Z 73 | public fun getAlg ()Lcom/appstractive/jwt/Algorithm; 74 | public final fun getK ()Ljava/lang/String; 75 | public fun getKid ()Ljava/lang/String; 76 | public fun hashCode ()I 77 | public fun toString ()Ljava/lang/String; 78 | } 79 | 80 | public synthetic class com/appstractive/jwt/JSONWebKeyHMAC$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 81 | public static final field INSTANCE Lcom/appstractive/jwt/JSONWebKeyHMAC$$serializer; 82 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 83 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/appstractive/jwt/JSONWebKeyHMAC; 84 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 85 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 86 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/appstractive/jwt/JSONWebKeyHMAC;)V 87 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 88 | } 89 | 90 | public final class com/appstractive/jwt/JSONWebKeyHMAC$Companion { 91 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 92 | } 93 | 94 | public final class com/appstractive/jwt/JSONWebKeyKt { 95 | public static final fun getCurve (Lcom/appstractive/jwt/Curve;)Ljava/lang/String; 96 | } 97 | 98 | public final class com/appstractive/jwt/JSONWebKeyRSA : com/appstractive/jwt/JSONWebKey { 99 | public static final field Companion Lcom/appstractive/jwt/JSONWebKeyRSA$Companion; 100 | public fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V 101 | public synthetic fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 102 | public final fun component1 ()Lcom/appstractive/jwt/Algorithm; 103 | public final fun component10 ()Ljava/lang/String; 104 | public final fun component11 ()Ljava/lang/String; 105 | public final fun component2 ()Ljava/lang/String; 106 | public final fun component3 ()Ljava/lang/String; 107 | public final fun component4 ()Ljava/lang/String; 108 | public final fun component5 ()Ljava/lang/String; 109 | public final fun component6 ()Ljava/lang/String; 110 | public final fun component7 ()Ljava/lang/String; 111 | public final fun component8 ()Ljava/lang/String; 112 | public final fun component9 ()Ljava/lang/String; 113 | public final fun copy (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/appstractive/jwt/JSONWebKeyRSA; 114 | public static synthetic fun copy$default (Lcom/appstractive/jwt/JSONWebKeyRSA;Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/appstractive/jwt/JSONWebKeyRSA; 115 | public fun equals (Ljava/lang/Object;)Z 116 | public fun getAlg ()Lcom/appstractive/jwt/Algorithm; 117 | public final fun getD ()Ljava/lang/String; 118 | public final fun getDp ()Ljava/lang/String; 119 | public final fun getDq ()Ljava/lang/String; 120 | public final fun getE ()Ljava/lang/String; 121 | public fun getKid ()Ljava/lang/String; 122 | public final fun getN ()Ljava/lang/String; 123 | public final fun getP ()Ljava/lang/String; 124 | public final fun getQ ()Ljava/lang/String; 125 | public final fun getQi ()Ljava/lang/String; 126 | public final fun getUse ()Ljava/lang/String; 127 | public fun hashCode ()I 128 | public fun toString ()Ljava/lang/String; 129 | } 130 | 131 | public synthetic class com/appstractive/jwt/JSONWebKeyRSA$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 132 | public static final field INSTANCE Lcom/appstractive/jwt/JSONWebKeyRSA$$serializer; 133 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 134 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/appstractive/jwt/JSONWebKeyRSA; 135 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 136 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 137 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/appstractive/jwt/JSONWebKeyRSA;)V 138 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 139 | } 140 | 141 | public final class com/appstractive/jwt/JSONWebKeyRSA$Companion { 142 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 143 | } 144 | 145 | public final class com/appstractive/jwt/JSONWebKeySet { 146 | public static final field Companion Lcom/appstractive/jwt/JSONWebKeySet$Companion; 147 | public fun (Ljava/util/List;)V 148 | public final fun component1 ()Ljava/util/List; 149 | public final fun copy (Ljava/util/List;)Lcom/appstractive/jwt/JSONWebKeySet; 150 | public static synthetic fun copy$default (Lcom/appstractive/jwt/JSONWebKeySet;Ljava/util/List;ILjava/lang/Object;)Lcom/appstractive/jwt/JSONWebKeySet; 151 | public fun equals (Ljava/lang/Object;)Z 152 | public final fun getKeys ()Ljava/util/List; 153 | public fun hashCode ()I 154 | public fun toString ()Ljava/lang/String; 155 | } 156 | 157 | public synthetic class com/appstractive/jwt/JSONWebKeySet$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 158 | public static final field INSTANCE Lcom/appstractive/jwt/JSONWebKeySet$$serializer; 159 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 160 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/appstractive/jwt/JSONWebKeySet; 161 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 162 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 163 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/appstractive/jwt/JSONWebKeySet;)V 164 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 165 | } 166 | 167 | public final class com/appstractive/jwt/JSONWebKeySet$Companion { 168 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 169 | } 170 | 171 | public final class com/appstractive/jwt/JwksConfig { 172 | public fun ()V 173 | public final fun getCacheDuration-UwyO8pc ()J 174 | public final fun getClient ()Lio/ktor/client/HttpClient; 175 | public final fun getEndpoint ()Ljava/lang/String; 176 | public final fun setCacheDuration-LRDsOJo (J)V 177 | public final fun setClient (Lio/ktor/client/HttpClient;)V 178 | public final fun setEndpoint (Ljava/lang/String;)V 179 | } 180 | 181 | public final class com/appstractive/jwt/JwksKt { 182 | public static final fun jwks (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 183 | } 184 | 185 | -------------------------------------------------------------------------------- /jwt-jwks/api/jvm/jwt-jwks-kt.api: -------------------------------------------------------------------------------- 1 | public final class com/appstractive/jwt/Curve : java/lang/Enum { 2 | public static final field P256 Lcom/appstractive/jwt/Curve; 3 | public static final field P384 Lcom/appstractive/jwt/Curve; 4 | public static final field P521 Lcom/appstractive/jwt/Curve; 5 | public static fun getEntries ()Lkotlin/enums/EnumEntries; 6 | public static fun valueOf (Ljava/lang/String;)Lcom/appstractive/jwt/Curve; 7 | public static fun values ()[Lcom/appstractive/jwt/Curve; 8 | } 9 | 10 | public abstract interface class com/appstractive/jwt/JSONWebKey { 11 | public static final field Companion Lcom/appstractive/jwt/JSONWebKey$Companion; 12 | public abstract fun getAlg ()Lcom/appstractive/jwt/Algorithm; 13 | public abstract fun getKid ()Ljava/lang/String; 14 | } 15 | 16 | public final class com/appstractive/jwt/JSONWebKey$Companion { 17 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 18 | } 19 | 20 | public final class com/appstractive/jwt/JSONWebKeyEC : com/appstractive/jwt/JSONWebKey { 21 | public static final field Companion Lcom/appstractive/jwt/JSONWebKeyEC$Companion; 22 | public fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Lcom/appstractive/jwt/Curve;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V 23 | public synthetic fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Lcom/appstractive/jwt/Curve;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 24 | public final fun component1 ()Lcom/appstractive/jwt/Algorithm; 25 | public final fun component2 ()Ljava/lang/String; 26 | public final fun component3 ()Lcom/appstractive/jwt/Curve; 27 | public final fun component4 ()Ljava/lang/String; 28 | public final fun component5 ()Ljava/lang/String; 29 | public final fun component6 ()Ljava/lang/String; 30 | public final fun copy (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Lcom/appstractive/jwt/Curve;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/appstractive/jwt/JSONWebKeyEC; 31 | public static synthetic fun copy$default (Lcom/appstractive/jwt/JSONWebKeyEC;Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Lcom/appstractive/jwt/Curve;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/appstractive/jwt/JSONWebKeyEC; 32 | public fun equals (Ljava/lang/Object;)Z 33 | public fun getAlg ()Lcom/appstractive/jwt/Algorithm; 34 | public final fun getCrv ()Lcom/appstractive/jwt/Curve; 35 | public final fun getD ()Ljava/lang/String; 36 | public fun getKid ()Ljava/lang/String; 37 | public final fun getX ()Ljava/lang/String; 38 | public final fun getY ()Ljava/lang/String; 39 | public fun hashCode ()I 40 | public fun toString ()Ljava/lang/String; 41 | } 42 | 43 | public synthetic class com/appstractive/jwt/JSONWebKeyEC$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 44 | public static final field INSTANCE Lcom/appstractive/jwt/JSONWebKeyEC$$serializer; 45 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 46 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/appstractive/jwt/JSONWebKeyEC; 47 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 48 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 49 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/appstractive/jwt/JSONWebKeyEC;)V 50 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 51 | public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; 52 | } 53 | 54 | public final class com/appstractive/jwt/JSONWebKeyEC$Companion { 55 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 56 | } 57 | 58 | public final class com/appstractive/jwt/JSONWebKeyExtKt { 59 | public static final fun getKey (Lcom/appstractive/jwt/JSONWebKeySet;Ljava/lang/String;)Lcom/appstractive/jwt/JSONWebKey; 60 | public static final fun getVerifier (Lcom/appstractive/jwt/JSONWebKey;Lkotlinx/serialization/json/Json;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 61 | public static synthetic fun getVerifier$default (Lcom/appstractive/jwt/JSONWebKey;Lkotlinx/serialization/json/Json;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; 62 | } 63 | 64 | public final class com/appstractive/jwt/JSONWebKeyHMAC : com/appstractive/jwt/JSONWebKey { 65 | public static final field Companion Lcom/appstractive/jwt/JSONWebKeyHMAC$Companion; 66 | public fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;)V 67 | public synthetic fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 68 | public final fun component1 ()Lcom/appstractive/jwt/Algorithm; 69 | public final fun component2 ()Ljava/lang/String; 70 | public final fun component3 ()Ljava/lang/String; 71 | public final fun copy (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;)Lcom/appstractive/jwt/JSONWebKeyHMAC; 72 | public static synthetic fun copy$default (Lcom/appstractive/jwt/JSONWebKeyHMAC;Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/appstractive/jwt/JSONWebKeyHMAC; 73 | public fun equals (Ljava/lang/Object;)Z 74 | public fun getAlg ()Lcom/appstractive/jwt/Algorithm; 75 | public final fun getK ()Ljava/lang/String; 76 | public fun getKid ()Ljava/lang/String; 77 | public fun hashCode ()I 78 | public fun toString ()Ljava/lang/String; 79 | } 80 | 81 | public synthetic class com/appstractive/jwt/JSONWebKeyHMAC$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 82 | public static final field INSTANCE Lcom/appstractive/jwt/JSONWebKeyHMAC$$serializer; 83 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 84 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/appstractive/jwt/JSONWebKeyHMAC; 85 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 86 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 87 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/appstractive/jwt/JSONWebKeyHMAC;)V 88 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 89 | public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; 90 | } 91 | 92 | public final class com/appstractive/jwt/JSONWebKeyHMAC$Companion { 93 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 94 | } 95 | 96 | public final class com/appstractive/jwt/JSONWebKeyKt { 97 | public static final fun getCurve (Lcom/appstractive/jwt/Curve;)Ljava/lang/String; 98 | } 99 | 100 | public final class com/appstractive/jwt/JSONWebKeyRSA : com/appstractive/jwt/JSONWebKey { 101 | public static final field Companion Lcom/appstractive/jwt/JSONWebKeyRSA$Companion; 102 | public fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V 103 | public synthetic fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 104 | public final fun component1 ()Lcom/appstractive/jwt/Algorithm; 105 | public final fun component10 ()Ljava/lang/String; 106 | public final fun component11 ()Ljava/lang/String; 107 | public final fun component2 ()Ljava/lang/String; 108 | public final fun component3 ()Ljava/lang/String; 109 | public final fun component4 ()Ljava/lang/String; 110 | public final fun component5 ()Ljava/lang/String; 111 | public final fun component6 ()Ljava/lang/String; 112 | public final fun component7 ()Ljava/lang/String; 113 | public final fun component8 ()Ljava/lang/String; 114 | public final fun component9 ()Ljava/lang/String; 115 | public final fun copy (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/appstractive/jwt/JSONWebKeyRSA; 116 | public static synthetic fun copy$default (Lcom/appstractive/jwt/JSONWebKeyRSA;Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/appstractive/jwt/JSONWebKeyRSA; 117 | public fun equals (Ljava/lang/Object;)Z 118 | public fun getAlg ()Lcom/appstractive/jwt/Algorithm; 119 | public final fun getD ()Ljava/lang/String; 120 | public final fun getDp ()Ljava/lang/String; 121 | public final fun getDq ()Ljava/lang/String; 122 | public final fun getE ()Ljava/lang/String; 123 | public fun getKid ()Ljava/lang/String; 124 | public final fun getN ()Ljava/lang/String; 125 | public final fun getP ()Ljava/lang/String; 126 | public final fun getQ ()Ljava/lang/String; 127 | public final fun getQi ()Ljava/lang/String; 128 | public final fun getUse ()Ljava/lang/String; 129 | public fun hashCode ()I 130 | public fun toString ()Ljava/lang/String; 131 | } 132 | 133 | public synthetic class com/appstractive/jwt/JSONWebKeyRSA$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 134 | public static final field INSTANCE Lcom/appstractive/jwt/JSONWebKeyRSA$$serializer; 135 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 136 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/appstractive/jwt/JSONWebKeyRSA; 137 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 138 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 139 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/appstractive/jwt/JSONWebKeyRSA;)V 140 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 141 | public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; 142 | } 143 | 144 | public final class com/appstractive/jwt/JSONWebKeyRSA$Companion { 145 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 146 | } 147 | 148 | public final class com/appstractive/jwt/JSONWebKeySet { 149 | public static final field Companion Lcom/appstractive/jwt/JSONWebKeySet$Companion; 150 | public fun (Ljava/util/List;)V 151 | public final fun component1 ()Ljava/util/List; 152 | public final fun copy (Ljava/util/List;)Lcom/appstractive/jwt/JSONWebKeySet; 153 | public static synthetic fun copy$default (Lcom/appstractive/jwt/JSONWebKeySet;Ljava/util/List;ILjava/lang/Object;)Lcom/appstractive/jwt/JSONWebKeySet; 154 | public fun equals (Ljava/lang/Object;)Z 155 | public final fun getKeys ()Ljava/util/List; 156 | public fun hashCode ()I 157 | public fun toString ()Ljava/lang/String; 158 | } 159 | 160 | public synthetic class com/appstractive/jwt/JSONWebKeySet$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 161 | public static final field INSTANCE Lcom/appstractive/jwt/JSONWebKeySet$$serializer; 162 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 163 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/appstractive/jwt/JSONWebKeySet; 164 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 165 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 166 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/appstractive/jwt/JSONWebKeySet;)V 167 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 168 | public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; 169 | } 170 | 171 | public final class com/appstractive/jwt/JSONWebKeySet$Companion { 172 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 173 | } 174 | 175 | public final class com/appstractive/jwt/JwksConfig { 176 | public fun ()V 177 | public final fun getCacheDuration-UwyO8pc ()J 178 | public final fun getClient ()Lio/ktor/client/HttpClient; 179 | public final fun getEndpoint ()Ljava/lang/String; 180 | public final fun setCacheDuration-LRDsOJo (J)V 181 | public final fun setClient (Lio/ktor/client/HttpClient;)V 182 | public final fun setEndpoint (Ljava/lang/String;)V 183 | } 184 | 185 | public final class com/appstractive/jwt/JwksKt { 186 | public static final fun jwks (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 187 | } 188 | 189 | -------------------------------------------------------------------------------- /jwt-jwks/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | alias(libs.plugins.android.library) 6 | alias(libs.plugins.kotlinx.serialization) 7 | alias(libs.plugins.kotlinx.binary.compatibility) 8 | } 9 | 10 | group = rootProject.group 11 | 12 | version = rootProject.version 13 | 14 | kotlin { 15 | androidTarget { 16 | publishAllLibraryVariants() 17 | compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } 18 | } 19 | 20 | jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } 21 | 22 | mingwX64() 23 | linuxArm64() 24 | linuxX64() 25 | 26 | js() 27 | 28 | listOf( 29 | iosX64(), 30 | iosArm64(), 31 | iosSimulatorArm64(), 32 | macosX64(), 33 | macosArm64(), 34 | tvosX64(), 35 | tvosArm64(), 36 | tvosSimulatorArm64(), 37 | watchosX64(), 38 | watchosArm64(), 39 | watchosSimulatorArm64(), 40 | ) 41 | .forEach { 42 | it.binaries.framework { 43 | baseName = "JWT-JWKS-KT" 44 | isStatic = true 45 | } 46 | } 47 | 48 | sourceSets { 49 | commonMain.dependencies { 50 | implementation(projects.jwtKt) 51 | implementation(projects.jwtRsaKt) 52 | implementation(projects.jwtEcdsaKt) 53 | 54 | implementation(libs.kotlin.coroutines) 55 | implementation(libs.kotlin.datetime) 56 | 57 | implementation(libs.ktor.serialization.json) 58 | implementation(libs.ktor.client.core) 59 | implementation(libs.ktor.client.serialization) 60 | implementation(libs.ktor.client.json) 61 | implementation(libs.ktor.client.contentnegotiation) 62 | } 63 | 64 | commonTest.dependencies { 65 | implementation(kotlin("test")) 66 | implementation(libs.test.kotlin.coroutines) 67 | implementation(libs.test.ktor.client.mock) 68 | } 69 | 70 | androidMain.dependencies { 71 | implementation(libs.crypto.jdk) 72 | implementation(libs.ktor.client.cio) 73 | } 74 | 75 | jvmMain.dependencies { 76 | implementation(libs.crypto.jdk) 77 | implementation(libs.ktor.client.cio) 78 | } 79 | 80 | appleMain.dependencies { 81 | implementation(libs.crypto.openssl3) 82 | implementation(libs.ktor.client.cio) 83 | } 84 | 85 | linuxMain.dependencies { 86 | implementation(libs.crypto.openssl3) 87 | implementation(libs.ktor.client.cio) 88 | } 89 | 90 | mingwMain.dependencies { 91 | implementation(libs.crypto.openssl3) 92 | implementation(libs.ktor.client.win) 93 | } 94 | 95 | jsMain.dependencies { 96 | implementation(libs.crypto.webcrypto) 97 | implementation(libs.ktor.client.js) 98 | } 99 | } 100 | } 101 | 102 | android { 103 | namespace = "com.appstractive.jwt.jwks" 104 | compileSdk = libs.versions.compileSdk.get().toInt() 105 | 106 | defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } 107 | sourceSets["main"].apply { manifest.srcFile("src/androidMain/AndroidManifest.xml") } 108 | compileOptions { 109 | sourceCompatibility = JavaVersion.VERSION_17 110 | targetCompatibility = JavaVersion.VERSION_17 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /jwt-jwks/src/commonMain/kotlin/com/appstractive/jwt/Common.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | @OptIn(kotlinx.serialization.ExperimentalSerializationApi::class) 6 | internal val json: Json = Json { 7 | isLenient = true 8 | explicitNulls = false 9 | encodeDefaults = true 10 | ignoreUnknownKeys = true 11 | } 12 | -------------------------------------------------------------------------------- /jwt-jwks/src/commonMain/kotlin/com/appstractive/jwt/JSONWebKey.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import dev.whyoleg.cryptography.algorithms.EC 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.json.JsonClassDiscriminator 8 | 9 | enum class Curve { 10 | @SerialName("P-256") 11 | P256, 12 | @SerialName("P-384") 13 | P384, 14 | @SerialName("P-521") 15 | P521, 16 | } 17 | 18 | val Curve.curve: EC.Curve 19 | get() = 20 | when (this) { 21 | Curve.P256 -> EC.Curve.P256 22 | Curve.P384 -> EC.Curve.P384 23 | Curve.P521 -> EC.Curve.P521 24 | } 25 | 26 | @OptIn(ExperimentalSerializationApi::class) 27 | @Serializable 28 | @JsonClassDiscriminator("kty") 29 | sealed interface JSONWebKey { 30 | val alg: Algorithm? 31 | val kid: String? 32 | } 33 | 34 | @Serializable 35 | @SerialName("RSA") 36 | data class JSONWebKeyRSA( 37 | override val alg: Algorithm? = null, 38 | override val kid: String, 39 | val use: String, 40 | val n: String, 41 | val e: String, 42 | val d: String? = null, 43 | val p: String? = null, 44 | val q: String? = null, 45 | val dp: String? = null, 46 | val dq: String? = null, 47 | val qi: String? = null, 48 | ) : JSONWebKey 49 | 50 | @Serializable 51 | @SerialName("EC") 52 | data class JSONWebKeyEC( 53 | override val alg: Algorithm? = null, 54 | override val kid: String, 55 | val crv: Curve, 56 | val x: String, 57 | val y: String, 58 | val d: String, 59 | ) : JSONWebKey 60 | 61 | @Serializable 62 | @SerialName("oct") 63 | data class JSONWebKeyHMAC( 64 | override val alg: Algorithm? = null, 65 | override val kid: String, 66 | val k: String, 67 | ) : JSONWebKey 68 | 69 | @Serializable 70 | data class JSONWebKeySet( 71 | val keys: List, 72 | ) 73 | -------------------------------------------------------------------------------- /jwt-jwks/src/commonMain/kotlin/com/appstractive/jwt/JSONWebKeyExt.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import dev.whyoleg.cryptography.algorithms.EC 4 | import dev.whyoleg.cryptography.algorithms.ECDSA 5 | import dev.whyoleg.cryptography.algorithms.HMAC 6 | import dev.whyoleg.cryptography.algorithms.RSA 7 | import dev.whyoleg.cryptography.algorithms.SHA256 8 | import dev.whyoleg.cryptography.operations.SignatureVerifier 9 | import kotlinx.serialization.encodeToString 10 | import kotlinx.serialization.json.Json 11 | 12 | fun JSONWebKeySet.getKey(kid: String?): JSONWebKey = 13 | when { 14 | kid == null && keys.size == 1 -> { 15 | keys.first() 16 | } 17 | 18 | kid != null -> { 19 | keys.first { it.kid == kid } 20 | } 21 | 22 | else -> throw IllegalArgumentException("No valid key found for JWT") 23 | } 24 | 25 | suspend fun JSONWebKey.getVerifier(serializer: Json = json): SignatureVerifier { 26 | val digest = alg?.digest ?: SHA256 27 | 28 | return when (this) { 29 | is JSONWebKeyEC -> 30 | ecdsa 31 | .publicKeyDecoder(curve = crv.curve) 32 | .decodeFromByteArray( 33 | format = EC.PublicKey.Format.JWK, 34 | bytes = serializer.encodeToString(this).encodeToByteArray(), // TODO NYI in Crypto 35 | ) 36 | .signatureVerifier(digest, ECDSA.SignatureFormat.RAW) 37 | 38 | is JSONWebKeyHMAC -> 39 | hmac 40 | .keyDecoder(digest) 41 | .decodeFromByteArray( 42 | format = HMAC.Key.Format.JWK, 43 | bytes = serializer.encodeToString(this).encodeToByteArray(), // TODO NYI in Crypto 44 | ) 45 | .signatureVerifier() 46 | 47 | is JSONWebKeyRSA -> 48 | when (alg) { 49 | Algorithm.PS256, 50 | Algorithm.PS384, 51 | Algorithm.PS512 -> 52 | pss.publicKeyDecoder(digest) 53 | .decodeFromByteArrayBlocking( 54 | format = RSA.PublicKey.Format.JWK, 55 | bytes = 56 | serializer.encodeToString(this).encodeToByteArray(), // TODO NYI in Crypto 57 | ) 58 | .signatureVerifier() 59 | 60 | Algorithm.RS256, 61 | Algorithm.RS384, 62 | Algorithm.RS512 -> 63 | pkcs1 64 | .publicKeyDecoder(digest) 65 | .decodeFromByteArrayBlocking( 66 | format = RSA.PublicKey.Format.JWK, 67 | bytes = 68 | serializer.encodeToString(this).encodeToByteArray(), // TODO NYI in Crypto 69 | ) 70 | .signatureVerifier() 71 | 72 | else -> throw IllegalArgumentException("Unknown algorithm $alg") 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /jwt-jwks/src/commonMain/kotlin/com/appstractive/jwt/Jwks.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import dev.whyoleg.cryptography.CryptographyProvider 4 | import dev.whyoleg.cryptography.CryptographyProviderApi 5 | import dev.whyoleg.cryptography.algorithms.ECDSA 6 | import dev.whyoleg.cryptography.algorithms.HMAC 7 | import dev.whyoleg.cryptography.algorithms.RSA 8 | import dev.whyoleg.cryptography.operations.SignatureVerifier 9 | import dev.whyoleg.cryptography.operations.VerifyFunction 10 | import io.ktor.client.* 11 | import io.ktor.client.call.* 12 | import io.ktor.client.plugins.contentnegotiation.* 13 | import io.ktor.client.request.* 14 | import io.ktor.http.* 15 | import io.ktor.serialization.kotlinx.json.* 16 | import kotlin.time.Duration 17 | import kotlin.time.Duration.Companion.hours 18 | import kotlinx.datetime.Clock 19 | import kotlinx.datetime.Instant 20 | 21 | private val provider by lazy { CryptographyProvider.Default } 22 | internal val pkcs1: RSA by lazy { 23 | provider.get(RSA.PKCS1) 24 | } 25 | internal val hmac by lazy { provider.get(HMAC) } 26 | internal val ecdsa: ECDSA by lazy { provider.get(ECDSA) } 27 | internal val pss: RSA.PSS by lazy { provider.get(RSA.PSS) } 28 | 29 | @OptIn(CryptographyProviderApi::class) 30 | internal class JwksVerifier( 31 | private val config: JwksConfig, 32 | ) : VerificationAlgorithm { 33 | 34 | private var keySet: JSONWebKeySet = JSONWebKeySet(emptyList()) 35 | private var lastUpdate: Instant = Clock.System.now() - config.cacheDuration 36 | 37 | private val keyVerifier: MutableMap = mutableMapOf() 38 | 39 | private val client = config.client.config { install(ContentNegotiation) { json(json) } } 40 | private val endpoint = checkNotNull(config.endpoint) { "Endpoint not configured" } 41 | 42 | override suspend fun verifier(jwt: JWT): SignatureVerifier { 43 | updateKeySet() 44 | 45 | val kid = jwt.header.kid 46 | val key: JSONWebKey = keySet.getKey(kid) 47 | 48 | val verifier: SignatureVerifier = keyVerifier.getOrPut(kid) { key.getVerifier() } 49 | 50 | return object : SignatureVerifier { 51 | override fun createVerifyFunction(): VerifyFunction { 52 | return verifier.createVerifyFunction() 53 | } 54 | } 55 | } 56 | 57 | private suspend fun updateKeySet() { 58 | if (Clock.System.now() - lastUpdate > config.cacheDuration) { 59 | val response = client.get(endpoint) 60 | 61 | if (response.status == HttpStatusCode.OK) { 62 | lastUpdate = Clock.System.now() 63 | keySet = response.body() 64 | } 65 | } 66 | } 67 | } 68 | 69 | class JwksConfig { 70 | var endpoint: String? = null 71 | var client: HttpClient = HttpClient() 72 | var cacheDuration: Duration = 24.hours 73 | } 74 | 75 | fun Verifier.jwks(configure: JwksConfig.() -> Unit) { 76 | val config = JwksConfig().apply(configure) 77 | val jwks = JwksVerifier(config = config) 78 | Algorithm.entries.forEach { 79 | algorithm( 80 | type = it, 81 | algorithm = jwks, 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /jwt-jwks/src/commonTest/kotlin/jwt/JwksVerifierTests.kt: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import com.appstractive.jwt.JWT 4 | import com.appstractive.jwt.from 5 | import com.appstractive.jwt.jwks 6 | import com.appstractive.jwt.jwtVerifier 7 | import com.appstractive.jwt.verify 8 | import io.ktor.client.HttpClient 9 | import io.ktor.client.engine.mock.MockEngine 10 | import io.ktor.client.engine.mock.respond 11 | import io.ktor.http.HttpHeaders 12 | import io.ktor.http.HttpStatusCode 13 | import io.ktor.http.headersOf 14 | import io.ktor.utils.io.ByteReadChannel 15 | import kotlin.test.Test 16 | import kotlin.test.assertTrue 17 | import kotlin.time.Duration.Companion.minutes 18 | import kotlinx.coroutines.test.runTest 19 | 20 | class JwksVerifierTests { 21 | 22 | private val mockEngine = MockEngine { request -> 23 | respond( 24 | content = ByteReadChannel(KEY_SET_RSA), 25 | status = HttpStatusCode.OK, 26 | headers = headersOf(HttpHeaders.ContentType, "application/json"), 27 | ) 28 | } 29 | 30 | private val mockClient = HttpClient(mockEngine) 31 | 32 | @Test 33 | fun testVerifyRSA() = runTest { 34 | val verifier = jwtVerifier { 35 | jwks { 36 | client = mockClient 37 | endpoint = "http://example.com/.well-known/jwt/jwks.json" 38 | cacheDuration = 1.minutes 39 | } 40 | } 41 | 42 | val jwt = JWT.from(RSA_JWT_VALID) 43 | 44 | assertTrue(jwt.verify(verifier)) 45 | } 46 | 47 | companion object { 48 | const val KEY_SET_RSA = 49 | "{\n" + 50 | " \"keys\": [\n" + 51 | " {\n" + 52 | " \"kty\": \"RSA\",\n" + 53 | " \"kid\": \"d-1723431926928\",\n" + 54 | " \"n\": \"qTLDwMJWj2upWCmnIRW9K6rfK_VI6PlNbF_P9tgWL5lwvksvphUa2lYGexbo7Sv0nU-ndQOEcGq9GELmg_H02BqwqRFZLmv9Gh3rgJWFUsx7KqD4Cy2IxJ55MYz_ZK0ucLhoX0Bur6tMkMcU2tySKBjm7ZupN141B4phOB0U6mhfswmKOdlEs5-sQnPz4akkcTw9RdK8-egGJqgUsTyT31NHGu8Szl-X87Z5vjP9Nd8jHfHJT4zDVSlI8O6LQFTPoZP-vs0GPvCAcypkNaHa7gjkNyCVRQ6tg0i2z24mINoczTC4jtR4UeqDPWqbb1xepHGWDgrhEpJqmVEq7Mh3wQ\",\n" + 55 | " \"e\": \"AQAB\",\n" + 56 | " \"alg\": \"RS256\",\n" + 57 | " \"use\": \"sig\"\n" + 58 | " },\n" + 59 | " {\n" + 60 | " \"kty\": \"RSA\",\n" + 61 | " \"kid\": \"s-dfffd02e-9a42-4430-a502-2d16253d7640\",\n" + 62 | " \"n\": \"guCueWspilZxxFfv3G60dZ8F6AEtDvM4CcrOJv1dkwYSXWWhBTAZCbq3GhIe1yPHsg2vnGaPP-QscBXIjZWs4_GwnohO1TENoi4Xehz3tsy6Dd-4upucaqpAvYNXqDRJ2STiv_JsISkQDMxjon5xFp61ipUreX1chz_HiIDirtr6YW_6HM_YyFOLj9rfr48l5rkxOR6s637787gkDrXikreFGEMOkk4ANLmojOzQBTfCeLOi2MaYsWs8xugZNdy3musrzWjjfJePytc4OvVM2FYoPhe0z9NuK5Q-p9QsqlvNGpNgjedz8HSIY0KUMtZTc9ojYUd7kMSIINAeSz2Z7Q\",\n" + 63 | " \"e\": \"AQAB\",\n" + 64 | " \"alg\": \"RS256\",\n" + 65 | " \"use\": \"sig\"\n" + 66 | " }\n" + 67 | " ]\n" + 68 | "}" 69 | 70 | const val RSA_JWT_VALID = 71 | "eyJraWQiOiJzLWRmZmZkMDJlLTlhNDItNDQzMC1hNTAyLTJkMTYyNTNkNzY0MCIsInR5cCI6IkpXVCIsInZlcnNpb24iOiI1IiwiYWxnIjoiUlMyNTYifQ.eyJpYXQiOjE3MjM3MjI0MTUsImV4cCI6MTcyMzcyNjAxNSwic3ViIjoiZmE0NDhhYWItZjA3MS00MTRhLTliNzItMjQ4ZTEyM2JiNWIyIiwidElkIjoicHVibGljIiwicnN1YiI6ImZhNDQ4YWFiLWYwNzEtNDE0YS05YjcyLTI0OGUxMjNiYjViMiIsInNlc3Npb25IYW5kbGUiOiJmNDJjYWI2Mi03NmNjLTRlZTUtOTkyYy05YTNhNzhlYmFjNjIiLCJyZWZyZXNoVG9rZW5IYXNoMSI6IjlkNDZkMThkNGNiZDEwZGEzOTAyMTk0NDU3ZWNmOWU4NDlkNjdhYjJkN2I1ZGIyZjgwYjc5MmRmNGQyZjhjMGMiLCJwYXJlbnRSZWZyZXNoVG9rZW5IYXNoMSI6bnVsbCwiYW50aUNzcmZUb2tlbiI6bnVsbCwiaXNzIjoiYXV0aC5hcHBzdHJhY3RpdmUuY2xvdWQiLCJhdWQiOiJhdXRoLmFwcHN0cmFjdGl2ZS5jbG91ZCIsImVtYWlsIjoidGVzdDNAdGVzdC5kZSIsInN0LWV2IjpmYWxzZSwic3QtbWZhIjp7ImMiOnsiZW1haWxwYXNzd29yZCI6MTcyMzcyMjQxNTQzN30sInYiOmZhbHNlfSwiaXNFeGFtcGxlIjp0cnVlfQ.IMbcjLsi3xISvGu230-TDt41UkPIXOL5pdZ_20VMkNmeGLeorRKw0qE3KE-hW-7fs4fhGZMuksQwxnhQAfAm1nrcYoxtlemYn2cpYkrGIqkU6-o6yMmV-eFr0F9VYsd54wlvSIb4eRzW2y0YD0o6BuASRf3odX1m1haPPpPP-UhyYMLOsyBvmHqQy06OxHxax3IAmO9lIrcDNUE7Ve_OGmWaR_Lh1-xMfhHdlVgHdf-GHgyhoKJ_t1KukhKm49m2GiTpDKGazhks8H7iDt5lXcjR3heTSHIWrbt92iwT5dWrBwrLKoIClBQrFxkE9UieJba-SGQm2ymK_tM-sH3Few" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /jwt-rsa/README.md: -------------------------------------------------------------------------------- 1 | # JWT Kotlin Multiplatform - RSA 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.appstractive/jwt-rsa-kt?label=Maven%20Central)](https://central.sonatype.com/artifact/com.appstractive/jwt-rsa-kt) 4 | 5 | ![badge][badge-android] 6 | ![badge][badge-apple] 7 | ![badge][badge-jvm] 8 | ![badge][badge-js] 9 | ![badge][badge-win] 10 | ![badge][badge-linux] 11 | 12 | Sign and verify JWTs using RSA algorithms. 13 | 14 | ## Supported Algorithms 15 | 16 | - RS256 17 | - RS384 18 | - RS512 19 | - PS256 20 | - PS384 21 | - PS512 22 | 23 | ## Usage 24 | 25 | ### Installation 26 | 27 | Gradle: 28 | 29 | ``` 30 | commonMain.dependencies { 31 | implementation("com.appstractive:jwt-kt:1.1.0") 32 | implementation("com.appstractive:jwt-rsa-kt:1.1.0") 33 | } 34 | ``` 35 | 36 | ### Sign JWT 37 | 38 | #### PKCS1 39 | 40 | ```kotlin 41 | val provider = CryptographyProvider.Default 42 | val pkcs1: RSA.PKCS1 = provider.get(RSA.PKCS1) 43 | 44 | val jwt: UnsignedJWT = jwt { 45 | claims { issuer = "example.com" } 46 | } 47 | val keys = pkcs1.keyPairGenerator(digest = SHA256).generateKey() 48 | val privateKey = keys.privateKey.encodeTo(RSA.PrivateKey.Format.PEM.Generic) 49 | 50 | val signedJWT = jwt.sign { 51 | // PKCS 1 52 | rs256 { pem(privateKey) } 53 | // or with different hashing 54 | // rs384 { pem(privateKey) } 55 | // rs512 { pem(privateKey) } 56 | } 57 | ``` 58 | 59 | #### PSS 60 | 61 | ```kotlin 62 | val provider = CryptographyProvider.Default 63 | val pss: RSA.PSS = provider.get(RSA.PSS) 64 | 65 | val jwt: UnsignedJWT = jwt { 66 | claims { issuer = "example.com" } 67 | } 68 | val keys = pss.keyPairGenerator(digest = SHA256).generateKey() 69 | val privateKey = keys.privateKey.encodeTo(RSA.PrivateKey.Format.PEM.Generic) 70 | 71 | val signedJWT = jwt.sign { 72 | // PSS 73 | ps256 { pem(privateKey) } 74 | // or with different hashing 75 | // ps384 { pem(privateKey) } 76 | // ps512 { pem(privateKey) } 77 | } 78 | ``` 79 | 80 | ### Verify JWT 81 | 82 | ```kotlin 83 | val provider = CryptographyProvider.Default 84 | val pkcs1: RSA.PSS = rovider.get(RSA.PKCS1) 85 | val pss: RSA.PSS = rovider.get(RSA.PSS) 86 | 87 | val jwt = JWT.from("eyJraWQiOiJzLWRmZmZkMDJlLTlhNDItNDQzMC1hNT...") 88 | val pkcs1Keys = pss.keyPairGenerator(digest = SHA256).generateKey() 89 | val pkcs1PublicKey = pkcs1Keys.publicKey.encodeTo(RSA.PublicKey.Format.PEM.Generic) 90 | val pssKeys = pss.keyPairGenerator(digest = SHA256).generateKey() 91 | val pssPublicKey = pssKeys.publicKey.encodeTo(RSA.PublicKey.Format.PEM.Generic) 92 | 93 | val isValid = jwt.verify { 94 | // PKCS 1 95 | rs256 { pem(pkcs1PublicKey) } 96 | // or with different hashing 97 | // rs384 { pem(publicKey) } 98 | // rs512 { pem(publicKey) } 99 | 100 | // PSS 101 | ps256 { pem(pssPublicKey) } 102 | // or with different hashing 103 | // ps384 { pem(publicKey) } 104 | // ps512 { pem(publicKey) } 105 | 106 | // verify issuer 107 | issuer("example.com") 108 | // verify audience 109 | audience("api.example.com") 110 | // verify expiration 111 | expiresAt() 112 | // verify not before time 113 | notBefore() 114 | } 115 | ``` 116 | 117 | ## License 118 | 119 | ``` 120 | Copyright 2024 Andreas Schulz. 121 | 122 | Licensed under the Apache License, Version 2.0 (the "License"); 123 | you may not use this file except in compliance with the License. 124 | You may obtain a copy of the License at 125 | 126 | http://www.apache.org/licenses/LICENSE-2.0 127 | 128 | Unless required by applicable law or agreed to in writing, software 129 | distributed under the License is distributed on an "AS IS" BASIS, 130 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 131 | See the License for the specific language governing permissions and 132 | limitations under the License. 133 | ``` 134 | 135 | [badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat 136 | 137 | [badge-apple]: http://img.shields.io/badge/platform-apple-111111.svg?style=flat 138 | 139 | [badge-jvm]: http://img.shields.io/badge/platform-jvm-CDCDCD.svg?style=flat 140 | 141 | [badge-js]: http://img.shields.io/badge/platform-js-f7df1e.svg?style=flat 142 | 143 | [badge-win]: http://img.shields.io/badge/platform-win-357EC7.svg?style=flat 144 | 145 | [badge-linux]: http://img.shields.io/badge/platform-linux-CDCDCD.svg?style=flat 146 | -------------------------------------------------------------------------------- /jwt-rsa/api/android/jwt-rsa-kt.api: -------------------------------------------------------------------------------- 1 | public final class com/appstractive/jwt/signatures/PKCS1Kt { 2 | public static final fun rs256 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 3 | public static final fun rs256 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 4 | public static final fun rs384 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 5 | public static final fun rs384 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 6 | public static final fun rs512 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 7 | public static final fun rs512 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 8 | } 9 | 10 | public final class com/appstractive/jwt/signatures/PSSKt { 11 | public static final fun ps256 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 12 | public static final fun ps256 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 13 | public static final fun ps384 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 14 | public static final fun ps384 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 15 | public static final fun ps512 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 16 | public static final fun ps512 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 17 | } 18 | 19 | public final class com/appstractive/jwt/signatures/RSASigningConfig { 20 | public fun ()V 21 | public final fun der (Ljava/lang/String;)V 22 | public final fun der ([B)V 23 | public final fun pem (Ljava/lang/String;)V 24 | public final fun pem ([B)V 25 | } 26 | 27 | public final class com/appstractive/jwt/signatures/RSAVerifierConfig { 28 | public fun ()V 29 | public final fun der (Ljava/lang/String;)V 30 | public final fun der ([B)V 31 | public final fun pem (Ljava/lang/String;)V 32 | public final fun pem ([B)V 33 | } 34 | 35 | -------------------------------------------------------------------------------- /jwt-rsa/api/jvm/jwt-rsa-kt.api: -------------------------------------------------------------------------------- 1 | public final class com/appstractive/jwt/signatures/PKCS1Kt { 2 | public static final fun rs256 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 3 | public static final fun rs256 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 4 | public static final fun rs384 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 5 | public static final fun rs384 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 6 | public static final fun rs512 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 7 | public static final fun rs512 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 8 | } 9 | 10 | public final class com/appstractive/jwt/signatures/PSSKt { 11 | public static final fun ps256 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 12 | public static final fun ps256 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 13 | public static final fun ps384 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 14 | public static final fun ps384 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 15 | public static final fun ps512 (Lcom/appstractive/jwt/Signer;Lkotlin/jvm/functions/Function1;)V 16 | public static final fun ps512 (Lcom/appstractive/jwt/Verifier;Lkotlin/jvm/functions/Function1;)V 17 | } 18 | 19 | public final class com/appstractive/jwt/signatures/RSASigningConfig { 20 | public fun ()V 21 | public final fun der (Ljava/lang/String;)V 22 | public final fun der ([B)V 23 | public final fun pem (Ljava/lang/String;)V 24 | public final fun pem ([B)V 25 | } 26 | 27 | public final class com/appstractive/jwt/signatures/RSAVerifierConfig { 28 | public fun ()V 29 | public final fun der (Ljava/lang/String;)V 30 | public final fun der ([B)V 31 | public final fun pem (Ljava/lang/String;)V 32 | public final fun pem ([B)V 33 | } 34 | 35 | -------------------------------------------------------------------------------- /jwt-rsa/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 3 | 4 | plugins { 5 | alias(libs.plugins.multiplatform) 6 | alias(libs.plugins.android.library) 7 | alias(libs.plugins.kotlinx.binary.compatibility) 8 | id("jwt.publication") 9 | } 10 | 11 | group = rootProject.group 12 | 13 | version = rootProject.version 14 | 15 | kotlin { 16 | androidTarget { 17 | publishAllLibraryVariants() 18 | compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } 19 | } 20 | 21 | jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } 22 | 23 | mingwX64() 24 | linuxArm64() 25 | linuxX64() 26 | 27 | js() 28 | @OptIn(ExperimentalWasmDsl::class) wasmJs() 29 | 30 | listOf( 31 | iosX64(), 32 | iosArm64(), 33 | iosSimulatorArm64(), 34 | macosX64(), 35 | macosArm64(), 36 | tvosX64(), 37 | tvosArm64(), 38 | tvosSimulatorArm64(), 39 | watchosX64(), 40 | watchosArm64(), 41 | watchosSimulatorArm64(), 42 | ) 43 | .forEach { 44 | it.binaries.framework { 45 | baseName = "JWT-RSA-KT" 46 | isStatic = true 47 | } 48 | } 49 | 50 | sourceSets { 51 | commonMain.dependencies { implementation(projects.jwtKt) } 52 | 53 | commonTest.dependencies { 54 | implementation(kotlin("test")) 55 | implementation(libs.test.kotlin.coroutines) 56 | } 57 | 58 | androidMain.dependencies { implementation(libs.crypto.jdk) } 59 | 60 | jvmMain.dependencies { implementation(libs.crypto.jdk) } 61 | 62 | appleMain.dependencies { implementation(libs.crypto.openssl3) } 63 | 64 | linuxMain.dependencies { implementation(libs.crypto.openssl3) } 65 | 66 | mingwMain.dependencies { implementation(libs.crypto.openssl3) } 67 | 68 | jsMain.dependencies { implementation(libs.crypto.webcrypto) } 69 | } 70 | } 71 | 72 | android { 73 | namespace = "com.appstractive.jwt.rsa" 74 | compileSdk = libs.versions.compileSdk.get().toInt() 75 | 76 | defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } 77 | sourceSets["main"].apply { manifest.srcFile("src/androidMain/AndroidManifest.xml") } 78 | compileOptions { 79 | sourceCompatibility = JavaVersion.VERSION_17 80 | targetCompatibility = JavaVersion.VERSION_17 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /jwt-rsa/src/commonMain/kotlin/com/appstractive/jwt/signatures/PKCS1.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.signatures 2 | 3 | import com.appstractive.jwt.Algorithm 4 | import com.appstractive.jwt.JWT 5 | import com.appstractive.jwt.Signer 6 | import com.appstractive.jwt.SigningAlgorithm 7 | import com.appstractive.jwt.VerificationAlgorithm 8 | import com.appstractive.jwt.Verifier 9 | import com.appstractive.jwt.digest 10 | import dev.whyoleg.cryptography.CryptographyAlgorithmId 11 | import dev.whyoleg.cryptography.CryptographyProvider 12 | import dev.whyoleg.cryptography.algorithms.Digest 13 | import dev.whyoleg.cryptography.algorithms.RSA 14 | import dev.whyoleg.cryptography.operations.SignatureGenerator 15 | import dev.whyoleg.cryptography.operations.SignatureVerifier 16 | 17 | private val provider by lazy { CryptographyProvider.Default } 18 | internal val pkcs1: RSA.PKCS1 by lazy { 19 | provider.get(RSA.PKCS1) 20 | } 21 | 22 | fun Signer.rs256(configure: RSASigningConfig.() -> Unit) { 23 | val config = RSASigningConfig().apply(configure) 24 | algorithm( 25 | algorithm = PKCS1Signer(config = config), 26 | type = Algorithm.RS256, 27 | ) 28 | } 29 | 30 | fun Signer.rs384(configure: RSASigningConfig.() -> Unit) { 31 | val config = RSASigningConfig().apply(configure) 32 | algorithm( 33 | algorithm = PKCS1Signer(config = config), 34 | type = Algorithm.RS384, 35 | ) 36 | } 37 | 38 | fun Signer.rs512(configure: RSASigningConfig.() -> Unit) { 39 | val config = RSASigningConfig().apply(configure) 40 | algorithm( 41 | algorithm = PKCS1Signer(config = config), 42 | type = Algorithm.RS512, 43 | ) 44 | } 45 | 46 | fun Verifier.rs256(configure: RSAVerifierConfig.() -> Unit) { 47 | val config = RSAVerifierConfig().apply(configure) 48 | algorithm( 49 | type = Algorithm.RS256, 50 | algorithm = PKCS1Verifier(config = config), 51 | ) 52 | } 53 | 54 | fun Verifier.rs384(configure: RSAVerifierConfig.() -> Unit) { 55 | val config = RSAVerifierConfig().apply(configure) 56 | algorithm( 57 | type = Algorithm.RS384, 58 | algorithm = PKCS1Verifier(config = config), 59 | ) 60 | } 61 | 62 | fun Verifier.rs512(configure: RSAVerifierConfig.() -> Unit) { 63 | val config = RSAVerifierConfig().apply(configure) 64 | algorithm( 65 | type = Algorithm.RS512, 66 | algorithm = PKCS1Verifier(config = config), 67 | ) 68 | } 69 | 70 | internal class PKCS1Signer( 71 | private val config: RSASigningConfig, 72 | ) : SigningAlgorithm { 73 | override suspend fun generator(digest: CryptographyAlgorithmId): SignatureGenerator { 74 | return pkcs1 75 | .privateKeyDecoder(digest) 76 | .decodeFromByteArray(checkNotNull(config.format), checkNotNull(config.privateKey)) 77 | .signatureGenerator() 78 | } 79 | } 80 | 81 | internal class PKCS1Verifier( 82 | private val config: RSAVerifierConfig, 83 | ) : VerificationAlgorithm { 84 | override suspend fun verifier(jwt: JWT): SignatureVerifier { 85 | return pkcs1 86 | .publicKeyDecoder(digest = jwt.header.alg.digest) 87 | .decodeFromByteArray(checkNotNull(config.format), checkNotNull(config.publicKey)) 88 | .signatureVerifier() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /jwt-rsa/src/commonMain/kotlin/com/appstractive/jwt/signatures/PSS.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.signatures 2 | 3 | import com.appstractive.jwt.Algorithm 4 | import com.appstractive.jwt.JWT 5 | import com.appstractive.jwt.Signer 6 | import com.appstractive.jwt.SigningAlgorithm 7 | import com.appstractive.jwt.VerificationAlgorithm 8 | import com.appstractive.jwt.Verifier 9 | import com.appstractive.jwt.digest 10 | import dev.whyoleg.cryptography.CryptographyAlgorithmId 11 | import dev.whyoleg.cryptography.CryptographyProvider 12 | import dev.whyoleg.cryptography.algorithms.Digest 13 | import dev.whyoleg.cryptography.algorithms.RSA 14 | import dev.whyoleg.cryptography.operations.SignatureGenerator 15 | import dev.whyoleg.cryptography.operations.SignatureVerifier 16 | 17 | private val provider by lazy { CryptographyProvider.Default } 18 | internal val pss: RSA.PSS by lazy { provider.get(RSA.PSS) } 19 | 20 | fun Signer.ps256(configure: RSASigningConfig.() -> Unit) { 21 | val config = RSASigningConfig().apply(configure) 22 | algorithm( 23 | algorithm = PSSSigner(config = config), 24 | type = Algorithm.PS256, 25 | ) 26 | } 27 | 28 | fun Signer.ps384(configure: RSASigningConfig.() -> Unit) { 29 | val config = RSASigningConfig().apply(configure) 30 | algorithm( 31 | algorithm = PSSSigner(config = config), 32 | type = Algorithm.PS384, 33 | ) 34 | } 35 | 36 | fun Signer.ps512(configure: RSASigningConfig.() -> Unit) { 37 | val config = RSASigningConfig().apply(configure) 38 | algorithm( 39 | algorithm = PSSSigner(config = config), 40 | type = Algorithm.PS512, 41 | ) 42 | } 43 | 44 | fun Verifier.ps256(configure: RSAVerifierConfig.() -> Unit) { 45 | val config = RSAVerifierConfig().apply(configure) 46 | algorithm( 47 | type = Algorithm.PS256, 48 | algorithm = PSSVerifier(config = config), 49 | ) 50 | } 51 | 52 | fun Verifier.ps384(configure: RSAVerifierConfig.() -> Unit) { 53 | val config = RSAVerifierConfig().apply(configure) 54 | algorithm( 55 | type = Algorithm.PS384, 56 | algorithm = PSSVerifier(config = config), 57 | ) 58 | } 59 | 60 | fun Verifier.ps512(configure: RSAVerifierConfig.() -> Unit) { 61 | val config = RSAVerifierConfig().apply(configure) 62 | algorithm( 63 | type = Algorithm.PS512, 64 | algorithm = PSSVerifier(config = config), 65 | ) 66 | } 67 | 68 | internal class PSSSigner( 69 | private val config: RSASigningConfig, 70 | ) : SigningAlgorithm { 71 | override suspend fun generator(digest: CryptographyAlgorithmId): SignatureGenerator { 72 | return pss.privateKeyDecoder(digest) 73 | .decodeFromByteArray(checkNotNull(config.format), checkNotNull(config.privateKey)) 74 | .signatureGenerator() 75 | } 76 | } 77 | 78 | internal class PSSVerifier( 79 | private val config: RSAVerifierConfig, 80 | ) : VerificationAlgorithm { 81 | override suspend fun verifier(jwt: JWT): SignatureVerifier { 82 | return pss.publicKeyDecoder(digest = jwt.header.alg.digest) 83 | .decodeFromByteArray(checkNotNull(config.format), checkNotNull(config.publicKey)) 84 | .signatureVerifier() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /jwt-rsa/src/commonMain/kotlin/com/appstractive/jwt/signatures/RSA.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.signatures 2 | 3 | import dev.whyoleg.cryptography.algorithms.RSA 4 | 5 | class RSASigningConfig { 6 | internal var privateKey: ByteArray? = null 7 | internal var format: RSA.PrivateKey.Format? = null 8 | 9 | fun pem(key: ByteArray) { 10 | privateKey = key 11 | format = RSA.PrivateKey.Format.PEM.Generic 12 | } 13 | 14 | fun pem(key: String) { 15 | privateKey = key.encodeToByteArray() 16 | format = RSA.PrivateKey.Format.PEM.Generic 17 | } 18 | 19 | fun der(key: ByteArray) { 20 | privateKey = key 21 | format = RSA.PrivateKey.Format.DER.Generic 22 | } 23 | 24 | fun der(key: String) { 25 | privateKey = key.encodeToByteArray() 26 | format = RSA.PrivateKey.Format.DER.Generic 27 | } 28 | } 29 | 30 | class RSAVerifierConfig { 31 | internal var publicKey: ByteArray? = null 32 | internal var format: RSA.PublicKey.Format? = null 33 | 34 | fun pem(key: ByteArray) { 35 | publicKey = key 36 | format = RSA.PublicKey.Format.PEM 37 | } 38 | 39 | fun pem(key: String) { 40 | publicKey = key.encodeToByteArray() 41 | format = RSA.PublicKey.Format.PEM 42 | } 43 | 44 | fun der(key: ByteArray) { 45 | publicKey = key 46 | format = RSA.PublicKey.Format.DER 47 | } 48 | 49 | fun der(key: String) { 50 | publicKey = key.encodeToByteArray() 51 | format = RSA.PublicKey.Format.DER 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /jwt-rsa/src/commonTest/kotlin/com/appstractive/jwt/signatures/JwtVerifierTests.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.signatures 2 | 3 | import com.appstractive.jwt.Algorithm 4 | import com.appstractive.jwt.JWT 5 | import com.appstractive.jwt.Signer 6 | import com.appstractive.jwt.Verifier 7 | import com.appstractive.jwt.jwt 8 | import com.appstractive.jwt.sign 9 | import com.appstractive.jwt.verify 10 | import dev.whyoleg.cryptography.CryptographyAlgorithmId 11 | import dev.whyoleg.cryptography.algorithms.Digest 12 | import dev.whyoleg.cryptography.algorithms.RSA 13 | import dev.whyoleg.cryptography.algorithms.SHA256 14 | import dev.whyoleg.cryptography.algorithms.SHA384 15 | import dev.whyoleg.cryptography.algorithms.SHA512 16 | import kotlin.test.Test 17 | import kotlin.test.assertEquals 18 | import kotlin.test.assertFalse 19 | import kotlin.test.assertTrue 20 | import kotlinx.coroutines.test.runTest 21 | 22 | class JwtVerifierTests { 23 | @Test 24 | fun createJwtRS256() = runTest { 25 | val jwt = 26 | createJwtRS( 27 | SHA256, 28 | builder = { rs256 { pem(it) } }, 29 | verifier = { rs256 { pem(it) } }, 30 | ) 31 | 32 | assertEquals(Algorithm.RS256, jwt.header.alg) 33 | } 34 | 35 | @Test 36 | fun createJwtR384() = runTest { 37 | val jwt = 38 | createJwtRS( 39 | SHA384, 40 | builder = { rs384 { pem(it) } }, 41 | verifier = { rs384 { pem(it) } }, 42 | ) 43 | 44 | assertEquals(Algorithm.RS384, jwt.header.alg) 45 | } 46 | 47 | @Test 48 | fun createJwtR512() = runTest { 49 | val jwt = 50 | createJwtRS( 51 | SHA512, 52 | builder = { rs512 { pem(it) } }, 53 | verifier = { rs512 { pem(it) } }, 54 | ) 55 | 56 | assertEquals(Algorithm.RS512, jwt.header.alg) 57 | } 58 | 59 | @Test 60 | fun createJwtPS256() = runTest { 61 | val jwt = 62 | createJwtPS( 63 | SHA256, 64 | builder = { ps256 { pem(it) } }, 65 | verifier = { ps256 { pem(it) } }, 66 | ) 67 | assertEquals(Algorithm.PS256, jwt.header.alg) 68 | } 69 | 70 | @Test 71 | fun createJwtP384() = runTest { 72 | val jwt = 73 | createJwtPS( 74 | SHA384, 75 | builder = { ps384 { pem(it) } }, 76 | verifier = { ps384 { pem(it) } }, 77 | ) 78 | 79 | assertEquals(Algorithm.PS384, jwt.header.alg) 80 | } 81 | 82 | @Test 83 | fun createJwtP512() = runTest { 84 | val jwt = 85 | createJwtPS( 86 | SHA512, 87 | builder = { ps512 { pem(it) } }, 88 | verifier = { ps512 { pem(it) } }, 89 | ) 90 | 91 | assertEquals(Algorithm.PS512, jwt.header.alg) 92 | } 93 | 94 | private suspend fun createJwtRS( 95 | digest: CryptographyAlgorithmId, 96 | builder: Signer.(ByteArray) -> Unit, 97 | verifier: Verifier.(ByteArray) -> Unit, 98 | ): JWT { 99 | val unsignedJwt = jwt { claims { issuer = "someone" } } 100 | val keys = pkcs1.keyPairGenerator(digest = digest).generateKey() 101 | val privateKey = keys.privateKey.encodeToByteArray(RSA.PrivateKey.Format.PEM.Generic) 102 | val publicKey = keys.publicKey.encodeToByteArray(RSA.PublicKey.Format.PEM.Generic) 103 | println(privateKey.decodeToString()) 104 | println(publicKey.decodeToString()) 105 | 106 | val signedJwt = unsignedJwt.sign { builder(privateKey) } 107 | 108 | println(signedJwt.toString()) 109 | 110 | assertTrue(signedJwt.verify { verifier(publicKey) }) 111 | 112 | return signedJwt 113 | } 114 | 115 | private suspend fun createJwtPS( 116 | digest: CryptographyAlgorithmId, 117 | builder: Signer.(ByteArray) -> Unit, 118 | verifier: Verifier.(ByteArray) -> Unit, 119 | ): JWT { 120 | val unsignedJwt = jwt { claims { issuer = "someone" } } 121 | val keys = pss.keyPairGenerator(digest = digest).generateKey() 122 | val privateKey = keys.privateKey.encodeToByteArray(RSA.PrivateKey.Format.PEM.Generic) 123 | val publicKey = keys.publicKey.encodeToByteArray(RSA.PublicKey.Format.PEM.Generic) 124 | println(privateKey.decodeToString()) 125 | println(publicKey.decodeToString()) 126 | 127 | val signedJwt = unsignedJwt.sign { builder(privateKey) } 128 | 129 | println(signedJwt.toString()) 130 | 131 | assertTrue(signedJwt.verify { verifier(publicKey) }) 132 | 133 | val wrongKey = 134 | pss.keyPairGenerator(digest = digest) 135 | .generateKey() 136 | .publicKey 137 | .encodeToByteArray(RSA.PublicKey.Format.PEM.Generic) 138 | assertFalse(signedJwt.verify { verifier(wrongKey) }) 139 | 140 | return signedJwt 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /jwt/api/jvm/jwt-kt.api: -------------------------------------------------------------------------------- 1 | public final class com/appstractive/jwt/Algorithm : java/lang/Enum { 2 | public static final field ES256 Lcom/appstractive/jwt/Algorithm; 3 | public static final field ES384 Lcom/appstractive/jwt/Algorithm; 4 | public static final field ES512 Lcom/appstractive/jwt/Algorithm; 5 | public static final field HS256 Lcom/appstractive/jwt/Algorithm; 6 | public static final field HS384 Lcom/appstractive/jwt/Algorithm; 7 | public static final field HS512 Lcom/appstractive/jwt/Algorithm; 8 | public static final field PS256 Lcom/appstractive/jwt/Algorithm; 9 | public static final field PS384 Lcom/appstractive/jwt/Algorithm; 10 | public static final field PS512 Lcom/appstractive/jwt/Algorithm; 11 | public static final field RS256 Lcom/appstractive/jwt/Algorithm; 12 | public static final field RS384 Lcom/appstractive/jwt/Algorithm; 13 | public static final field RS512 Lcom/appstractive/jwt/Algorithm; 14 | public static fun getEntries ()Lkotlin/enums/EnumEntries; 15 | public static fun valueOf (Ljava/lang/String;)Lcom/appstractive/jwt/Algorithm; 16 | public static fun values ()[Lcom/appstractive/jwt/Algorithm; 17 | } 18 | 19 | public final class com/appstractive/jwt/ClaimsBuilder { 20 | public fun ()V 21 | public final fun arrayClaim (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V 22 | public final fun claim (Ljava/lang/String;D)V 23 | public final fun claim (Ljava/lang/String;J)V 24 | public final fun claim (Ljava/lang/String;Ljava/lang/String;)V 25 | public final fun claim (Ljava/lang/String;Ljava/lang/Void;)V 26 | public final fun claim (Ljava/lang/String;Lkotlinx/serialization/json/JsonArray;)V 27 | public final fun claim (Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)V 28 | public final fun claim (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V 29 | public final fun claim (Ljava/lang/String;Z)V 30 | public final fun expires (Lkotlinx/datetime/Instant;)V 31 | public static synthetic fun expires$default (Lcom/appstractive/jwt/ClaimsBuilder;Lkotlinx/datetime/Instant;ILjava/lang/Object;)V 32 | public final fun getAudience ()Ljava/lang/String; 33 | public final fun getExpiresAt ()Lkotlinx/datetime/Instant; 34 | public final fun getId ()Ljava/lang/String; 35 | public final fun getIssuedAt ()Lkotlinx/datetime/Instant; 36 | public final fun getIssuer ()Ljava/lang/String; 37 | public final fun getNotBefore ()Lkotlinx/datetime/Instant; 38 | public final fun getSubject ()Ljava/lang/String; 39 | public final fun issuedAt (Lkotlinx/datetime/Instant;)V 40 | public static synthetic fun issuedAt$default (Lcom/appstractive/jwt/ClaimsBuilder;Lkotlinx/datetime/Instant;ILjava/lang/Object;)V 41 | public final fun notBefore (Lkotlinx/datetime/Instant;)V 42 | public static synthetic fun notBefore$default (Lcom/appstractive/jwt/ClaimsBuilder;Lkotlinx/datetime/Instant;ILjava/lang/Object;)V 43 | public final fun objectClaim (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V 44 | public final fun setAudience (Ljava/lang/String;)V 45 | public final fun setExpiresAt (Lkotlinx/datetime/Instant;)V 46 | public final fun setId (Ljava/lang/String;)V 47 | public final fun setIssuedAt (Lkotlinx/datetime/Instant;)V 48 | public final fun setIssuer (Ljava/lang/String;)V 49 | public final fun setNotBefore (Lkotlinx/datetime/Instant;)V 50 | public final fun setSubject (Ljava/lang/String;)V 51 | } 52 | 53 | public final class com/appstractive/jwt/ClaimsKt { 54 | public static final fun getAudience (Lcom/appstractive/jwt/JWT;)Ljava/lang/String; 55 | public static final fun getAudience (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String; 56 | public static final fun getExpiresAt (Lcom/appstractive/jwt/JWT;)Lkotlinx/datetime/Instant; 57 | public static final fun getExpiresAt (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/datetime/Instant; 58 | public static final fun getId (Lcom/appstractive/jwt/JWT;)Ljava/lang/String; 59 | public static final fun getId (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String; 60 | public static final fun getIssuedAt (Lcom/appstractive/jwt/JWT;)Lkotlinx/datetime/Instant; 61 | public static final fun getIssuedAt (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/datetime/Instant; 62 | public static final fun getIssuer (Lcom/appstractive/jwt/JWT;)Ljava/lang/String; 63 | public static final fun getIssuer (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String; 64 | public static final fun getNotBefore (Lcom/appstractive/jwt/JWT;)Lkotlinx/datetime/Instant; 65 | public static final fun getNotBefore (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/datetime/Instant; 66 | public static final fun getSubject (Lcom/appstractive/jwt/JWT;)Ljava/lang/String; 67 | public static final fun getSubject (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String; 68 | } 69 | 70 | public final class com/appstractive/jwt/Header { 71 | public static final field Companion Lcom/appstractive/jwt/Header$Companion; 72 | public fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;)V 73 | public synthetic fun (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 74 | public final fun component1 ()Lcom/appstractive/jwt/Algorithm; 75 | public final fun component2 ()Ljava/lang/String; 76 | public final fun component3 ()Ljava/lang/String; 77 | public final fun copy (Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;)Lcom/appstractive/jwt/Header; 78 | public static synthetic fun copy$default (Lcom/appstractive/jwt/Header;Lcom/appstractive/jwt/Algorithm;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/appstractive/jwt/Header; 79 | public fun equals (Ljava/lang/Object;)Z 80 | public final fun getAlg ()Lcom/appstractive/jwt/Algorithm; 81 | public final fun getKid ()Ljava/lang/String; 82 | public final fun getTyp ()Ljava/lang/String; 83 | public fun hashCode ()I 84 | public fun toString ()Ljava/lang/String; 85 | } 86 | 87 | public synthetic class com/appstractive/jwt/Header$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 88 | public static final field INSTANCE Lcom/appstractive/jwt/Header$$serializer; 89 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 90 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/appstractive/jwt/Header; 91 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 92 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 93 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/appstractive/jwt/Header;)V 94 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 95 | } 96 | 97 | public final class com/appstractive/jwt/Header$Companion { 98 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 99 | } 100 | 101 | public final class com/appstractive/jwt/HeaderBuilder { 102 | public fun ()V 103 | public final fun getTyp ()Ljava/lang/String; 104 | public final fun setTyp (Ljava/lang/String;)V 105 | } 106 | 107 | public final class com/appstractive/jwt/JWT { 108 | public static final field Companion Lcom/appstractive/jwt/JWT$Companion; 109 | public fun (Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;[B)V 110 | public final fun component1 ()Lcom/appstractive/jwt/Header; 111 | public final fun component2 ()Lkotlinx/serialization/json/JsonObject; 112 | public final fun component3 ()Ljava/lang/String; 113 | public final fun component4 ()[B 114 | public final fun copy (Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;[B)Lcom/appstractive/jwt/JWT; 115 | public static synthetic fun copy$default (Lcom/appstractive/jwt/JWT;Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;[BILjava/lang/Object;)Lcom/appstractive/jwt/JWT; 116 | public fun equals (Ljava/lang/Object;)Z 117 | public final fun getClaims ()Lkotlinx/serialization/json/JsonObject; 118 | public final fun getHeader ()Lcom/appstractive/jwt/Header; 119 | public final fun getSignature ()[B 120 | public final fun getSignedData ()Ljava/lang/String; 121 | public fun hashCode ()I 122 | public fun toString ()Ljava/lang/String; 123 | } 124 | 125 | public final class com/appstractive/jwt/JWT$Companion { 126 | } 127 | 128 | public final class com/appstractive/jwt/JWTKt { 129 | public static final fun jwt (Lkotlin/jvm/functions/Function1;)Lcom/appstractive/jwt/UnsignedJWT; 130 | } 131 | 132 | public final class com/appstractive/jwt/JwtBuilder { 133 | public fun ()V 134 | public final fun claims (Lkotlin/jvm/functions/Function1;)V 135 | public final fun header (Lkotlin/jvm/functions/Function1;)V 136 | } 137 | 138 | public abstract interface annotation class com/appstractive/jwt/JwtDsl : java/lang/annotation/Annotation { 139 | } 140 | 141 | public final class com/appstractive/jwt/ParserKt { 142 | public static final fun from (Lcom/appstractive/jwt/JWT$Companion;Ljava/lang/String;)Lcom/appstractive/jwt/JWT; 143 | } 144 | 145 | public final class com/appstractive/jwt/Signer { 146 | public fun ()V 147 | public final fun algorithm (Lcom/appstractive/jwt/SigningAlgorithm;Lcom/appstractive/jwt/Algorithm;)V 148 | } 149 | 150 | public abstract interface class com/appstractive/jwt/SigningAlgorithm { 151 | public abstract fun generator (Ldev/whyoleg/cryptography/CryptographyAlgorithmId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 152 | public abstract fun sign (Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 153 | } 154 | 155 | public final class com/appstractive/jwt/SigningAlgorithm$DefaultImpls { 156 | public static fun sign (Lcom/appstractive/jwt/SigningAlgorithm;Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 157 | } 158 | 159 | public final class com/appstractive/jwt/SigningKt { 160 | public static final fun getDigest (Lcom/appstractive/jwt/Algorithm;)Ldev/whyoleg/cryptography/CryptographyAlgorithmId; 161 | public static final fun sign (Lcom/appstractive/jwt/UnsignedJWT;Ljava/lang/String;Lcom/appstractive/jwt/Signer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 162 | public static final fun sign (Lcom/appstractive/jwt/UnsignedJWT;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 163 | public static synthetic fun sign$default (Lcom/appstractive/jwt/UnsignedJWT;Ljava/lang/String;Lcom/appstractive/jwt/Signer;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; 164 | public static synthetic fun sign$default (Lcom/appstractive/jwt/UnsignedJWT;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; 165 | public static final fun signer (Lkotlin/jvm/functions/Function1;)Lcom/appstractive/jwt/Signer; 166 | } 167 | 168 | public final class com/appstractive/jwt/UnsignedJWT { 169 | public fun (Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;)V 170 | public final fun component1 ()Lcom/appstractive/jwt/Header; 171 | public final fun component2 ()Lkotlinx/serialization/json/JsonObject; 172 | public final fun copy (Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;)Lcom/appstractive/jwt/UnsignedJWT; 173 | public static synthetic fun copy$default (Lcom/appstractive/jwt/UnsignedJWT;Lcom/appstractive/jwt/Header;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lcom/appstractive/jwt/UnsignedJWT; 174 | public fun equals (Ljava/lang/Object;)Z 175 | public final fun getClaims ()Lkotlinx/serialization/json/JsonObject; 176 | public final fun getHeader ()Lcom/appstractive/jwt/Header; 177 | public fun hashCode ()I 178 | public fun toString ()Ljava/lang/String; 179 | } 180 | 181 | public abstract interface class com/appstractive/jwt/VerificationAlgorithm { 182 | public abstract fun verifier (Lcom/appstractive/jwt/JWT;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 183 | public abstract fun verify (Lcom/appstractive/jwt/JWT;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 184 | } 185 | 186 | public final class com/appstractive/jwt/VerificationAlgorithm$DefaultImpls { 187 | public static fun verify (Lcom/appstractive/jwt/VerificationAlgorithm;Lcom/appstractive/jwt/JWT;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 188 | } 189 | 190 | public final class com/appstractive/jwt/VerificationKt { 191 | public static final fun jwtVerifier (Lkotlin/jvm/functions/Function1;)Lcom/appstractive/jwt/Verifier; 192 | public static final fun verify (Lcom/appstractive/jwt/JWT;Lcom/appstractive/jwt/Verifier;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 193 | public static final fun verify (Lcom/appstractive/jwt/JWT;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 194 | } 195 | 196 | public final class com/appstractive/jwt/Verifier { 197 | public fun ()V 198 | public final fun algorithm (Lcom/appstractive/jwt/Algorithm;Lcom/appstractive/jwt/VerificationAlgorithm;)V 199 | public final fun audience ([Ljava/lang/String;)V 200 | public final fun expiresAt (Lkotlinx/datetime/Instant;)V 201 | public static synthetic fun expiresAt$default (Lcom/appstractive/jwt/Verifier;Lkotlinx/datetime/Instant;ILjava/lang/Object;)V 202 | public final fun issuer ([Ljava/lang/String;)V 203 | public final fun notBefore (Lkotlinx/datetime/Instant;)V 204 | public static synthetic fun notBefore$default (Lcom/appstractive/jwt/Verifier;Lkotlinx/datetime/Instant;ILjava/lang/Object;)V 205 | } 206 | 207 | public final class com/appstractive/jwt/utils/Base64UtilsKt { 208 | public static final fun urlDecodedBytes (Ljava/lang/String;)[B 209 | public static final fun urlDecodedString (Ljava/lang/String;)Ljava/lang/String; 210 | public static final fun urlEncoded (Lcom/appstractive/jwt/Header;)Ljava/lang/String; 211 | public static final fun urlEncoded (Lcom/appstractive/jwt/UnsignedJWT;)Ljava/lang/String; 212 | public static final fun urlEncoded (Ljava/lang/String;)Ljava/lang/String; 213 | public static final fun urlEncoded (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String; 214 | public static final fun urlEncoded ([B)Ljava/lang/String; 215 | } 216 | 217 | public final class com/appstractive/jwt/utils/ClaimsExtKt { 218 | public static final fun instantOrNull (Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lkotlinx/datetime/Instant; 219 | } 220 | 221 | -------------------------------------------------------------------------------- /jwt/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | 4 | plugins { 5 | alias(libs.plugins.multiplatform) 6 | alias(libs.plugins.android.library) 7 | alias(libs.plugins.kotlinx.serialization) 8 | alias(libs.plugins.kotlinx.binary.compatibility) 9 | id("jwt.publication") 10 | } 11 | 12 | group = rootProject.group 13 | 14 | version = rootProject.version 15 | 16 | kotlin { 17 | androidTarget { 18 | publishAllLibraryVariants() 19 | compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } 20 | } 21 | 22 | jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } 23 | 24 | mingwX64() 25 | linuxArm64() 26 | linuxX64() 27 | 28 | js() 29 | @OptIn(ExperimentalWasmDsl::class) wasmJs() 30 | 31 | listOf( 32 | iosX64(), 33 | iosArm64(), 34 | iosSimulatorArm64(), 35 | macosX64(), 36 | macosArm64(), 37 | tvosX64(), 38 | tvosArm64(), 39 | tvosSimulatorArm64(), 40 | watchosX64(), 41 | watchosArm64(), 42 | watchosSimulatorArm64(), 43 | ) 44 | .forEach { 45 | it.binaries.framework { 46 | baseName = "JWT-KT" 47 | isStatic = true 48 | } 49 | } 50 | 51 | sourceSets { 52 | commonMain.dependencies { 53 | implementation(libs.kotlin.serialization) 54 | implementation(libs.kotlin.serialization.json) 55 | implementation(libs.kotlin.datetime) 56 | api(libs.crypto) 57 | } 58 | 59 | commonTest.dependencies { 60 | implementation(kotlin("test")) 61 | implementation(libs.test.kotlin.coroutines) 62 | } 63 | 64 | androidMain.dependencies { implementation(libs.crypto.jdk) } 65 | 66 | jvmMain.dependencies { implementation(libs.crypto.jdk) } 67 | 68 | appleMain.dependencies { implementation(libs.crypto.openssl3) } 69 | 70 | linuxMain.dependencies { implementation(libs.crypto.openssl3) } 71 | 72 | mingwMain.dependencies { implementation(libs.crypto.openssl3) } 73 | 74 | jsMain.dependencies { implementation(libs.crypto.webcrypto) } 75 | } 76 | } 77 | 78 | android { 79 | namespace = group.toString() 80 | compileSdk = libs.versions.compileSdk.get().toInt() 81 | 82 | defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } 83 | sourceSets["main"].apply { manifest.srcFile("src/androidMain/AndroidManifest.xml") } 84 | compileOptions { 85 | sourceCompatibility = JavaVersion.VERSION_17 86 | targetCompatibility = JavaVersion.VERSION_17 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /jwt/src/commonMain/kotlin/com/appstractive/jwt/Claims.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import com.appstractive.jwt.utils.instantOrNull 4 | import kotlin.time.Duration.Companion.minutes 5 | import kotlinx.datetime.Clock 6 | import kotlinx.datetime.Instant 7 | import kotlinx.serialization.ExperimentalSerializationApi 8 | import kotlinx.serialization.json.JsonArray 9 | import kotlinx.serialization.json.JsonArrayBuilder 10 | import kotlinx.serialization.json.JsonElement 11 | import kotlinx.serialization.json.JsonObject 12 | import kotlinx.serialization.json.JsonObjectBuilder 13 | import kotlinx.serialization.json.JsonPrimitive 14 | import kotlinx.serialization.json.buildJsonArray 15 | import kotlinx.serialization.json.buildJsonObject 16 | import kotlinx.serialization.json.contentOrNull 17 | import kotlinx.serialization.json.jsonPrimitive 18 | 19 | typealias Claims = JsonObject 20 | 21 | @JwtDsl 22 | class ClaimsBuilder { 23 | /** 24 | * @see RFC7519, Section 25 | * 4.1.1 26 | */ 27 | var issuer: String? = null 28 | 29 | /** 30 | * @see RFC7519, Section 31 | * 4.1.2 32 | */ 33 | var subject: String? = null 34 | 35 | /** 36 | * @see RFC7519, Section 37 | * 4.1.3 38 | */ 39 | var audience: String? = null 40 | 41 | /** 42 | * @see RFC7519, Section 43 | * 4.1.4 44 | */ 45 | var expiresAt: Instant? = null 46 | 47 | /** 48 | * @see RFC7519, Section 49 | * 4.1.5 50 | */ 51 | var notBefore: Instant? = null 52 | 53 | /** 54 | * @see RFC7519, Section 55 | * 4.1.6 56 | */ 57 | var issuedAt: Instant? = null 58 | 59 | /** 60 | * @see RFC7519, Section 61 | * 4.1.7 62 | */ 63 | var id: String? = null 64 | 65 | private val additionalClaims: MutableMap = mutableMapOf() 66 | 67 | fun issuedAt(now: Instant = Clock.System.now()) { 68 | issuedAt = now 69 | } 70 | 71 | fun notBefore(before: Instant = Clock.System.now()) { 72 | notBefore = before 73 | } 74 | 75 | fun expires(at: Instant = Clock.System.now() + 60.minutes) { 76 | expiresAt = at 77 | } 78 | 79 | fun claim(key: String, value: String) { 80 | additionalClaims[key] = JsonPrimitive(value) 81 | } 82 | 83 | fun claim(key: String, value: Double) { 84 | additionalClaims[key] = JsonPrimitive(value) 85 | } 86 | 87 | fun claim(key: String, value: Long) { 88 | additionalClaims[key] = JsonPrimitive(value) 89 | } 90 | 91 | fun claim(key: String, value: Boolean) { 92 | additionalClaims[key] = JsonPrimitive(value) 93 | } 94 | 95 | @ExperimentalSerializationApi 96 | fun claim(key: String, value: Nothing?) { 97 | additionalClaims[key] = JsonPrimitive(value) 98 | } 99 | 100 | fun claim(key: String, value: JsonElement) { 101 | additionalClaims[key] = value 102 | } 103 | 104 | fun claim(key: String, value: JsonObject) { 105 | additionalClaims[key] = value 106 | } 107 | 108 | fun objectClaim(key: String, builder: JsonObjectBuilder.() -> Unit) { 109 | additionalClaims[key] = buildJsonObject(builder) 110 | } 111 | 112 | fun arrayClaim(key: String, builder: JsonArrayBuilder.() -> Unit) { 113 | additionalClaims[key] = buildJsonArray(builder) 114 | } 115 | 116 | fun claim(key: String, value: JsonArray) { 117 | additionalClaims[key] = value 118 | } 119 | 120 | internal fun build(): JsonObject { 121 | return JsonObject( 122 | buildMap { 123 | issuer?.let { put("iss", JsonPrimitive(it)) } 124 | subject?.let { put("sub", JsonPrimitive(it)) } 125 | audience?.let { put("aud", JsonPrimitive(it)) } 126 | expiresAt?.let { put("exp", JsonPrimitive(it.epochSeconds)) } 127 | notBefore?.let { put("nbf", JsonPrimitive(it.epochSeconds)) } 128 | issuedAt?.let { put("iat", JsonPrimitive(it.epochSeconds)) } 129 | id?.let { put("jti", JsonPrimitive(it)) } 130 | putAll(additionalClaims) 131 | }, 132 | ) 133 | } 134 | } 135 | 136 | /** 137 | * The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this 138 | * claim is generally application specific. The "iss" value is a case-sensitive string containing a 139 | * StringOrURI value. 140 | * 141 | * @see RFC7519, Section 142 | * 4.1.1 143 | */ 144 | val JWT.issuer: String? 145 | get() = claims.issuer 146 | 147 | val Claims.issuer: String? 148 | get() = get("iss")?.jsonPrimitive?.contentOrNull 149 | 150 | /** 151 | * The "sub" (subject) claim identifies the principal that is the subject of the JWT. The claims in 152 | * a JWT are normally statements about the subject. The subject value MUST either be scoped to be 153 | * locally unique in the context of the issuer or be globally unique. The processing of this claim 154 | * is generally application specific. The "sub" value is a case-sensitive string containing a 155 | * StringOrURI value. 156 | * 157 | * @see RFC7519, Section 158 | * 4.1.2 159 | */ 160 | val JWT.subject: String? 161 | get() = claims.subject 162 | 163 | val Claims.subject: String? 164 | get() = get("sub")?.jsonPrimitive?.contentOrNull 165 | 166 | /** 167 | * The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal 168 | * intended to process the JWT MUST identify itself with a value in the audience claim. If the 169 | * principal processing the claim does not identify itself with a value in the "aud" claim when this 170 | * claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array 171 | * of case- sensitive strings, each containing a StringOrURI value. In the special case when the JWT 172 | * has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI 173 | * value. The interpretation of audience values is generally application specific. 174 | * 175 | * @see RFC7519, Section 176 | * 4.1.3 177 | */ 178 | val JWT.audience: String? 179 | get() = claims.audience 180 | 181 | val Claims.audience: String? 182 | get() = get("aud")?.jsonPrimitive?.contentOrNull 183 | 184 | /** 185 | * The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST 186 | * NOT be accepted for processing. The processing of the "exp" claim requires that the current 187 | * date/time MUST be before the expiration date/time listed in the "exp" claim. Implementers MAY 188 | * provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its 189 | * value MUST be a number containing a NumericDate value. 190 | * 191 | * @see RFC7519, Section 192 | * 4.1.4 193 | */ 194 | val JWT.expiresAt: Instant? 195 | get() = claims.expiresAt 196 | 197 | val Claims.expiresAt: Instant? 198 | get() = instantOrNull("exp") 199 | 200 | /** 201 | * The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for 202 | * processing. The processing of the "nbf" claim requires that the current date/time MUST be after 203 | * or equal to the not-before date/time listed in the "nbf" claim. Implementers MAY provide for some 204 | * small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a 205 | * number containing a NumericDate value. 206 | * 207 | * @see RFC7519, Section 208 | * 4.1.5 209 | */ 210 | val JWT.notBefore: Instant? 211 | get() = claims.notBefore 212 | 213 | val Claims.notBefore: Instant? 214 | get() = instantOrNull("nbf") 215 | 216 | /** 217 | * The "iat" (issued at) claim identifies the time at which the JWT was issued. This claim can be 218 | * used to determine the age of the JWT. Its value MUST be a number containing a NumericDate value. 219 | * 220 | * @see RFC7519, Section 221 | * 4.1.6 222 | */ 223 | val JWT.issuedAt: Instant? 224 | get() = claims.issuedAt 225 | 226 | val Claims.issuedAt: Instant? 227 | get() = instantOrNull("iat") 228 | 229 | /** 230 | * The "jti" (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be 231 | * assigned in a manner that ensures that there is a negligible probability that the same value will 232 | * be accidentally assigned to a different data object; if the application uses multiple issuers, 233 | * collisions MUST be prevented among values produced by different issuers as well. The "jti" claim 234 | * can be used to prevent the JWT from being replayed. The "jti" value is a case- sensitive string. 235 | * 236 | * @see RFC7519, Section 237 | * 4.1.7 238 | */ 239 | val JWT.id: String? 240 | get() = claims.id 241 | 242 | val Claims.id: String? 243 | get() = get("jti")?.jsonPrimitive?.contentOrNull 244 | -------------------------------------------------------------------------------- /jwt/src/commonMain/kotlin/com/appstractive/jwt/Header.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Header( 7 | val alg: Algorithm = Algorithm.HS256, 8 | val typ: String, 9 | val kid: String? = null, 10 | ) 11 | 12 | @JwtDsl 13 | class HeaderBuilder { 14 | var typ: String = "JWT" 15 | 16 | internal fun build(): Header { 17 | return Header( 18 | typ = typ, 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jwt/src/commonMain/kotlin/com/appstractive/jwt/JWT.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import com.appstractive.jwt.utils.urlEncoded 4 | 5 | @DslMarker 6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) 7 | annotation class JwtDsl 8 | 9 | /** @see JSON Web Token (JWT) */ 10 | data class UnsignedJWT( 11 | val header: Header, 12 | val claims: Claims, 13 | ) { 14 | override fun toString(): String { 15 | return listOf( 16 | header.urlEncoded(), 17 | claims.urlEncoded(), 18 | "", 19 | ) 20 | .joinToString(".") 21 | } 22 | } 23 | 24 | /** @see JSON Web Token (JWT) */ 25 | data class JWT( 26 | val header: Header, 27 | val claims: Claims, 28 | val signedData: String, 29 | val signature: ByteArray, 30 | ) { 31 | 32 | override fun toString(): String { 33 | return listOf( 34 | signedData, 35 | urlEncoded(signature), 36 | ) 37 | .joinToString(".") 38 | } 39 | 40 | override fun equals(other: Any?): Boolean { 41 | if (this === other) return true 42 | if (other == null || this::class != other::class) return false 43 | 44 | other as JWT 45 | 46 | if (signedData != other.signedData) return false 47 | if (header != other.header) return false 48 | if (claims != other.claims) return false 49 | if (!signature.contentEquals(other.signature)) return false 50 | 51 | return true 52 | } 53 | 54 | override fun hashCode(): Int { 55 | var result = header.hashCode() 56 | result = 31 * result + claims.hashCode() 57 | result = 31 * result + signedData.hashCode() 58 | result = 31 * result + signature.contentHashCode() 59 | return result 60 | } 61 | 62 | companion object 63 | } 64 | 65 | fun jwt(builder: JwtBuilder.() -> Unit): UnsignedJWT { 66 | val jwtBuilder = JwtBuilder().apply(builder) 67 | 68 | return jwtBuilder.build() 69 | } 70 | 71 | @JwtDsl 72 | class JwtBuilder { 73 | 74 | private var header: HeaderBuilder = HeaderBuilder() 75 | private var claims: ClaimsBuilder? = null 76 | 77 | fun header(header: HeaderBuilder.() -> Unit) { 78 | this.header = HeaderBuilder().apply(header) 79 | } 80 | 81 | fun claims(builder: ClaimsBuilder.() -> Unit) { 82 | this.claims = ClaimsBuilder().apply(builder) 83 | } 84 | 85 | internal fun build(): UnsignedJWT { 86 | val header = header.build() 87 | 88 | val claims = claims?.build() ?: Claims(emptyMap()) 89 | 90 | return UnsignedJWT( 91 | header = header, 92 | claims = claims, 93 | ) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /jwt/src/commonMain/kotlin/com/appstractive/jwt/Parser.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import com.appstractive.jwt.utils.json 4 | import com.appstractive.jwt.utils.urlDecodedBytes 5 | import com.appstractive.jwt.utils.urlDecodedString 6 | 7 | fun JWT.Companion.from(jwt: String): JWT { 8 | val parts = jwt.split(".") 9 | check(parts.size == 3) { "JWT expects 3 parts but got ${parts.size}" } 10 | val header = json.decodeFromString
(parts[0].urlDecodedString()) 11 | val claims = json.decodeFromString(parts[1].urlDecodedString()) 12 | val signedData = "${parts[0]}.${parts[1]}" 13 | val signature = parts[2].urlDecodedBytes() 14 | 15 | return JWT( 16 | header = header, 17 | claims = claims, 18 | signedData = signedData, 19 | signature = signature, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /jwt/src/commonMain/kotlin/com/appstractive/jwt/Signing.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import com.appstractive.jwt.utils.urlEncoded 4 | import dev.whyoleg.cryptography.CryptographyAlgorithmId 5 | import dev.whyoleg.cryptography.algorithms.Digest 6 | import dev.whyoleg.cryptography.algorithms.SHA256 7 | import dev.whyoleg.cryptography.algorithms.SHA384 8 | import dev.whyoleg.cryptography.algorithms.SHA512 9 | import dev.whyoleg.cryptography.operations.SignatureGenerator 10 | import kotlinx.serialization.json.JsonObject 11 | 12 | enum class Algorithm { 13 | HS256, 14 | HS384, 15 | HS512, 16 | RS256, 17 | RS384, 18 | RS512, 19 | PS256, 20 | PS384, 21 | PS512, 22 | ES256, 23 | ES384, 24 | ES512, 25 | } 26 | 27 | val Algorithm.digest: CryptographyAlgorithmId 28 | get() = 29 | when (this) { 30 | Algorithm.ES256, 31 | Algorithm.PS256, 32 | Algorithm.RS256, 33 | Algorithm.HS256 -> SHA256 34 | 35 | Algorithm.ES384, 36 | Algorithm.RS384, 37 | Algorithm.PS384, 38 | Algorithm.HS384 -> SHA384 39 | 40 | Algorithm.ES512, 41 | Algorithm.HS512, 42 | Algorithm.RS512, 43 | Algorithm.PS512 -> SHA512 44 | } 45 | 46 | interface SigningAlgorithm { 47 | suspend fun generator(digest: CryptographyAlgorithmId): SignatureGenerator 48 | 49 | suspend fun sign(header: Header, claims: JsonObject): Pair { 50 | val headerEncoded = header.urlEncoded() 51 | val claimsEncoded = claims.urlEncoded() 52 | val tbsData = "$headerEncoded.$claimsEncoded".encodeToByteArray() 53 | 54 | return generator(digest = header.alg.digest) 55 | .generateSignature(tbsData) to tbsData 56 | } 57 | } 58 | 59 | @JwtDsl 60 | class Signer { 61 | 62 | private var algorithm: SigningAlgorithm? = null 63 | internal var type: Algorithm? = null 64 | private set 65 | 66 | fun algorithm(algorithm: SigningAlgorithm, type: Algorithm) { 67 | this.algorithm = algorithm 68 | this.type = type 69 | } 70 | 71 | internal suspend fun sign(header: Header, claims: JsonObject): Pair { 72 | return algorithm?.sign( 73 | header = header, 74 | claims = claims, 75 | ) ?: throw IllegalStateException("No algorithm configured") 76 | } 77 | } 78 | 79 | fun signer(builder: Signer.() -> Unit): Signer = Signer().apply(builder) 80 | 81 | suspend fun UnsignedJWT.sign(kid: String? = null, builder: Signer.() -> Unit): JWT = 82 | sign( 83 | kid = kid, 84 | signer = signer(builder), 85 | ) 86 | 87 | suspend fun UnsignedJWT.sign(kid: String? = null, signer: Signer): JWT { 88 | val finalHeader = 89 | header.copy( 90 | alg = checkNotNull(signer.type) { "No algorithm configured" }, 91 | kid = kid, 92 | ) 93 | val (signature, signedData) = signer.sign( 94 | header = finalHeader, 95 | claims = claims, 96 | ) 97 | return JWT( 98 | header = finalHeader, 99 | claims = claims, 100 | signedData = signedData.decodeToString(), 101 | signature = signature, 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /jwt/src/commonMain/kotlin/com/appstractive/jwt/Verification.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import dev.whyoleg.cryptography.operations.SignatureVerifier 4 | import kotlinx.datetime.Clock 5 | import kotlinx.datetime.Instant 6 | 7 | interface VerificationAlgorithm { 8 | suspend fun verifier(jwt: JWT): SignatureVerifier 9 | 10 | suspend fun verify(jwt: JWT): Boolean { 11 | return verifier(jwt = jwt).tryVerifySignature(jwt.signedData.encodeToByteArray(), jwt.signature) 12 | } 13 | } 14 | 15 | class Verifier { 16 | private val algorithms: MutableMap = mutableMapOf() 17 | 18 | private var verifyExpiresAt: Instant? = null 19 | private var verifyNotBefore: Instant? = null 20 | 21 | private var audiences: MutableSet = mutableSetOf() 22 | private var issuers: MutableSet = mutableSetOf() 23 | 24 | fun algorithm(type: Algorithm, algorithm: VerificationAlgorithm) { 25 | algorithms[type] = algorithm 26 | } 27 | 28 | fun audience(vararg audience: String) { 29 | audiences.addAll(audience) 30 | } 31 | 32 | fun issuer(vararg issuer: String) { 33 | issuers.addAll(issuer) 34 | } 35 | 36 | fun expiresAt(now: Instant = Clock.System.now()) { 37 | verifyExpiresAt = now 38 | } 39 | 40 | fun notBefore(now: Instant = Clock.System.now()) { 41 | verifyNotBefore = now 42 | } 43 | 44 | internal suspend fun verify(jwt: JWT): Boolean { 45 | if (audiences.isNotEmpty() && !audiences.contains(jwt.audience)) { 46 | return false 47 | } 48 | 49 | if (issuers.isNotEmpty() && !issuers.contains(jwt.issuer)) { 50 | return false 51 | } 52 | 53 | verifyExpiresAt?.let { 54 | val expiresAt = jwt.expiresAt ?: return false 55 | 56 | if (expiresAt < it) { 57 | return false 58 | } 59 | } 60 | 61 | verifyNotBefore?.let { 62 | val notBefore = jwt.notBefore ?: return false 63 | 64 | if (notBefore >= it) { 65 | return false 66 | } 67 | } 68 | 69 | return algorithms[jwt.header.alg]?.verify(jwt = jwt) 70 | ?: throw IllegalStateException("No verifier configured for ${jwt.header.alg}") 71 | } 72 | } 73 | 74 | fun jwtVerifier(builder: Verifier.() -> Unit): Verifier = Verifier().apply(builder) 75 | 76 | suspend fun JWT.verify(builder: Verifier.() -> Unit): Boolean = verify(jwtVerifier(builder)) 77 | 78 | suspend fun JWT.verify(verifier: Verifier): Boolean { 79 | return verifier.verify(this) 80 | } 81 | -------------------------------------------------------------------------------- /jwt/src/commonMain/kotlin/com/appstractive/jwt/utils/Base64Utils.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalEncodingApi::class, ExperimentalEncodingApi::class) 2 | 3 | package com.appstractive.jwt.utils 4 | 5 | import com.appstractive.jwt.Claims 6 | import com.appstractive.jwt.Header 7 | import com.appstractive.jwt.UnsignedJWT 8 | import kotlin.io.encoding.Base64 9 | import kotlin.io.encoding.Base64.Default.UrlSafe 10 | import kotlin.io.encoding.ExperimentalEncodingApi 11 | import kotlinx.serialization.encodeToString 12 | 13 | fun UnsignedJWT.urlEncoded(): String = "${header.urlEncoded()}.${claims.urlEncoded()}" 14 | 15 | private val base64 = UrlSafe.withPadding(Base64.PaddingOption.ABSENT) 16 | 17 | fun Header.urlEncoded(): String { 18 | return urlEncoded(json.encodeToString(this)) 19 | } 20 | 21 | fun Claims.urlEncoded(): String { 22 | return urlEncoded(json.encodeToString(this)) 23 | } 24 | 25 | fun urlEncoded(source: String): String { 26 | return urlEncoded(source.encodeToByteArray()) 27 | } 28 | 29 | fun urlEncoded(source: ByteArray): String { 30 | return base64.encode(source) 31 | } 32 | 33 | fun String.urlDecodedString(): String { 34 | return base64.decode(this).decodeToString() 35 | } 36 | 37 | fun String.urlDecodedBytes(): ByteArray { 38 | return base64.decode(this) 39 | } 40 | -------------------------------------------------------------------------------- /jwt/src/commonMain/kotlin/com/appstractive/jwt/utils/ClaimsExt.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.utils 2 | 3 | import com.appstractive.jwt.ClaimsBuilder 4 | import kotlinx.datetime.Instant 5 | import kotlinx.serialization.StringFormat 6 | import kotlinx.serialization.decodeFromString 7 | import kotlinx.serialization.encodeToString 8 | import kotlinx.serialization.json.Json 9 | import kotlinx.serialization.json.JsonElement 10 | import kotlinx.serialization.json.JsonObject 11 | import kotlinx.serialization.json.jsonPrimitive 12 | import kotlinx.serialization.json.longOrNull 13 | 14 | fun JsonObject.instantOrNull(claim: String): Instant? = 15 | this[claim]?.jsonPrimitive?.longOrNull?.let { Instant.fromEpochSeconds(it) } 16 | 17 | inline fun ClaimsBuilder.claim( 18 | key: String, 19 | value: T, 20 | serializer: StringFormat = Json, 21 | ) { 22 | claim( 23 | key = key, value = serializer.decodeFromString(serializer.encodeToString(value)), 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /jwt/src/commonMain/kotlin/com/appstractive/jwt/utils/Serialization.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt.utils 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | internal val json = Json { 6 | isLenient = true 7 | ignoreUnknownKeys = true 8 | explicitNulls = false 9 | encodeDefaults = true 10 | } 11 | -------------------------------------------------------------------------------- /jwt/src/commonTest/kotlin/com/appstractive/jwt/Common.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import com.appstractive.jwt.utils.claim 4 | import dev.whyoleg.cryptography.CryptographyAlgorithmId 5 | import dev.whyoleg.cryptography.CryptographyProvider 6 | import dev.whyoleg.cryptography.CryptographyProviderApi 7 | import dev.whyoleg.cryptography.algorithms.Digest 8 | import dev.whyoleg.cryptography.operations.SignFunction 9 | import dev.whyoleg.cryptography.operations.SignatureGenerator 10 | import dev.whyoleg.cryptography.operations.SignatureVerifier 11 | import dev.whyoleg.cryptography.operations.VerifyFunction 12 | import kotlin.time.Duration.Companion.hours 13 | import kotlinx.datetime.Clock 14 | import kotlinx.serialization.ExperimentalSerializationApi 15 | import kotlinx.serialization.Serializable 16 | import kotlinx.serialization.json.JsonPrimitive 17 | 18 | @OptIn(CryptographyProviderApi::class) 19 | object MockSignerVerifier : VerificationAlgorithm, SigningAlgorithm { 20 | override suspend fun verifier(jwt: JWT): SignatureVerifier { 21 | return object : SignatureVerifier { 22 | override fun createVerifyFunction(): VerifyFunction { 23 | return object : VerifyFunction { 24 | override fun tryVerify(signature: ByteArray, startIndex: Int, endIndex: Int): Boolean { 25 | return true 26 | } 27 | 28 | override fun close() = Unit 29 | 30 | override fun reset() = Unit 31 | 32 | override fun update(source: ByteArray, startIndex: Int, endIndex: Int) = Unit 33 | 34 | override fun verify(signature: ByteArray, startIndex: Int, endIndex: Int) = Unit 35 | } 36 | } 37 | } 38 | } 39 | 40 | override suspend fun generator(digest: CryptographyAlgorithmId): SignatureGenerator { 41 | return object : SignatureGenerator { 42 | override fun createSignFunction(): SignFunction { 43 | return object : SignFunction { 44 | override fun signIntoByteArray(destination: ByteArray, destinationOffset: Int): Int { 45 | return 0 46 | } 47 | 48 | override fun close() = Unit 49 | 50 | override fun reset() = Unit 51 | 52 | override fun signToByteArray(): ByteArray { 53 | return "123456".encodeToByteArray() 54 | } 55 | 56 | override fun update(source: ByteArray, startIndex: Int, endIndex: Int) = Unit 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | @OptIn(CryptographyProviderApi::class) 64 | open class MockHashingVerifier(val referenceHash: ByteArray, val hashAlg: CryptographyAlgorithmId) : VerificationAlgorithm { 65 | override suspend fun verifier(jwt: JWT): SignatureVerifier { 66 | return object : SignatureVerifier { 67 | override fun createVerifyFunction(): VerifyFunction { 68 | return object : VerifyFunction { 69 | override fun tryVerify(signature: ByteArray, startIndex: Int, endIndex: Int): Boolean { 70 | return referenceHash contentEquals CryptographyProvider.Default.get(hashAlg).hasher().hashBlocking(jwt.signedData.encodeToByteArray()) 71 | } 72 | 73 | override fun close() = Unit 74 | 75 | override fun reset() = Unit 76 | 77 | override fun update(source: ByteArray, startIndex: Int, endIndex: Int) = Unit 78 | 79 | override fun verify(signature: ByteArray, startIndex: Int, endIndex: Int) { 80 | check(tryVerify(signature = signature, startIndex = startIndex, endIndex = endIndex)) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | private enum class TestEnum { 89 | VALUE1, 90 | VALUE2, 91 | VALUE3, 92 | } 93 | 94 | @Serializable 95 | private data class TestClass( 96 | val long: Long, 97 | val int: Int, 98 | val double: Double, 99 | val float: Float, 100 | val bool: Boolean, 101 | val string: String, 102 | val enum: TestEnum, 103 | val list: List, 104 | val nullable: Nothing? = null, 105 | ) 106 | 107 | @OptIn(ExperimentalSerializationApi::class) 108 | fun unsignedJwt(): UnsignedJWT = jwt { 109 | claims { 110 | issuer = "example.com" 111 | subject = "me" 112 | audience = "api.example.com" 113 | expires(at = Clock.System.now() + 24.hours) 114 | notBefore() 115 | issuedAt() 116 | id = "123456" 117 | 118 | claim("double", 1.1) 119 | claim("long", 1L) 120 | claim("bool", true) 121 | claim("string", "test") 122 | claim("null", null) 123 | objectClaim("object") { put("key", JsonPrimitive("value")) } 124 | arrayClaim("list") { 125 | add(JsonPrimitive(0)) 126 | add(JsonPrimitive(1)) 127 | add(JsonPrimitive(2)) 128 | } 129 | 130 | claim( 131 | key = "complex", 132 | value = 133 | TestClass( 134 | long = 123443543642353L, 135 | int = 1, 136 | double = 124321412.325325, 137 | float = 1.2414f, 138 | bool = true, 139 | string = "test", 140 | enum = TestEnum.VALUE1, 141 | list = 142 | listOf( 143 | TestClass( 144 | long = 123443543642353L, 145 | int = 1, 146 | double = 124321412.325325, 147 | float = 1.2414f, 148 | bool = false, 149 | string = "test2", 150 | enum = TestEnum.VALUE2, 151 | list = emptyList(), 152 | ), 153 | ), 154 | ), 155 | ) 156 | } 157 | } 158 | 159 | suspend fun signedJwt( 160 | builder: Signer.() -> Unit, 161 | ): JWT { 162 | return unsignedJwt().sign(builder = builder) 163 | } 164 | -------------------------------------------------------------------------------- /jwt/src/commonTest/kotlin/com/appstractive/jwt/JwtParserTests.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlinx.datetime.Instant 6 | import kotlinx.serialization.json.double 7 | import kotlinx.serialization.json.jsonPrimitive 8 | 9 | class JwtParserTests { 10 | 11 | @Test 12 | fun testParse() { 13 | val jwt = 14 | JWT.from( 15 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQ1NiJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsInN1YiI6Im1lIiwiYXVkIjoiYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzIzNzMyMjY4LCJuYmYiOjE3MjM2NDU4NjgsImlhdCI6MTcyMzY0NTg2OCwianRpIjoiMTIzNDU2IiwiZG91YmxlIjoxLjEsImxvbmciOjEsImJvb2wiOnRydWUsInN0cmluZyI6InRlc3QiLCJudWxsIjpudWxsLCJvYmplY3QiOnsia2V5IjoidmFsdWUifSwibGlzdCI6WzAsMSwyXSwiY29tcGxleCI6eyJsb25nIjoxMjM0NDM1NDM2NDIzNTMsImludCI6MSwiZG91YmxlIjoxLjI0MzIxNDEyMzI1MzI1RTgsImZsb2F0IjoxLjI0MTQsImJvb2wiOnRydWUsInN0cmluZyI6InRlc3QiLCJlbnVtIjoiVkFMVUUxIiwibGlzdCI6W3sibG9uZyI6MTIzNDQzNTQzNjQyMzUzLCJpbnQiOjEsImRvdWJsZSI6MS4yNDMyMTQxMjMyNTMyNUU4LCJmbG9hdCI6MS4yNDE0LCJib29sIjpmYWxzZSwic3RyaW5nIjoidGVzdDIiLCJlbnVtIjoiVkFMVUUyIiwibGlzdCI6W119XX19.15OVSybg14VvqHOthDNZrpGLI18BGmB6LdHMOg--LtE", 16 | ) 17 | 18 | assertEquals("example.com", jwt.issuer) 19 | assertEquals("me", jwt.subject) 20 | assertEquals(Instant.fromEpochSeconds(1723732268), jwt.expiresAt) 21 | assertEquals(Instant.fromEpochSeconds(1723645868), jwt.notBefore) 22 | assertEquals(Instant.fromEpochSeconds(1723645868), jwt.issuedAt) 23 | assertEquals("123456", jwt.id) 24 | assertEquals(1.1, jwt.claims["double"]?.jsonPrimitive?.double) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jwt/src/commonTest/kotlin/com/appstractive/jwt/JwtVerifierTests.kt: -------------------------------------------------------------------------------- 1 | package com.appstractive.jwt 2 | 3 | import com.appstractive.jwt.utils.urlEncoded 4 | import dev.whyoleg.cryptography.CryptographyProvider 5 | import kotlin.test.Test 6 | import kotlin.test.assertFalse 7 | import kotlin.test.assertTrue 8 | import kotlin.time.Duration.Companion.hours 9 | import kotlinx.coroutines.test.runTest 10 | import kotlinx.datetime.Clock 11 | 12 | class JwtVerifierTests { 13 | 14 | @Test 15 | fun testVerifyAudSuccess() = runTest { 16 | val jwt = signedJwt { 17 | algorithm( 18 | type = Algorithm.HS256, 19 | algorithm = MockSignerVerifier, 20 | ) 21 | } 22 | 23 | val result = 24 | jwt.verify { 25 | algorithm( 26 | type = Algorithm.HS256, 27 | algorithm = MockSignerVerifier, 28 | ) 29 | 30 | audience("api.example.com", "api.example2.com") 31 | } 32 | 33 | assertTrue(result) 34 | } 35 | 36 | @Test 37 | fun testVerifyAudFail() = runTest { 38 | val jwt = signedJwt { 39 | algorithm( 40 | type = Algorithm.HS256, 41 | algorithm = MockSignerVerifier, 42 | ) 43 | } 44 | 45 | val result = 46 | jwt.verify { 47 | algorithm( 48 | type = Algorithm.HS256, 49 | algorithm = MockSignerVerifier, 50 | ) 51 | 52 | audience("example.com", "example2.com") 53 | } 54 | 55 | assertFalse(result) 56 | } 57 | 58 | @Test 59 | fun testVerifyIssSuccess() = runTest { 60 | val jwt = signedJwt { 61 | algorithm( 62 | type = Algorithm.HS256, 63 | algorithm = MockSignerVerifier, 64 | ) 65 | } 66 | 67 | val result = 68 | jwt.verify { 69 | algorithm( 70 | type = Algorithm.HS256, 71 | algorithm = MockSignerVerifier, 72 | ) 73 | 74 | issuer("example.com", "example2.com") 75 | } 76 | 77 | assertTrue(result) 78 | } 79 | 80 | @Test 81 | fun testVerifyIssFail() = runTest { 82 | val jwt = signedJwt { 83 | algorithm( 84 | type = Algorithm.HS256, 85 | algorithm = MockSignerVerifier, 86 | ) 87 | } 88 | 89 | val result = 90 | jwt.verify { 91 | algorithm( 92 | type = Algorithm.HS256, 93 | algorithm = MockSignerVerifier, 94 | ) 95 | 96 | issuer("example2.com", "example3.com") 97 | } 98 | 99 | assertFalse(result) 100 | } 101 | 102 | @Test 103 | fun testVerifyExpSuccess() = runTest { 104 | val jwt = signedJwt { 105 | algorithm( 106 | type = Algorithm.HS256, 107 | algorithm = MockSignerVerifier, 108 | ) 109 | } 110 | 111 | val result = 112 | jwt.verify { 113 | algorithm( 114 | type = Algorithm.HS256, 115 | algorithm = MockSignerVerifier, 116 | ) 117 | 118 | expiresAt() 119 | } 120 | 121 | assertTrue(result) 122 | } 123 | 124 | @Test 125 | fun testVerifyExpFail() = runTest { 126 | val jwt = signedJwt { 127 | algorithm( 128 | type = Algorithm.HS256, 129 | algorithm = MockSignerVerifier, 130 | ) 131 | } 132 | 133 | val result = 134 | jwt.verify { 135 | algorithm( 136 | type = Algorithm.HS256, 137 | algorithm = MockSignerVerifier, 138 | ) 139 | 140 | expiresAt(now = Clock.System.now() + 25.hours) 141 | } 142 | 143 | assertFalse(result) 144 | } 145 | 146 | @Test 147 | fun testVerifyNbfSuccess() = runTest { 148 | val jwt = signedJwt { 149 | algorithm( 150 | type = Algorithm.HS256, 151 | algorithm = MockSignerVerifier, 152 | ) 153 | } 154 | 155 | val result = 156 | jwt.verify { 157 | algorithm( 158 | type = Algorithm.HS256, 159 | algorithm = MockSignerVerifier, 160 | ) 161 | 162 | notBefore() 163 | } 164 | 165 | assertTrue(result) 166 | } 167 | 168 | @Test 169 | fun testVerifyNbfFail() = runTest { 170 | val jwt = signedJwt { 171 | algorithm( 172 | type = Algorithm.HS256, 173 | algorithm = MockSignerVerifier, 174 | ) 175 | } 176 | 177 | val result = 178 | jwt.verify { 179 | algorithm( 180 | type = Algorithm.HS256, 181 | algorithm = MockSignerVerifier, 182 | ) 183 | 184 | notBefore(now = Clock.System.now() - 1.hours) 185 | } 186 | 187 | assertFalse(result) 188 | } 189 | 190 | @Test 191 | fun testVerifyDifferentHeaderOrderSuccess() = runTest { 192 | val signedHeaderNormal = urlEncoded("""{"alg":"ES256","typ":"JWT"}""") 193 | assertTrue { checkSignatureHash(signedHeaderNormal, Algorithm.ES256) } 194 | val signedHeaderReverse = urlEncoded("""{"typ":"JWT","alg":"ES256"}""") 195 | assertTrue { checkSignatureHash(signedHeaderReverse, Algorithm.ES256) } 196 | } 197 | 198 | private suspend fun checkSignatureHash(signedHeader: String, sigAlg: Algorithm): Boolean { 199 | val signedBody = urlEncoded("{}") 200 | val signedPart = "$signedHeader.$signedBody" 201 | val signaturePart = urlEncoded("don't care") 202 | val expectedHash = CryptographyProvider.Default.get(sigAlg.digest).hasher().hash(signedPart.encodeToByteArray()) 203 | val jwt = JWT.from("$signedPart.$signaturePart") 204 | 205 | return jwt.verify { 206 | this.algorithm( 207 | type = sigAlg, 208 | algorithm = MockHashingVerifier(expectedHash, sigAlg.digest), 209 | ) 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /ktor-server-auth-jwt/README.md: -------------------------------------------------------------------------------- 1 | # KTOR Server Auth JWT Kotlin Multiplatform 2 | 3 | ![badge][badge-android] 4 | ![badge][badge-apple] 5 | ![badge][badge-jvm] 6 | ![badge][badge-linux] 7 | 8 | A fork of the [KTOR Server Auth JWT Plugin](https://ktor.io/docs/server-jwt.html) supporting more 9 | platforms than just JVM. 10 | 11 | ## Usage 12 | 13 | ### Installation 14 | 15 | Gradle: 16 | 17 | ``` 18 | commonMain.dependencies { 19 | implementation("com.appstractive:ktor-server-auth-jwt:1.1.0") 20 | implementation("com.appstractive:jwt-hmac-kt:1.1.0") 21 | // or 22 | // implementation("com.appstractive:jwt-rsa-kt:1.1.0") 23 | // implementation("com.appstractive:jwt-ecdsa-kt:1.1.0") 24 | } 25 | ``` 26 | 27 | ### Install 28 | 29 | ```kotlin 30 | val mySecret = CryptographyRandom.nextBytes(64) 31 | 32 | install(Authentication) { 33 | jwt("auth-jwt") { 34 | realm = "myRealm" 35 | verifier( 36 | issuer = "example.com", 37 | audience = "api.example.com", 38 | ) { 39 | hs256 { secret = mySecret } 40 | } 41 | 42 | validate { credential -> 43 | if (credential.claims["username"]?.jsonPrimitive?.content != "") { 44 | JWTPrincipal(credential.claims) 45 | } else { 46 | null 47 | } 48 | } 49 | 50 | challenge { defaultScheme, realm -> 51 | call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired") 52 | } 53 | } 54 | } 55 | 56 | post("/login") { 57 | val user = call.receive() 58 | // Check username and password 59 | // ... 60 | val token = 61 | jwt { 62 | claims { 63 | this.audience = audience 64 | this.issuer = issuer 65 | claim("username", user.username) 66 | expires() 67 | } 68 | } 69 | .sign { hs256 { secret = mySecret } } 70 | 71 | call.respond(hashMapOf("token" to token.toString())) 72 | } 73 | 74 | ``` 75 | 76 | ## License 77 | 78 | ``` 79 | Copyright 2024 Andreas Schulz. 80 | 81 | Licensed under the Apache License, Version 2.0 (the "License"); 82 | you may not use this file except in compliance with the License. 83 | You may obtain a copy of the License at 84 | 85 | http://www.apache.org/licenses/LICENSE-2.0 86 | 87 | Unless required by applicable law or agreed to in writing, software 88 | distributed under the License is distributed on an "AS IS" BASIS, 89 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 90 | See the License for the specific language governing permissions and 91 | limitations under the License. 92 | ``` 93 | 94 | [badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat 95 | 96 | [badge-apple]: http://img.shields.io/badge/platform-apple-111111.svg?style=flat 97 | 98 | [badge-jvm]: http://img.shields.io/badge/platform-jvm-CDCDCD.svg?style=flat 99 | 100 | [badge-linux]: http://img.shields.io/badge/platform-linux-CDCDCD.svg?style=flat -------------------------------------------------------------------------------- /ktor-server-auth-jwt/api/android/ktor-server-auth-jwt.api: -------------------------------------------------------------------------------- 1 | public final class io/ktor/server/auth/jwt/JWTAuthKt { 2 | public static final fun jwt (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V 3 | public static synthetic fun jwt$default (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V 4 | } 5 | 6 | public final class io/ktor/server/auth/jwt/JWTAuthenticationProvider : io/ktor/server/auth/AuthenticationProvider { 7 | public fun onAuthenticate (Lio/ktor/server/auth/AuthenticationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 8 | } 9 | 10 | public final class io/ktor/server/auth/jwt/JWTAuthenticationProvider$Config : io/ktor/server/auth/AuthenticationProvider$Config { 11 | public final fun authHeader (Lkotlin/jvm/functions/Function1;)V 12 | public final fun authSchemes (Ljava/lang/String;[Ljava/lang/String;)V 13 | public static synthetic fun authSchemes$default (Lio/ktor/server/auth/jwt/JWTAuthenticationProvider$Config;Ljava/lang/String;[Ljava/lang/String;ILjava/lang/Object;)V 14 | public final fun challenge (Lkotlin/jvm/functions/Function4;)V 15 | public final fun getRealm ()Ljava/lang/String; 16 | public final fun setRealm (Ljava/lang/String;)V 17 | public final fun validate (Lkotlin/jvm/functions/Function3;)V 18 | public final fun verifier (Lcom/appstractive/jwt/Verifier;)V 19 | public final fun verifier (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V 20 | public final fun verifier (Lkotlin/jvm/functions/Function1;)V 21 | public static synthetic fun verifier$default (Lio/ktor/server/auth/jwt/JWTAuthenticationProvider$Config;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V 22 | } 23 | 24 | public final class io/ktor/server/auth/jwt/JWTChallengeContext { 25 | public fun (Lio/ktor/server/application/ApplicationCall;)V 26 | public final fun getCall ()Lio/ktor/server/application/ApplicationCall; 27 | } 28 | 29 | public final class io/ktor/server/auth/jwt/JWTCredential { 30 | public fun (Lkotlinx/serialization/json/JsonObject;)V 31 | public final fun getClaims ()Lkotlinx/serialization/json/JsonObject; 32 | } 33 | 34 | public final class io/ktor/server/auth/jwt/JWTPrincipal { 35 | public fun (Lkotlinx/serialization/json/JsonObject;)V 36 | public final fun getClaims ()Lkotlinx/serialization/json/JsonObject; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /ktor-server-auth-jwt/api/jvm/ktor-server-auth-jwt.api: -------------------------------------------------------------------------------- 1 | public final class io/ktor/server/auth/jwt/JWTAuthKt { 2 | public static final fun jwt (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V 3 | public static synthetic fun jwt$default (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V 4 | } 5 | 6 | public final class io/ktor/server/auth/jwt/JWTAuthenticationProvider : io/ktor/server/auth/AuthenticationProvider { 7 | public fun onAuthenticate (Lio/ktor/server/auth/AuthenticationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 8 | } 9 | 10 | public final class io/ktor/server/auth/jwt/JWTAuthenticationProvider$Config : io/ktor/server/auth/AuthenticationProvider$Config { 11 | public final fun authHeader (Lkotlin/jvm/functions/Function1;)V 12 | public final fun authSchemes (Ljava/lang/String;[Ljava/lang/String;)V 13 | public static synthetic fun authSchemes$default (Lio/ktor/server/auth/jwt/JWTAuthenticationProvider$Config;Ljava/lang/String;[Ljava/lang/String;ILjava/lang/Object;)V 14 | public final fun challenge (Lkotlin/jvm/functions/Function4;)V 15 | public final fun getRealm ()Ljava/lang/String; 16 | public final fun setRealm (Ljava/lang/String;)V 17 | public final fun validate (Lkotlin/jvm/functions/Function3;)V 18 | public final fun verifier (Lcom/appstractive/jwt/Verifier;)V 19 | public final fun verifier (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V 20 | public final fun verifier (Lkotlin/jvm/functions/Function1;)V 21 | public static synthetic fun verifier$default (Lio/ktor/server/auth/jwt/JWTAuthenticationProvider$Config;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V 22 | } 23 | 24 | public final class io/ktor/server/auth/jwt/JWTChallengeContext { 25 | public fun (Lio/ktor/server/application/ApplicationCall;)V 26 | public final fun getCall ()Lio/ktor/server/application/ApplicationCall; 27 | } 28 | 29 | public final class io/ktor/server/auth/jwt/JWTCredential { 30 | public fun (Lkotlinx/serialization/json/JsonObject;)V 31 | public final fun getClaims ()Lkotlinx/serialization/json/JsonObject; 32 | } 33 | 34 | public final class io/ktor/server/auth/jwt/JWTPrincipal { 35 | public fun (Lkotlinx/serialization/json/JsonObject;)V 36 | public final fun getClaims ()Lkotlinx/serialization/json/JsonObject; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /ktor-server-auth-jwt/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | alias(libs.plugins.android.library) 6 | alias(libs.plugins.kotlinx.serialization) 7 | alias(libs.plugins.kotlinx.binary.compatibility) 8 | id("jwt.publication") 9 | } 10 | 11 | group = rootProject.group 12 | 13 | version = rootProject.version 14 | 15 | kotlin { 16 | androidTarget { 17 | publishAllLibraryVariants() 18 | compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } 19 | } 20 | 21 | jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } 22 | 23 | linuxArm64() 24 | linuxX64() 25 | 26 | listOf( 27 | iosX64(), 28 | iosArm64(), 29 | iosSimulatorArm64(), 30 | macosX64(), 31 | macosArm64(), 32 | tvosX64(), 33 | tvosArm64(), 34 | tvosSimulatorArm64(), 35 | watchosX64(), 36 | watchosArm64(), 37 | watchosSimulatorArm64(), 38 | ) 39 | .forEach { 40 | it.binaries.framework { 41 | baseName = "Ktor Server Auth JWT" 42 | isStatic = true 43 | } 44 | } 45 | 46 | sourceSets { 47 | commonMain.dependencies { 48 | api(projects.jwtKt) 49 | implementation(libs.ktor.server.auth) 50 | } 51 | 52 | commonTest.dependencies {} 53 | 54 | androidMain.dependencies {} 55 | 56 | jvmMain.dependencies {} 57 | 58 | appleMain.dependencies {} 59 | 60 | linuxMain.dependencies {} 61 | } 62 | } 63 | 64 | android { 65 | namespace = group.toString() 66 | compileSdk = libs.versions.compileSdk.get().toInt() 67 | 68 | defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } 69 | sourceSets["main"].apply { manifest.srcFile("src/androidMain/AndroidManifest.xml") } 70 | compileOptions { 71 | sourceCompatibility = JavaVersion.VERSION_17 72 | targetCompatibility = JavaVersion.VERSION_17 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ktor-server-auth-jwt/src/commonMain/kotlin/io/ktor/server/auth/jwt/JWTAuth.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package io.ktor.server.auth.jwt 6 | 7 | import com.appstractive.jwt.Claims 8 | import com.appstractive.jwt.Verifier 9 | import com.appstractive.jwt.jwtVerifier 10 | import io.ktor.http.auth.* 11 | import io.ktor.server.application.* 12 | import io.ktor.server.auth.* 13 | import io.ktor.server.response.* 14 | import kotlin.reflect.* 15 | 16 | internal val JWTAuthKey: Any = "JWTAuthNative" 17 | 18 | /** 19 | * A JWT credential that consists of the specified [claims]. 20 | * 21 | * @param claims JWT 22 | * @see Claims 23 | */ 24 | public class JWTCredential(val claims: Claims) 25 | 26 | /** 27 | * A JWT principal that consists of the specified [claims]. 28 | * 29 | * @param payload JWT 30 | * @see Payload 31 | */ 32 | public class JWTPrincipal(val claims: Claims) 33 | 34 | /** 35 | * A JWT [Authentication] provider. 36 | * 37 | * @see [jwt] 38 | */ 39 | public class JWTAuthenticationProvider internal constructor(config: Config) : 40 | AuthenticationProvider(config) { 41 | 42 | private val realm: String = config.realm 43 | private val schemes: JWTAuthSchemes = config.schemes 44 | private val authHeader: (ApplicationCall) -> HttpAuthHeader? = config.authHeader 45 | private val verifier: ((HttpAuthHeader) -> Verifier?) = config.verifier 46 | private val authenticationFunction = config.authenticationFunction 47 | private val challengeFunction: JWTAuthChallengeFunction = config.challenge 48 | 49 | override suspend fun onAuthenticate(context: AuthenticationContext) { 50 | val call = context.call 51 | val token = authHeader(call) 52 | if (token == null) { 53 | context.bearerChallenge( 54 | AuthenticationFailedCause.NoCredentials, realm, schemes, challengeFunction, 55 | ) 56 | return 57 | } 58 | 59 | try { 60 | val principal = 61 | verifyAndValidate( 62 | call = call, 63 | jwtVerifier = verifier(token), 64 | token = token, 65 | schemes = schemes, 66 | validate = authenticationFunction, 67 | ) 68 | if (principal != null) { 69 | context.principal(name, principal) 70 | return 71 | } 72 | 73 | context.bearerChallenge( 74 | AuthenticationFailedCause.InvalidCredentials, realm, schemes, challengeFunction, 75 | ) 76 | } catch (cause: Throwable) { 77 | val message = cause.message ?: cause.toString() 78 | context.error(JWTAuthKey, AuthenticationFailedCause.Error(message)) 79 | } 80 | } 81 | 82 | /** A configuration for the [jwt] authentication provider. */ 83 | public class Config internal constructor(name: String?) : AuthenticationProvider.Config(name) { 84 | internal var authenticationFunction: AuthenticationFunction = { 85 | throw NotImplementedError( 86 | "JWT auth validate function is not specified. Use jwt { validate { ... } } to fix.", 87 | ) 88 | } 89 | 90 | internal var schemes = JWTAuthSchemes("Bearer") 91 | 92 | internal var authHeader: (ApplicationCall) -> HttpAuthHeader? = { call -> 93 | call.request.parseAuthorizationHeaderOrNull() 94 | } 95 | 96 | internal var verifier: ((HttpAuthHeader) -> Verifier?) = { null } 97 | 98 | internal var challenge: JWTAuthChallengeFunction = { scheme, realm -> 99 | call.respond( 100 | UnauthorizedResponse( 101 | HttpAuthHeader.Parameterized( 102 | scheme, mapOf(HttpAuthHeader.Parameters.Realm to realm), 103 | ), 104 | ), 105 | ) 106 | } 107 | 108 | /** Specifies a JWT realm to be passed in `WWW-Authenticate` header. */ 109 | public var realm: String = "Ktor Server" 110 | 111 | /** 112 | * Retrieves an HTTP authentication header. By default, it parses the `Authorization` header 113 | * content. 114 | */ 115 | public fun authHeader(block: (ApplicationCall) -> HttpAuthHeader?) { 116 | authHeader = block 117 | } 118 | 119 | /** 120 | * @param [defaultScheme] default scheme used to challenge the client when no valid 121 | * authentication is provided 122 | * @param [additionalSchemes] additional schemes that are accepted when validating the 123 | * authentication 124 | */ 125 | public fun authSchemes(defaultScheme: String = "Bearer", vararg additionalSchemes: String) { 126 | schemes = JWTAuthSchemes(defaultScheme, *additionalSchemes) 127 | } 128 | 129 | /** 130 | * Provides a [Verifier] used to verify a token format and signature. 131 | * 132 | * @param [verifier] verifies token format and signature 133 | */ 134 | public fun verifier(verifier: Verifier) { 135 | this.verifier = { verifier } 136 | } 137 | 138 | /** Provides a [JWTVerifier] used to verify a token format and signature. */ 139 | public fun verifier(verifier: (HttpAuthHeader) -> Verifier?) { 140 | this.verifier = verifier 141 | } 142 | 143 | /** 144 | * Provides a [Verifier] used to verify a token format and signature. 145 | * 146 | * @param [issuer] of the JSON Web Token 147 | * @param [audience] restriction 148 | */ 149 | public fun verifier(issuer: String, audience: String, block: Verifier.() -> Unit = {}) { 150 | val verification: Verifier = jwtVerifier { 151 | audience(audience) 152 | issuer(issuer) 153 | block() 154 | } 155 | 156 | verifier(verification) 157 | } 158 | 159 | /** 160 | * Allows you to perform additional validations on the JWT payload. 161 | * 162 | * @return a principal (usually an instance of [JWTPrincipal]) or `null` 163 | */ 164 | public fun validate(validate: suspend ApplicationCall.(JWTCredential) -> Any?) { 165 | authenticationFunction = validate 166 | } 167 | 168 | /** Specifies what to send back if JWT authentication fails. */ 169 | public fun challenge(block: JWTAuthChallengeFunction) { 170 | challenge = block 171 | } 172 | 173 | internal fun build() = JWTAuthenticationProvider(this) 174 | } 175 | } 176 | 177 | /** 178 | * Installs the JWT [Authentication] provider. JWT (JSON Web Token) is an open standard that defines 179 | * a way for securely transmitting information between parties as a JSON object. To learn how to 180 | * configure it, see [JSON Web Tokens](https://ktor.io/docs/jwt.html). 181 | */ 182 | public fun AuthenticationConfig.jwt( 183 | name: String? = null, 184 | configure: JWTAuthenticationProvider.Config.() -> Unit 185 | ) { 186 | val provider = JWTAuthenticationProvider.Config(name).apply(configure).build() 187 | register(provider) 188 | } 189 | 190 | /** A context for [JWTAuthChallengeFunction]. */ 191 | public class JWTChallengeContext(public val call: ApplicationCall) 192 | 193 | /** Specifies what to send back if JWT authentication fails. */ 194 | public typealias JWTAuthChallengeFunction = 195 | suspend JWTChallengeContext.(defaultScheme: String, realm: String) -> Unit 196 | -------------------------------------------------------------------------------- /ktor-server-auth-jwt/src/commonMain/kotlin/io/ktor/server/auth/jwt/JWTAuthSchemes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package io.ktor.server.auth.jwt 6 | 7 | internal class JWTAuthSchemes(val defaultScheme: String, vararg additionalSchemes: String) { 8 | val schemes = (arrayOf(defaultScheme) + additionalSchemes).toSet() 9 | val schemesLowerCase = schemes.map { it.lowercase() }.toSet() 10 | 11 | operator fun contains(scheme: String): Boolean = scheme.lowercase() in schemesLowerCase 12 | } 13 | -------------------------------------------------------------------------------- /ktor-server-auth-jwt/src/commonMain/kotlin/io/ktor/server/auth/jwt/JWTUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package io.ktor.server.auth.jwt 6 | 7 | import com.appstractive.jwt.JWT 8 | import com.appstractive.jwt.Verifier 9 | import com.appstractive.jwt.from 10 | import com.appstractive.jwt.verify 11 | import io.ktor.http.auth.HttpAuthHeader 12 | import io.ktor.server.application.ApplicationCall 13 | import io.ktor.server.auth.AuthenticationContext 14 | import io.ktor.server.auth.AuthenticationFailedCause 15 | import io.ktor.server.auth.parseAuthorizationHeader 16 | import io.ktor.server.request.ApplicationRequest 17 | 18 | internal fun AuthenticationContext.bearerChallenge( 19 | cause: AuthenticationFailedCause, 20 | realm: String, 21 | schemes: JWTAuthSchemes, 22 | challengeFunction: JWTAuthChallengeFunction 23 | ) { 24 | challenge(JWTAuthKey, cause) { challenge, call -> 25 | challengeFunction(JWTChallengeContext(call), schemes.defaultScheme, realm) 26 | if (!challenge.completed && call.response.status() != null) { 27 | challenge.complete() 28 | } 29 | } 30 | } 31 | 32 | internal suspend fun verifyAndValidate( 33 | call: ApplicationCall, 34 | jwtVerifier: Verifier?, 35 | token: HttpAuthHeader, 36 | schemes: JWTAuthSchemes, 37 | validate: suspend ApplicationCall.(JWTCredential) -> Any? 38 | ): Any? { 39 | val jwt = token.getBlob(schemes)?.let { JWT.from(it) } ?: return null 40 | 41 | jwtVerifier?.let { verifier -> 42 | if (!jwt.verify(verifier)) { 43 | return null 44 | } 45 | } 46 | 47 | val credentials = JWTCredential(jwt.claims) 48 | return validate(call, credentials) 49 | } 50 | 51 | internal fun HttpAuthHeader.getBlob(schemes: JWTAuthSchemes) = 52 | when { 53 | this is HttpAuthHeader.Single && authScheme in schemes -> blob 54 | else -> null 55 | } 56 | 57 | internal fun ApplicationRequest.parseAuthorizationHeaderOrNull() = 58 | try { 59 | parseAuthorizationHeader() 60 | } catch (cause: IllegalArgumentException) { 61 | null 62 | } 63 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | google() 6 | gradlePluginPortal() 7 | mavenCentral() 8 | } 9 | } 10 | 11 | dependencyResolutionManagement { 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | rootProject.name = "jwt-kotlin-multiplatform" 19 | 20 | include( 21 | ":jwt", 22 | ":jwt-hmac", 23 | ":jwt-rsa", 24 | ":jwt-ecdsa", 25 | ":jwt-jwks", 26 | ":ktor-server-auth-jwt", 27 | ":examples:ktor-server") 28 | 29 | project(":jwt").name = "jwt-kt" 30 | 31 | project(":jwt-hmac").name = "jwt-hmac-kt" 32 | 33 | project(":jwt-rsa").name = "jwt-rsa-kt" 34 | 35 | project(":jwt-ecdsa").name = "jwt-ecdsa-kt" 36 | 37 | project(":jwt-jwks").name = "jwt-jwks-kt" 38 | --------------------------------------------------------------------------------