├── .gitignore
├── .gitattributes
├── tests
├── server-2016.json
├── server-2018.json
├── sampleJWK
│ ├── sampleECPrivate.json
│ ├── sampleEC384Private.json
│ ├── sampleECPublic.json
│ ├── sampleEC384Public.json
│ ├── sampleRSAPublic.json
│ └── sampleRSAPrivate.json
├── sampleTokens
│ ├── ES256Token.txt
│ ├── ES384Token.txt
│ └── RS512Token.txt
├── server.json
├── server-java8.json
├── sampleKeys
│ ├── sampleEC.pub
│ ├── sampleEC384.pub
│ ├── unsupported
│ │ ├── sampleEC.key
│ │ ├── sampleECWithParams.key
│ │ └── sampleRSA.key
│ ├── sampleEC.key
│ ├── sampleEC384.key
│ ├── sampleRSA.pub
│ ├── sampleEC.crt
│ ├── sampleRSA.crt
│ └── sampleRSA.key
├── Application.cfc
├── index.cfm
└── specs
│ ├── encodingUtilsSpec.cfc
│ └── jwtSpec.cfc
├── ModuleConfig.cfc
├── LICENSE
├── box.json
├── .cfformat.json
├── README.md
└── models
├── jwt.cfc
└── encodingUtils.cfc
/.gitignore:
--------------------------------------------------------------------------------
1 | testbox/
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/tests/server-2016.json:
--------------------------------------------------------------------------------
1 | {
2 | "app":{
3 | "cfengine":"adobe@2016"
4 | },
5 | "name":"jwtcfml-cf2016"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/server-2018.json:
--------------------------------------------------------------------------------
1 | {
2 | "app":{
3 | "cfengine":"adobe@2018"
4 | },
5 | "name":"jwtcfml-cf2018"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/sampleJWK/sampleECPrivate.json:
--------------------------------------------------------------------------------
1 | {
2 | "crv": "P-256",
3 | "kty": "EC",
4 | "d": "ao0goNJKSAJOXfKbYmgS5UM4CpF5-40TmGcAMo1koQ4"
5 | }
6 |
--------------------------------------------------------------------------------
/tests/sampleJWK/sampleEC384Private.json:
--------------------------------------------------------------------------------
1 | {
2 | "crv": "P-384",
3 | "kty": "EC",
4 | "d": "AOMh3Is6yjedOi6TS8maYRTk2fFOKoFE4f7aqVxxw2fxbc1PDaLRb_YTZeBTOU8gyw"
5 | }
6 |
--------------------------------------------------------------------------------
/tests/sampleTokens/ES256Token.txt:
--------------------------------------------------------------------------------
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.A8tObqrWfNuG4Ln382_mr7U8728xt26F5Shj47x85eKu3BLsduNDwnMXBlVuewZ1ibCx0jtuKE8e1LDyXMiH6A
2 |
--------------------------------------------------------------------------------
/tests/server.json:
--------------------------------------------------------------------------------
1 | {
2 | "app":{
3 | "cfengine":"lucee@5"
4 | },
5 | "jvm":{
6 | "javaVersion":"openjdk11"
7 | },
8 | "name":"jwtcfml"
9 | }
10 |
--------------------------------------------------------------------------------
/tests/server-java8.json:
--------------------------------------------------------------------------------
1 | {
2 | "app":{
3 | "cfengine":"lucee@5"
4 | },
5 | "jvm":{
6 | "javaVersion":"openjdk8"
7 | },
8 | "name":"jwtcfml-java8"
9 | }
10 |
--------------------------------------------------------------------------------
/tests/sampleJWK/sampleECPublic.json:
--------------------------------------------------------------------------------
1 | {
2 | "crv": "P-256",
3 | "kty": "EC",
4 | "x": "ANxLaruRUsNrYI9MiiH-oK9OQAH2O5tJ72Y5zRSzyrS1",
5 | "y": "AJ-raaFowSNRVNpr4sR8lfSXNX7dYOqSYQSAGhuZGGTD"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/sampleKeys/sampleEC.pub:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3Etqu5FSw2tgj0yKIf6gr05AAfY7
3 | m0nvZjnNFLPKtLWfq2mhaMEjUVTaa+LEfJX0lzV+3WDqkmEEgBobmRhkww==
4 | -----END PUBLIC KEY-----
5 |
--------------------------------------------------------------------------------
/tests/sampleTokens/ES384Token.txt:
--------------------------------------------------------------------------------
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.uoMh_N27Uk0iMJf4PLUTGGia9CWhm_oRWcw3DTt313_vzlFbx4pWtvVtKRaGINdn3htARw3qwBknDLW-TwVY3BIwJRANs9JEXaOVjlIBlXu4UkT3XBsCgD6u1P3TUxmO
2 |
--------------------------------------------------------------------------------
/tests/sampleJWK/sampleEC384Public.json:
--------------------------------------------------------------------------------
1 | {
2 | "crv": "P-384",
3 | "kty": "EC",
4 | "x": "bYtC5AenYjyswReNKf92ZylzSQ3oTDRKuEhsYvpsuyMJNpRLCWyOnXw7RlTwvgpx",
5 | "y": "VGYjCX0Z0LCRj1VmEc08DxPp7ln-vm_eb8wK-xbnACWiDA73ZMlodv3Foo7Tcaxv"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/sampleKeys/sampleEC384.pub:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbYtC5AenYjyswReNKf92ZylzSQ3oTDRK
3 | uEhsYvpsuyMJNpRLCWyOnXw7RlTwvgpxVGYjCX0Z0LCRj1VmEc08DxPp7ln+vm/e
4 | b8wK+xbnACWiDA73ZMlodv3Foo7Tcaxv
5 | -----END PUBLIC KEY-----
6 |
--------------------------------------------------------------------------------
/tests/sampleKeys/unsupported/sampleEC.key:
--------------------------------------------------------------------------------
1 | -----BEGIN EC PRIVATE KEY-----
2 | MHcCAQEEILjLYxXi532Ylm1BH3prwm+BH3WydlVMwe1zd0wWjPrcoAoGCCqGSM49
3 | AwEHoUQDQgAEmAFJznwVEzPgU6G4IzMzIBS7A9E6vDNp7hSnmaFl27zK1AdYlBWP
4 | vX7BiwRUAkM4VsYYt2G+LqqVuj7tIrgDew==
5 | -----END EC PRIVATE KEY-----
6 |
--------------------------------------------------------------------------------
/tests/sampleKeys/sampleEC.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgao0goNJKSAJOXfKb
3 | YmgS5UM4CpF5+40TmGcAMo1koQ6hRANCAATcS2q7kVLDa2CPTIoh/qCvTkAB9jub
4 | Se9mOc0Us8q0tZ+raaFowSNRVNpr4sR8lfSXNX7dYOqSYQSAGhuZGGTD
5 | -----END PRIVATE KEY-----
6 |
--------------------------------------------------------------------------------
/ModuleConfig.cfc:
--------------------------------------------------------------------------------
1 | component {
2 |
3 | this.title = 'jwt-cfml';
4 | this.author = 'John Berquist';
5 | this.webURL = 'https://github.com/jcberquist/jwt-cfml';
6 | this.description = 'This module supports encoding and decoding JSON Web Tokens.';
7 |
8 | function configure() {}
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/tests/sampleKeys/sampleEC384.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDjIdyLOso3nTouk0vJ
3 | mmEU5NnxTiqBROH+2qlcccNn8W3NTw2i0W/2E2XgUzlPIMuhZANiAARti0LkB6di
4 | PKzBF40p/3ZnKXNJDehMNEq4SGxi+my7Iwk2lEsJbI6dfDtGVPC+CnFUZiMJfRnQ
5 | sJGPVWYRzTwPE+nuWf6+b95vzAr7FucAJaIMDvdkyWh2/cWijtNxrG8=
6 | -----END PRIVATE KEY-----
7 |
--------------------------------------------------------------------------------
/tests/sampleKeys/unsupported/sampleECWithParams.key:
--------------------------------------------------------------------------------
1 | -----BEGIN EC PARAMETERS-----
2 | BggqhkjOPQMBBw==
3 | -----END EC PARAMETERS-----
4 | -----BEGIN EC PRIVATE KEY-----
5 | MHcCAQEEIGqNIKDSSkgCTl3ym2JoEuVDOAqRefuNE5hnADKNZKEOoAoGCCqGSM49
6 | AwEHoUQDQgAE3Etqu5FSw2tgj0yKIf6gr05AAfY7m0nvZjnNFLPKtLWfq2mhaMEj
7 | UVTaa+LEfJX0lzV+3WDqkmEEgBobmRhkww==
8 | -----END EC PRIVATE KEY-----
9 |
--------------------------------------------------------------------------------
/tests/sampleTokens/RS512Token.txt:
--------------------------------------------------------------------------------
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.ztKnr2euWi8g-ag6S0omMKSBsXwDXY3_0THc8UVKZ65xMgHEY6ZadXS7ksn82uZyBb9FnhBIcaMXXk0JrMUShZI26zK_dJHaHItCiMqMQlTVBojV5FUQFuia5EcSCYJpOryoh94UsRPOswv0yE2ylw0sUiYiA8befWUsRlWPJu7_RcOzsC7o60p-4znLiT1sta9KYlvA2bF5tDr8ale-RRMyUeua-4dNsAB2JdZEIKuK2GiCdiug1Km6HWW2vsRpVcal_r4C8EWzGlIUJyJ8Y07DOSQDrgHkH-JETQiTKXCEjC79kST9gk2YBoVuVxaCz83EiaA16ol3YkTCAJ6Y-w
2 |
--------------------------------------------------------------------------------
/tests/Application.cfc:
--------------------------------------------------------------------------------
1 | component {
2 |
3 | rootPath = getDirectoryFromPath( getCurrentTemplatePath() ).replace( '\', '/', 'all' ).replaceNoCase( 'tests/', '' );
4 |
5 | this.mappings[ '/testbox' ] = rootPath & '/testbox';
6 | this.mappings[ '/models' ] = rootPath & '/models';
7 |
8 | public boolean function onRequestStart( String targetPage ) {
9 | setting requestTimeout="9999";
10 | return true;
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/tests/sampleJWK/sampleRSAPublic.json:
--------------------------------------------------------------------------------
1 | {
2 | "e": "AQAB",
3 | "kty": "RSA",
4 | "alg": "RS256",
5 | "n": "ANzdh51XtFOGyDLtjRT3TUrwWW7NtycbvL_U7PRLN5fv7oNbzx0iwr9tqX_YqrtQFhHgHdtcsXPWLJ_5SK5UQgdqDdWy5dD6GL1yZ98t3kvEDQsd2gniLiaF8lEHoh9G-Uume-PLtsMSrzagspDGMaLHb37L6Wk55tktGvG8QNKGdDR5wOzB2h02fXxfXROQq9mVm3Rn_6UZP2o46i6-g3Zp304VeCWjXRmy8saxEt1pAVY8LlAL65YTYSqjrkwd1UiY7chR2_AQmJ5FkFweBoPafmbGLUWAM5juTNcuwf5x5mBIzddBz4S9sCaPBb8BYjVw3WUTBW_R7q1xCNr63Cc",
6 | "use": "sig"
7 | }
8 |
--------------------------------------------------------------------------------
/tests/sampleKeys/sampleRSA.pub:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3N2HnVe0U4bIMu2NFPdN
3 | SvBZbs23Jxu8v9Ts9Es3l+/ug1vPHSLCv22pf9iqu1AWEeAd21yxc9Ysn/lIrlRC
4 | B2oN1bLl0PoYvXJn3y3eS8QNCx3aCeIuJoXyUQeiH0b5S6Z748u2wxKvNqCykMYx
5 | osdvfsvpaTnm2S0a8bxA0oZ0NHnA7MHaHTZ9fF9dE5Cr2ZWbdGf/pRk/ajjqLr6D
6 | dmnfThV4JaNdGbLyxrES3WkBVjwuUAvrlhNhKqOuTB3VSJjtyFHb8BCYnkWQXB4G
7 | g9p+ZsYtRYAzmO5M1y7B/nHmYEjN10HPhL2wJo8FvwFiNXDdZRMFb9HurXEI2vrc
8 | JwIDAQAB
9 | -----END PUBLIC KEY-----
10 |
--------------------------------------------------------------------------------
/tests/sampleKeys/sampleEC.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIBgjCCASmgAwIBAgIUdorHUF77VCfTuzOEBszNueKCOGswCgYIKoZIzj0EAwIw
3 | FzEVMBMGA1UEAwwMc2FtcGxlRUMuY3J0MB4XDTE5MDgxMjIzMTUyMloXDTIxMDgx
4 | MTIzMTUyMlowFzEVMBMGA1UEAwwMc2FtcGxlRUMuY3J0MFkwEwYHKoZIzj0CAQYI
5 | KoZIzj0DAQcDQgAE3Etqu5FSw2tgj0yKIf6gr05AAfY7m0nvZjnNFLPKtLWfq2mh
6 | aMEjUVTaa+LEfJX0lzV+3WDqkmEEgBobmRhkw6NTMFEwHQYDVR0OBBYEFEjIIzTU
7 | 0SLrLJKl9o14jQH/1lJcMB8GA1UdIwQYMBaAFEjIIzTU0SLrLJKl9o14jQH/1lJc
8 | MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgPJvHVJw4oemer7i/
9 | lHCSDKLw1IEGtwhR8/AFjyCLo9YCIH4USuW7jrnBdXSZnW/GR51ew0wYxeWDVNwN
10 | Aq1OWcre
11 | -----END CERTIFICATE-----
12 |
--------------------------------------------------------------------------------
/tests/index.cfm:
--------------------------------------------------------------------------------
1 |
2 |
3 | testbox = new testbox.system.TestBox();
4 | param name="url.reporter" default="simple";
5 | param name="url.directory" default="specs";
6 | args = {
7 | reporter: url.reporter,
8 | directory: url.directory
9 | };
10 | if ( structKeyExists( url, 'bundles' ) ) args.bundles = url.bundles;
11 | results = testBox.run( argumentCollection = args );
12 |
13 |
14 |
15 |
16 |
17 | #server.coldfusion.productname##structKeyExists( server, 'lucee' ) ? server.lucee.version : server.coldfusion.productversion#
18 | Java #createObject( 'java', 'java.lang.System' ).getProperty( 'java.version' )#
19 |
20 | #trim( results )#
21 |
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/sampleKeys/sampleRSA.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDETCCAfmgAwIBAgIUF2ktIPZMoxVF+Zqutw/1SiV8tcMwDQYJKoZIhvcNAQEL
3 | BQAwGDEWMBQGA1UEAwwNc2FtcGxlUlNBLmNydDAeFw0xOTA4MTIyMzE0MTNaFw0y
4 | MTA4MTEyMzE0MTNaMBgxFjAUBgNVBAMMDXNhbXBsZVJTQS5jcnQwggEiMA0GCSqG
5 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDc3YedV7RThsgy7Y0U901K8FluzbcnG7y/
6 | 1Oz0SzeX7+6DW88dIsK/bal/2Kq7UBYR4B3bXLFz1iyf+UiuVEIHag3VsuXQ+hi9
7 | cmffLd5LxA0LHdoJ4i4mhfJRB6IfRvlLpnvjy7bDEq82oLKQxjGix29+y+lpOebZ
8 | LRrxvEDShnQ0ecDswdodNn18X10TkKvZlZt0Z/+lGT9qOOouvoN2ad9OFXglo10Z
9 | svLGsRLdaQFWPC5QC+uWE2Eqo65MHdVImO3IUdvwEJieRZBcHgaD2n5mxi1FgDOY
10 | 7kzXLsH+ceZgSM3XQc+EvbAmjwW/AWI1cN1lEwVv0e6tcQja+twnAgMBAAGjUzBR
11 | MB0GA1UdDgQWBBShD+I6pR+ylK5pnS3sM2Di0FCwiTAfBgNVHSMEGDAWgBShD+I6
12 | pR+ylK5pnS3sM2Di0FCwiTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA
13 | A4IBAQAZdepAovemmC74+ErbadHZcp0qFV/QcFx3vBaMRAwY8UMYhzeK0ugD3JNU
14 | D0SVLd0ZvrcyB1oZfmMACHaSUyyWSCRmzybF4kGmsV0ubYwJqlJtrsTqDKdZmTDA
15 | ItFE7q97lIO0k5P2JS/wJaCJ7T6+9DuSQzDfq5R8/Yx+CW/7rL7++P1cF7c0matN
16 | hM8AbKHJ4wuLHcHCO8B7Ai1PhQ7YrIL8TZho4JfToNqbmMNvFGvpA5n+lDLi228l
17 | mvoyv/Ae3OMmpQt1oBGy6TJzC2KnXCBy1hQ+7hSH7EFMlVjDtj5L2Ak3NX0jqmi9
18 | RwY7TV32DfKx4SGOSumuh33d0jY9
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/box.json:
--------------------------------------------------------------------------------
1 | {
2 | "name":"JWT CFML",
3 | "version":"1.2.1",
4 | "author":"John Berquist",
5 | "location":"forgeboxStorage",
6 | "homepage":"https://github.com/jcberquist/jwt-cfml",
7 | "documentation":"",
8 | "repository":{
9 | "type":"",
10 | "URL":""
11 | },
12 | "bugs":"",
13 | "slug":"jwt-cfml",
14 | "packageDirectory": "jwtcfml",
15 | "shortDescription":"JWT CFML is a CFML (Lucee and ColdFusion) library for using JSON Web Tokens.",
16 | "description":"",
17 | "instructions":"",
18 | "changelog":"",
19 | "type":"modules",
20 | "keywords":[
21 | "jwt"
22 | ],
23 | "private":false,
24 | "projectURL":"",
25 | "license":[
26 | {
27 | "type":"MIT",
28 | "URL":""
29 | }
30 | ],
31 | "contributors":[],
32 | "dependencies":{},
33 | "devDependencies":{
34 | "testbox":"^4.0.0"
35 | },
36 | "installPaths":{
37 | "testbox":"testbox/"
38 | },
39 | "ignore":[
40 | "**/.*",
41 | "tests",
42 | "**/*.md"
43 | ],
44 | "scripts": {
45 | "tests": "start tests/server.json && start tests/server-java8.json && start tests/server-2018.json && start tests/server-2016.json",
46 | "format": "cfformat run models,tests --overwrite"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/sampleJWK/sampleRSAPrivate.json:
--------------------------------------------------------------------------------
1 | {
2 | "alg": "RS256",
3 | "d": "cIz_j-HixfHYUYOWsol3vOmQWZPBcs-CtysVeURfVzDwlcYSXGGbZpvGlZzfHEcqh_9yl5e74jDRWtBZBmVvpIGZ_T2GOaFJPDlxz1x7fJayouiadRDqvbziiAJgzpmHbtKvLZ1hTDVYTnlDpd0p7C6_lZjqIDJefmuq4GczjVP-q76qrL6wzz54WTI22iMk0IB528uEABDKfWY-RKDtdxVokl8dTXMXMRrT6HIq1y1H9TOLhYSROHHM9npGybC-w6cBA7Q0dpUlVvdBXt78FErWJOkjx5zBm36QHcEee5H8pTY6Blu8YYCfxYZJxZDhJ7F1VcFxKT3tS7JTzHRbiQ",
4 | "dp": "AKX9zQDeNnAYZeHWesmUVu5FmNokFdQdlWEp8OGu4cFBjOED3YYsbaX32ynYzFgk47uLaCQj_QKfOPEh79nJhmqZG8G7UfXkqFtF_9Vd2bERyiz1qq6spDjCCPyMmTWupjidHbgWPJK9hzWnlMclfS_deC06L545jsbTqhvR4GHZ",
5 | "dq": "ANWK5PBrS-7y-lVJfBaIVATrokBnamgcWkEVxAI-W7WPE6p84MCUCy6d3xLYLokWmL1IfJUd50uq_u5ow9UxFOZZxP2E_TxGHT1Q1aedXZ_LnYwh9SbcGp2qg0qseqqHLgrqD8hFVttvAlGo1pkxO7ctcIp_KbpP0mFWSG18FvU",
6 | "e": "AQAB",
7 | "kty": "RSA",
8 | "n": "ANzdh51XtFOGyDLtjRT3TUrwWW7NtycbvL_U7PRLN5fv7oNbzx0iwr9tqX_YqrtQFhHgHdtcsXPWLJ_5SK5UQgdqDdWy5dD6GL1yZ98t3kvEDQsd2gniLiaF8lEHoh9G-Uume-PLtsMSrzagspDGMaLHb37L6Wk55tktGvG8QNKGdDR5wOzB2h02fXxfXROQq9mVm3Rn_6UZP2o46i6-g3Zp304VeCWjXRmy8saxEt1pAVY8LlAL65YTYSqjrkwd1UiY7chR2_AQmJ5FkFweBoPafmbGLUWAM5juTNcuwf5x5mBIzddBz4S9sCaPBb8BYjVw3WUTBW_R7q1xCNr63Cc",
9 | "p": "APUfmtaaCM3mm4cqmOKTiN0dsMOlLsVj2PPWHENH5MYshWEe1z2vxcbDcE7vH3Rd1ssWbPNDootjWg7ImzarQJs8SA09JJQT1UR-4DN8kXLnH1mAQ2ZH3hEoFrI0mDSnHthPUSIaMTLAKsAaRYk7Hbn23PeLsQuFO17G2jnMhjd7",
10 | "q": "AOaqX5M90wOBhj7vmcoCnCPqBZYvKwCRkA6DzVVX491YV7yPK3dPtkoqLzFhfbXCkrd_tD44sZfXRLKqVqzHvSDfPNuWHw9oEurcbzm7-LJm3_cWJhz3oooDYStoh2NiIQWApxkz-rzIxFLQpHbaAyQW2ePYy1ByAuhoaZo2kThF",
11 | "qi": "AMrKrgN9RzxggKOcbe-mKOx2SNLw9SBDN_FWOsXJw_HLXW2uToImf7RIVyThhKUYdpwJMCjFsrDsKjcYIVVhsiV2FxNqtYUi-jf3-ec4muRTmFERwhAZiDD-dPCHZ2W748K3a4WGH7ybQ3u9dOU1TGc-zWOO-3hhXd09U1EaWd4U"
12 | }
13 |
--------------------------------------------------------------------------------
/tests/sampleKeys/unsupported/sampleRSA.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEA3N2HnVe0U4bIMu2NFPdNSvBZbs23Jxu8v9Ts9Es3l+/ug1vP
3 | HSLCv22pf9iqu1AWEeAd21yxc9Ysn/lIrlRCB2oN1bLl0PoYvXJn3y3eS8QNCx3a
4 | CeIuJoXyUQeiH0b5S6Z748u2wxKvNqCykMYxosdvfsvpaTnm2S0a8bxA0oZ0NHnA
5 | 7MHaHTZ9fF9dE5Cr2ZWbdGf/pRk/ajjqLr6DdmnfThV4JaNdGbLyxrES3WkBVjwu
6 | UAvrlhNhKqOuTB3VSJjtyFHb8BCYnkWQXB4Gg9p+ZsYtRYAzmO5M1y7B/nHmYEjN
7 | 10HPhL2wJo8FvwFiNXDdZRMFb9HurXEI2vrcJwIDAQABAoIBAHCM/4/h4sXx2FGD
8 | lrKJd7zpkFmTwXLPgrcrFXlEX1cw8JXGElxhm2abxpWc3xxHKof/cpeXu+Iw0VrQ
9 | WQZlb6SBmf09hjmhSTw5cc9ce3yWsqLomnUQ6r284ogCYM6Zh27Sry2dYUw1WE55
10 | Q6XdKewuv5WY6iAyXn5rquBnM41T/qu+qqy+sM8+eFkyNtojJNCAedvLhAAQyn1m
11 | PkSg7XcVaJJfHU1zFzEa0+hyKtctR/Uzi4WEkThxzPZ6RsmwvsOnAQO0NHaVJVb3
12 | QV7e/BRK1iTpI8ecwZt+kB3BHnuR/KU2OgZbvGGAn8WGScWQ4SexdVXBcSk97Uuy
13 | U8x0W4kCgYEA9R+a1poIzeabhyqY4pOI3R2ww6UuxWPY89YcQ0fkxiyFYR7XPa/F
14 | xsNwTu8fdF3WyxZs80Oii2NaDsibNqtAmzxIDT0klBPVRH7gM3yRcucfWYBDZkfe
15 | ESgWsjSYNKce2E9RIhoxMsAqwBpFiTsdufbc94uxC4U7XsbaOcyGN3sCgYEA5qpf
16 | kz3TA4GGPu+ZygKcI+oFli8rAJGQDoPNVVfj3VhXvI8rd0+2SiovMWF9tcKSt3+0
17 | Pjixl9dEsqpWrMe9IN8825YfD2gS6txvObv4smbf9xYmHPeiigNhK2iHY2IhBYCn
18 | GTP6vMjEUtCkdtoDJBbZ49jLUHIC6GhpmjaROEUCgYEApf3NAN42cBhl4dZ6yZRW
19 | 7kWY2iQV1B2VYSnw4a7hwUGM4QPdhixtpffbKdjMWCTju4toJCP9Ap848SHv2cmG
20 | apkbwbtR9eSoW0X/1V3ZsRHKLPWqrqykOMII/IyZNa6mOJ0duBY8kr2HNaeUxyV9
21 | L914LTovnjmOxtOqG9HgYdkCgYAA1Yrk8GtL7vL6VUl8FohUBOuiQGdqaBxaQRXE
22 | Aj5btY8TqnzgwJQLLp3fEtguiRaYvUh8lR3nS6r+7mjD1TEU5lnE/YT9PEYdPVDV
23 | p51dn8udjCH1JtwanaqDSqx6qocuCuoPyEVW228CUajWmTE7ty1win8puk/SYVZI
24 | bXwW9QKBgQDKyq4DfUc8YICjnG3vpijsdkjS8PUgQzfxVjrFycPxy11trk6CJn+0
25 | SFck4YSlGHacCTAoxbKw7Co3GCFVYbIldhcTarWFIvo39/nnOJrkU5hREcIQGYgw
26 | /nTwh2dlu+PCt2uFhh+8m0N7vXTlNUxnPs1jjvt4YV3dPVNRGlneFA==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/tests/sampleKeys/sampleRSA.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDc3YedV7RThsgy
3 | 7Y0U901K8FluzbcnG7y/1Oz0SzeX7+6DW88dIsK/bal/2Kq7UBYR4B3bXLFz1iyf
4 | +UiuVEIHag3VsuXQ+hi9cmffLd5LxA0LHdoJ4i4mhfJRB6IfRvlLpnvjy7bDEq82
5 | oLKQxjGix29+y+lpOebZLRrxvEDShnQ0ecDswdodNn18X10TkKvZlZt0Z/+lGT9q
6 | OOouvoN2ad9OFXglo10ZsvLGsRLdaQFWPC5QC+uWE2Eqo65MHdVImO3IUdvwEJie
7 | RZBcHgaD2n5mxi1FgDOY7kzXLsH+ceZgSM3XQc+EvbAmjwW/AWI1cN1lEwVv0e6t
8 | cQja+twnAgMBAAECggEAcIz/j+HixfHYUYOWsol3vOmQWZPBcs+CtysVeURfVzDw
9 | lcYSXGGbZpvGlZzfHEcqh/9yl5e74jDRWtBZBmVvpIGZ/T2GOaFJPDlxz1x7fJay
10 | ouiadRDqvbziiAJgzpmHbtKvLZ1hTDVYTnlDpd0p7C6/lZjqIDJefmuq4GczjVP+
11 | q76qrL6wzz54WTI22iMk0IB528uEABDKfWY+RKDtdxVokl8dTXMXMRrT6HIq1y1H
12 | 9TOLhYSROHHM9npGybC+w6cBA7Q0dpUlVvdBXt78FErWJOkjx5zBm36QHcEee5H8
13 | pTY6Blu8YYCfxYZJxZDhJ7F1VcFxKT3tS7JTzHRbiQKBgQD1H5rWmgjN5puHKpji
14 | k4jdHbDDpS7FY9jz1hxDR+TGLIVhHtc9r8XGw3BO7x90XdbLFmzzQ6KLY1oOyJs2
15 | q0CbPEgNPSSUE9VEfuAzfJFy5x9ZgENmR94RKBayNJg0px7YT1EiGjEywCrAGkWJ
16 | Ox259tz3i7ELhTtexto5zIY3ewKBgQDmql+TPdMDgYY+75nKApwj6gWWLysAkZAO
17 | g81VV+PdWFe8jyt3T7ZKKi8xYX21wpK3f7Q+OLGX10Syqlasx70g3zzblh8PaBLq
18 | 3G85u/iyZt/3FiYc96KKA2EraIdjYiEFgKcZM/q8yMRS0KR22gMkFtnj2MtQcgLo
19 | aGmaNpE4RQKBgQCl/c0A3jZwGGXh1nrJlFbuRZjaJBXUHZVhKfDhruHBQYzhA92G
20 | LG2l99sp2MxYJOO7i2gkI/0CnzjxIe/ZyYZqmRvBu1H15KhbRf/VXdmxEcos9aqu
21 | rKQ4wgj8jJk1rqY4nR24FjySvYc1p5THJX0v3XgtOi+eOY7G06ob0eBh2QKBgADV
22 | iuTwa0vu8vpVSXwWiFQE66JAZ2poHFpBFcQCPlu1jxOqfODAlAsund8S2C6JFpi9
23 | SHyVHedLqv7uaMPVMRTmWcT9hP08Rh09UNWnnV2fy52MIfUm3BqdqoNKrHqqhy4K
24 | 6g/IRVbbbwJRqNaZMTu3LXCKfym6T9JhVkhtfBb1AoGBAMrKrgN9RzxggKOcbe+m
25 | KOx2SNLw9SBDN/FWOsXJw/HLXW2uToImf7RIVyThhKUYdpwJMCjFsrDsKjcYIVVh
26 | siV2FxNqtYUi+jf3+ec4muRTmFERwhAZiDD+dPCHZ2W748K3a4WGH7ybQ3u9dOU1
27 | TGc+zWOO+3hhXd09U1EaWd4U
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/.cfformat.json:
--------------------------------------------------------------------------------
1 | {
2 | "array.empty_padding": true,
3 | "array.multiline.element_count": 4,
4 | "array.multiline.leading_comma": false,
5 | "array.multiline.leading_comma.padding": true,
6 | "array.multiline.min_length": 80,
7 | "array.padding": true,
8 | "binary_operators.padding": true,
9 | "brackets.padding": true,
10 | "comment.asterisks": "indent",
11 | "for_loop_semicolons.padding": true,
12 | "function_anonymous.empty_padding": false,
13 | "function_anonymous.group_to_block_spacing": "spaced",
14 | "function_anonymous.multiline.element_count": 4,
15 | "function_anonymous.multiline.leading_comma": false,
16 | "function_anonymous.multiline.leading_comma.padding": true,
17 | "function_anonymous.multiline.min_length": 40,
18 | "function_anonymous.padding": true,
19 | "function_call.empty_padding": false,
20 | "function_call.multiline.element_count": 4,
21 | "function_call.multiline.leading_comma": false,
22 | "function_call.multiline.leading_comma.padding": true,
23 | "function_call.multiline.min_length": 40,
24 | "function_call.padding": true,
25 | "function_declaration.empty_padding": false,
26 | "function_declaration.group_to_block_spacing": "spaced",
27 | "function_declaration.multiline.element_count": 4,
28 | "function_declaration.multiline.leading_comma": false,
29 | "function_declaration.multiline.leading_comma.padding": true,
30 | "function_declaration.multiline.min_length": 40,
31 | "function_declaration.padding": true,
32 | "indent_size": 4,
33 | "keywords.block_to_keyword_spacing": "spaced",
34 | "keywords.empty_group_spacing": false,
35 | "keywords.group_to_block_spacing": "spaced",
36 | "keywords.padding_inside_group": true,
37 | "keywords.spacing_to_block": "spaced",
38 | "keywords.spacing_to_group": true,
39 | "max_columns": 120,
40 | "parentheses.padding": true,
41 | "strings.attributes.quote": "double",
42 | "strings.quote": "single",
43 | "struct.empty_padding": true,
44 | "struct.multiline.element_count": 0,
45 | "struct.multiline.leading_comma": false,
46 | "struct.multiline.leading_comma.padding": true,
47 | "struct.multiline.min_length": 0,
48 | "struct.padding": true,
49 | "struct.separator": ": ",
50 | "tab_indent": false
51 | }
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # jwt-cfml
2 |
3 | **jwt-cfml** is a CFML (Lucee and ColdFusion) library for encoding and decoding JSON Web Tokens.
4 |
5 | It supports the following algorithms:
6 |
7 | - HS256
8 | - HS384
9 | - HS512
10 | - RS256
11 | - RS384
12 | - RS512
13 | - ES256
14 | - ES384
15 | - ES512
16 |
17 | In the case of the `RS` and `ES` algorithms, asymmetric keys are expected to be provided in unencrypted PEM or JWK format (in the latter case first deserialize the JWK to a CFML struct). When using PEM, private keys need to be encoded in PKCS#8 format.
18 |
19 | If your private key is not currently in this format, conversion should be straightforward:
20 |
21 | ```bash
22 | $ openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pk8
23 | ```
24 |
25 | When decoding tokens, either a public key or certificate can be provided. (If a certificate is provided, the public key will be extracted from it.)
26 |
27 | *You can pre-parse your encoded keys and pass the returned Java classes to the `encode()` and `decode()` methods, to avoid having them parsed on every method call. See [Parsing Asymmetric Keys](https://github.com/jcberquist/jwt-cfml-dev/blob/master/README.md#parsing-asymmetric-keys) below.*
28 |
29 | ## Installation
30 |
31 | Installation is done via CommandBox:
32 |
33 | ```bash
34 | $ box install jwt-cfml
35 | ```
36 |
37 | `jwt-cfml` will be installed into a `jwtcfml` package directory by default.
38 |
39 | *Alternatively the git repository can be cloned into the desired directory.*
40 |
41 | ### Standalone
42 |
43 | Once the library has been installed, the core `jwt` component can be instantiated directly:
44 |
45 | ```cfc
46 | jwt = new path.to.jwtcfml.models.jwt();
47 | ```
48 |
49 | ### ColdBox Module
50 |
51 | You can make use of the library via the injection DSL: `jwt@jwtcfml`
52 |
53 | ## Usage
54 |
55 | ### Encoding tokens:
56 |
57 | ```cfc
58 | payload = {'key': 'value'};
59 | secret = 'secret';
60 | token = jwt.encode(payload, secret, 'HS256');
61 | ```
62 |
63 | ```cfc
64 | pemPrivateKey = '
65 | -----BEGIN PRIVATE KEY-----
66 | ...
67 | -----END PRIVATE KEY-----
68 | ';
69 | token = jwt.encode(payload, pemPrivateKey, 'RS256');
70 | ```
71 |
72 | ```cfc
73 | jwk = {
74 | "alg": "RS256",
75 | "d": "...",
76 | "dp": "...",
77 | "dq": "...",
78 | "e": "AQAB",
79 | "kty": "RSA",
80 | "n": "...",
81 | "p": "...",
82 | "q": "...",
83 | "qi": "..."
84 | };
85 | token = jwt.encode(payload, jwk, 'RS256');
86 | ```
87 |
88 | When a token is encoded, a header is automatically included containing
89 | `"typ"` set to `"JWT"` and `"alg"` set to the passed in algorithm. If you
90 | need to add additional headers a fourth argument, `headers`, is available
91 | for this:
92 |
93 | ```cfc
94 | token = jwt.encode(payload, pemPrivateKey, 'RS256', {'kid': 'abc123'});
95 | ```
96 |
97 | If your token payload contains `"iat"`, `"exp"`, or `"nbf"` claims, you can
98 | set these to CFML date objects, and they will automatically be converted to
99 | UNIX timestamps in the generated token for you.
100 |
101 | ```cfc
102 | payload = {'iat': now()};
103 | token = jwt.encode(payload, secret, 'HS256');
104 | ```
105 |
106 | ### Decoding tokens:
107 |
108 | ```cfc
109 | token = 'eyJ0e...';
110 | secret = 'secret';
111 | payload = jwt.decode(token, secret, 'HS256');
112 | ```
113 |
114 | ```cfc
115 | token = 'eyJ0e...';
116 | pemPublicKey = '
117 | -----BEGIN PUBLIC KEY-----
118 | ...
119 | -----END PUBLIC KEY-----
120 | ';
121 | payload = jwt.decode(token, pemPublicKey, 'RS256');
122 | ```
123 |
124 | ```cfc
125 | token = 'eyJ0e...';
126 | pemCertificate = '
127 | -----BEGIN CERTIFICATE-----
128 | ...
129 | -----END CERTIFICATE-----
130 | ';
131 | payload = jwt.decode(token, pemCertificate, 'RS256');
132 | ```
133 |
134 | ```cfc
135 | token = 'eyJ0e...';
136 | jwk = {
137 | "e": "AQAB",
138 | "kty": "RSA",
139 | "alg": "RS256",
140 | "n": "...",
141 | "use": "sig"
142 | }
143 | payload = jwt.decode(token, jwk, 'RS256');
144 | ```
145 |
146 | *Note: This library does not rely solely on the algorithm specified in the token header. You **must** specify the allowed algorithms (either as a string or an array) when calling `decode()`. The algorithm in the token header must match one of the allowed algorithms.*
147 |
148 | If the decoded payload contains `"iat"`, `"exp"`, or `"nbf"` claims, they will be automatically converted from UNIX timestamps to CFML date objects for you.
149 |
150 | #### Getting the token header
151 |
152 | If you need to get the token header before decoding (e.g. you need a `"kid"` from it), you can use the `jwt.getHeader()` method. This will return the token header as a struct.
153 |
154 | ```cfc
155 | token = 'eyJ0e...';
156 | header = jwt.getHeader(token);
157 |
158 | ```
159 |
160 | #### Token validity
161 |
162 | If a token signature is invalid, the `jwt.decode()` method will throw an error. Further, if the payload contains a `"exp"` or `"nbf"` claim these will be verified as well.
163 |
164 | If you also wish to verify an audience or issuer claim, you can pass valid claims into the decode method:
165 |
166 | ```cfc
167 | claims = {
168 | "iss": "somissuer",
169 | "aud": "someaudience" // this can also be an array
170 | };
171 |
172 | payload = jwt.decode(token, pemCertificate, 'RS256', claims);
173 | ```
174 |
175 | This argument can also be used to ignore the `"exp"` and `"nbf"` claims or to validate them against a timestamp other than the current time:
176 |
177 | ```cfc
178 | claims = {
179 | // `exp` will be validated against 1 min in the past instead of the current time
180 | "exp": dateAdd('n', -1, now()),
181 | // `nbf` will be ignored
182 | "nbf": false
183 | };
184 |
185 | payload = jwt.decode(token, pemCertificate, 'RS256', claims);
186 | ```
187 |
188 | #### Unverified Payload
189 |
190 | If you need to get the payload without doing any verification at all you can pass `verify=false` into the decode method:
191 |
192 | ```cfc
193 | jwt.decode(token = token, verify = false);
194 | ```
195 |
196 | ### Parsing Asymmetric Keys
197 |
198 | Every time a PEM key or JWK is passed into `encode()` and `decode()` it must be converted to binary data and then the appropriate Java class created. You can avoid this (minor) overhead by parsing your key upfront, and then passing the generated Java key class directly into `encode()` and `decode()`:
199 |
200 | ```cfc
201 | pemCertificate = '
202 | -----BEGIN CERTIFICATE-----
203 | ...
204 | -----END CERTIFICATE-----
205 | ';
206 | publicKey = jwt.parsePEMEncodedKey(pemCertificate);
207 | payload = jwt.decode(token, publicKey, 'RS256');
208 | ```
209 |
210 | ```cfc
211 | jwk = {
212 | "e": "AQAB",
213 | "kty": "RSA",
214 | "alg": "RS256",
215 | "n": "AN...",
216 | "use": "sig"
217 | };
218 | publicKey = jwt.parseJWK(jwk);
219 | payload = jwt.decode(token, publicKey, 'RS256');
220 | ```
221 |
222 | ### Acknowledgments
223 |
224 | -
225 | -
226 | -
227 | -
228 |
--------------------------------------------------------------------------------
/tests/specs/encodingUtilsSpec.cfc:
--------------------------------------------------------------------------------
1 | component extends=testbox.system.BaseSpec {
2 |
3 | function run() {
4 | describe( 'The encodingUtils component', function() {
5 | var encodingUtils = new models.encodingUtils();
6 |
7 | it( 'can convert dates to UNIX timestamps', function() {
8 | var dt = parseDateTime( '2019-09-01T00:00:00Z' );
9 | var ut = encodingUtils.convertDateToUnixTimestamp( dt );
10 | expect( ut ).toBe( 1567296000 );
11 | } );
12 |
13 | it( 'can convert UNIX timestamps to dates', function() {
14 | var dt = encodingUtils.convertUnixTimestampToDate( 1567296000 );
15 | expect( dt ).toBe( parseDateTime( '2019-09-01T00:00:00Z' ) );
16 | } );
17 |
18 | it( 'can convert binary data to Base64 URL encoding', function() {
19 | var binaryData = binaryDecode( 'gIecWZe5dS6rVhI7MXgoxeZ/IcUjJ5qZ9+2GUuR3ejk=', 'base64' );
20 | var encoded = encodingUtils.binaryToBase64Url( binaryData );
21 | expect( encoded ).toBe( 'gIecWZe5dS6rVhI7MXgoxeZ_IcUjJ5qZ9-2GUuR3ejk' );
22 | } );
23 |
24 | it( 'can convert Base64 URL encoded data to binary', function() {
25 | var encoded = 'gIecWZe5dS6rVhI7MXgoxeZ_IcUjJ5qZ9-2GUuR3ejk';
26 | var converted = encodingUtils.base64UrlToBinary( encoded );
27 | var binaryData = binaryDecode( 'gIecWZe5dS6rVhI7MXgoxeZ/IcUjJ5qZ9+2GUuR3ejk=', 'base64' );
28 | expect( converted ).toBe( binaryData );
29 | } );
30 |
31 | it( 'can convert an EC P1363 signature to an ASN.1 DER signature', function() {
32 | var P1363Data = binaryDecode(
33 | 'tyh+VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP/3cYHBw7AhHale5wky6+sVA==',
34 | 'base64'
35 | );
36 | var DERData = binaryDecode(
37 | 'MEYCIQC3KH5V+7MjELIZgOWQEDsN/KOuqZIe7qlDaGhm4WpRIgIhAM81jY3SakdveeTkrXsdY//dxgcHDsCEdqV7nCTLr6xU',
38 | 'base64'
39 | );
40 | expect( encodingUtils.convertP1363ToDER( P1363Data ) ).toBe( DERData );
41 | } );
42 |
43 | it( 'can convert an EC ASN.1 DER signature to an P1363 signature', function() {
44 | var P1363Data = binaryDecode(
45 | 'tyh+VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP/3cYHBw7AhHale5wky6+sVA==',
46 | 'base64'
47 | );
48 | var DERData = binaryDecode(
49 | 'MEYCIQC3KH5V+7MjELIZgOWQEDsN/KOuqZIe7qlDaGhm4WpRIgIhAM81jY3SakdveeTkrXsdY//dxgcHDsCEdqV7nCTLr6xU',
50 | 'base64'
51 | );
52 | expect( encodingUtils.convertDERtoP1363( DERData, 'ES256' ) ).toBe( P1363Data );
53 | } );
54 |
55 | describe( 'the parsePEMEncodedKey() method', function() {
56 | it( 'throws a jwtcfml.InvalidPrivateKey exception when given a non PKCS8 format RSA or EC private key', function() {
57 | var rsaKey = fileRead( expandPath( '/sampleKeys/unsupported/sampleRSA.key' ) );
58 | expect( function() {
59 | encodingUtils.parsePEMEncodedKey( rsaKey );
60 | } ).toThrow( type = 'jwtcfml.InvalidPrivateKey' );
61 |
62 | var ecKey = fileRead( expandPath( '/sampleKeys/unsupported/sampleEC.key' ) );
63 | expect( function() {
64 | encodingUtils.parsePEMEncodedKey( ecKey );
65 | } ).toThrow( type = 'jwtcfml.InvalidPrivateKey' );
66 |
67 | var ecKeyWithParams = fileRead( expandPath( '/sampleKeys/unsupported/sampleECWithParams.key' ) );
68 | expect( function() {
69 | encodingUtils.parsePEMEncodedKey( ecKeyWithParams );
70 | } ).toThrow( type = 'jwtcfml.InvalidPrivateKey' );
71 | } );
72 |
73 | it( 'can parse an RSA private key in PKCS8 Format', function() {
74 | var rsaKey = fileRead( expandPath( '/sampleKeys/sampleRSA.key' ) );
75 | var key = encodingUtils.parsePEMEncodedKey( rsaKey );
76 | expect( key.getAlgorithm() ).toBe( 'RSA' );
77 | expect( key.getFormat() ).toBe( 'PKCS##8' );
78 | } );
79 |
80 | it( 'can parse an RSA public key', function() {
81 | var rsaKey = fileRead( expandPath( '/sampleKeys/sampleRSA.pub' ) );
82 | var key = encodingUtils.parsePEMEncodedKey( rsaKey );
83 | expect( key.getAlgorithm() ).toBe( 'RSA' );
84 | expect( key.getFormat() ).toBe( 'X.509' );
85 | } );
86 |
87 | it( 'can parse an RSA certificate', function() {
88 | var rsaCert = fileRead( expandPath( '/sampleKeys/sampleRSA.crt' ) );
89 | var key = encodingUtils.parsePEMEncodedKey( rsaCert );
90 | expect( key.getAlgorithm() ).toBe( 'RSA' );
91 | expect( key.getFormat() ).toBe( 'X.509' );
92 | } );
93 |
94 | it( 'can parse an EC private key in PKCS8 Format', function() {
95 | var ecKey = fileRead( expandPath( '/sampleKeys/sampleEC.key' ) );
96 | var key = encodingUtils.parsePEMEncodedKey( ecKey );
97 | expect( key.getAlgorithm() ).toBe( 'EC' );
98 | expect( key.getFormat() ).toBe( 'PKCS##8' );
99 | } );
100 |
101 | it( 'can parse an EC public key', function() {
102 | var ecKey = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) );
103 | var key = encodingUtils.parsePEMEncodedKey( ecKey );
104 | expect( key.getAlgorithm() ).toBe( 'EC' );
105 | expect( key.getFormat() ).toBe( 'X.509' );
106 | } );
107 |
108 | it( 'can parse an EC certificate', function() {
109 | var ecCert = fileRead( expandPath( '/sampleKeys/sampleEC.crt' ) );
110 | var key = encodingUtils.parsePEMEncodedKey( ecCert );
111 | expect( key.getAlgorithm() ).toBe( 'EC' );
112 | expect( key.getFormat() ).toBe( 'X.509' );
113 | } );
114 | } );
115 |
116 | describe( 'the parseJWK() method', function() {
117 | it( 'can parse an RSA public key', function() {
118 | var jwk = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleRSAPublic.json' ) ) );
119 | var key = encodingUtils.parseJWK( jwk );
120 | expect( key.getAlgorithm() ).toBe( 'RSA' );
121 | expect( key.getFormat() ).toBe( 'X.509' );
122 | } );
123 |
124 |
125 | it( 'can parse an RSA private key', function() {
126 | var jwk = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleRSAPrivate.json' ) ) );
127 | var key = encodingUtils.parseJWK( jwk );
128 | expect( key.getAlgorithm() ).toBe( 'RSA' );
129 | expect( key.getFormat() ).toBe( 'PKCS##8' );
130 |
131 | jwk = jwk.filter( function( k, v ) {
132 | return arrayFind( [ 'kty', 'n', 'd' ], k );
133 | } );
134 | var key = encodingUtils.parseJWK( jwk );
135 | expect( key.getAlgorithm() ).toBe( 'RSA' );
136 | expect( key.getFormat() ).toBe( 'PKCS##8' );
137 | } );
138 |
139 | it( 'can parse an EC public key', function() {
140 | var jwk = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPublic.json' ) ) );
141 | var key = encodingUtils.parseJWK( jwk );
142 | expect( key.getAlgorithm() ).toBe( 'EC' );
143 | expect( key.getFormat() ).toBe( 'X.509' );
144 | } );
145 |
146 | it( 'can parse an EC private key', function() {
147 | var jwk = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPrivate.json' ) ) );
148 | var key = encodingUtils.parseJWK( jwk );
149 | expect( key.getAlgorithm() ).toBe( 'EC' );
150 | expect( key.getFormat() ).toBe( 'PKCS##8' );
151 | } );
152 | } );
153 | } );
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/models/jwt.cfc:
--------------------------------------------------------------------------------
1 | component {
2 |
3 | variables.algorithmMap = {
4 | HS256: 'HmacSHA256',
5 | HS384: 'HmacSHA384',
6 | HS512: 'HmacSHA512',
7 | RS256: 'SHA256withRSA',
8 | RS384: 'SHA384withRSA',
9 | RS512: 'SHA512withRSA',
10 | ES256: 'SHA256withECDSAinP1363Format',
11 | ES384: 'SHA384withECDSAinP1363Format',
12 | ES512: 'SHA512withECDSAinP1363Format'
13 | };
14 |
15 | variables.legacyAlgorithmMap = {
16 | ES256: 'SHA256withECDSA',
17 | ES384: 'SHA384withECDSA',
18 | ES512: 'SHA512withECDSA'
19 | };
20 |
21 | public any function init() {
22 | variables.encodingUtils = new encodingUtils();
23 | variables.jss = createObject( 'java', 'java.security.Signature' );
24 | variables.messageDigest = createObject( 'java', 'java.security.MessageDigest' );
25 | variables.javaVersion = getJavaVersion();
26 |
27 | if ( variables.javaVersion < 11 ) {
28 | structAppend( variables.algorithmMap, variables.legacyAlgorithmMap );
29 | }
30 |
31 | return this;
32 | }
33 |
34 | public string function encode(
35 | required struct payload,
36 | required any key,
37 | required string algorithm,
38 | struct headers = { }
39 | ) {
40 | if ( !algorithmMap.keyExists( algorithm ) ) {
41 | throw(
42 | type = 'jwtcfml.InvalidAlgorithm',
43 | message = 'Invalid JWT Algorithm.',
44 | detail = 'The passed in algorithm is not supported.'
45 | );
46 | }
47 |
48 | var header = { };
49 | header.append( headers );
50 | header.append( {
51 | 'typ': 'JWT',
52 | 'alg': algorithm
53 | } );
54 |
55 | var duplicatedPayload = duplicate( payload );
56 | for ( var claim in [ 'iat', 'exp', 'nbf' ] ) {
57 | if ( duplicatedPayload.keyExists( claim ) && isDate( duplicatedPayload[ claim ] ) ) {
58 | duplicatedPayload[ claim ] = encodingUtils.convertDateToUnixTimestamp( duplicatedPayload[ claim ] );
59 | }
60 | }
61 |
62 | var stringToSignParts = [
63 | encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( header ), 'utf-8' ) ),
64 | encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( duplicatedPayload ), 'utf-8' ) )
65 | ];
66 | var stringToSign = stringToSignParts.toList( '.' );
67 |
68 | return stringToSign & '.' & encodingUtils.binaryToBase64Url( sign( stringToSign, key, algorithm ) );
69 | }
70 |
71 | public struct function decode(
72 | required string token,
73 | any key,
74 | any algorithms = [ ],
75 | struct claims = { },
76 | boolean verify = true
77 | ) {
78 | var parts = listToArray( token, '.' );
79 |
80 | if ( arrayLen( parts ) != 3 ) {
81 | throw(
82 | type = 'jwtcfml.InvalidToken',
83 | message = 'Invalid JWT.',
84 | detail = 'The passed in token does not have three `.` delimited parts.'
85 | );
86 | }
87 |
88 | algorithms = isArray( algorithms ) ? algorithms : [ algorithms ];
89 |
90 | var decoded = {
91 | header: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 1 ] ), 'utf-8' ) ),
92 | payload: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 2 ] ), 'utf-8' ) )
93 | };
94 |
95 | if ( verify ) {
96 | if (
97 | !algorithms.find( decoded.header.alg ) ||
98 | !algorithmMap.keyExists( decoded.header.alg )
99 | ) {
100 | throw(
101 | type = 'jwtcfml.InvalidAlgorithm',
102 | message = 'Unsupported or invalid algorithm',
103 | detail = 'The passed in token does not have an algorithm declaration or its declared algorithm does not match the specified algorithms of #serializeJSON( algorithms )#.'
104 | );
105 | }
106 |
107 | var stringToSign = parts[ 1 ] & '.' & parts[ 2 ];
108 | var signature = encodingUtils.base64UrlToBinary( parts[ 3 ] );
109 |
110 | if (
111 | !verifySignature(
112 | stringToSign,
113 | key,
114 | signature,
115 | decoded.header.alg
116 | )
117 | ) {
118 | throw(
119 | type = 'jwtcfml.InvalidSignature',
120 | message = 'Signature is Invalid',
121 | detail = 'The signature of the passed in token is invalid.'
122 | );
123 | }
124 |
125 | var baseClaims = {
126 | 'exp': true,
127 | 'nbf': true
128 | };
129 | baseClaims.append( claims );
130 | verifyClaims( decoded.payload, baseClaims );
131 | }
132 |
133 | for ( var claim in [ 'iat', 'exp', 'nbf' ] ) {
134 | if ( decoded.payload.keyExists( claim ) ) {
135 | decoded.payload[ claim ] = encodingUtils.convertUnixTimestampToDate( decoded.payload[ claim ] );
136 | }
137 | }
138 |
139 | return decoded.payload;
140 | }
141 |
142 | public struct function getHeader( required string token ) {
143 | return deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( listFirst( token, '.' ) ), 'utf-8' ) );
144 | }
145 |
146 | public function parsePEMEncodedKey( required string pemKey ) {
147 | return encodingUtils.parsePEMEncodedKey( pemKey );
148 | }
149 |
150 | public function parseJWK( required struct jwk ) {
151 | return encodingUtils.parseJWK( jwk );
152 | }
153 |
154 | private function sign( message, key, algorithm ) {
155 | if ( left( algorithm, 1 ) == 'H' ) {
156 | var sig = binaryDecode(
157 | hmac(
158 | message,
159 | key,
160 | algorithmMap[ algorithm ],
161 | 'utf-8'
162 | ),
163 | 'hex'
164 | );
165 | } else {
166 | if ( isSimpleValue( key ) ) {
167 | key = encodingUtils.parsePEMEncodedKey( key );
168 | } else if ( isStruct( key ) ) {
169 | key = encodingUtils.parseJWK( key );
170 | }
171 |
172 | var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] );
173 | jssInstance.initSign( key );
174 | jssInstance.update( charsetDecode( message, 'utf-8' ) );
175 | var sig = jssInstance.sign();
176 | if ( variables.javaVersion < 11 && left( algorithm, 1 ) == 'E' ) {
177 | sig = encodingUtils.convertDERtoP1363( sig, algorithm );
178 | }
179 | }
180 | return sig;
181 | }
182 |
183 | private function verifySignature( message, key, signature, algorithm ) {
184 | if ( left( algorithm, 1 ) == 'H' ) {
185 | var sig = binaryDecode(
186 | hmac(
187 | message,
188 | key,
189 | algorithmMap[ algorithm ],
190 | 'utf-8'
191 | ),
192 | 'hex'
193 | );
194 | return MessageDigest.isEqual( signature, sig );
195 | }
196 |
197 | if ( variables.javaVersion < 11 && left( algorithm, 1 ) == 'E' ) {
198 | signature = encodingUtils.convertP1363ToDER( signature );
199 | }
200 |
201 | if ( isSimpleValue( key ) ) {
202 | key = encodingUtils.parsePEMEncodedKey( key );
203 | } else if ( isStruct( key ) ) {
204 | key = encodingUtils.parseJWK( key );
205 | }
206 |
207 | var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] );
208 | jssInstance.initVerify( key );
209 | jssInstance.update( charsetDecode( message, 'utf-8' ) );
210 | return jssInstance.verify( signature );
211 | }
212 |
213 | private function verifyClaims( payload, claims ) {
214 | if (
215 | structKeyExists( payload, 'exp' )
216 | && !verifyDateClaim( payload.exp, claims.exp, -1 )
217 | ) {
218 | throw(
219 | type = 'jwtcfml.ExpiredSignature',
220 | message = 'Token has expired',
221 | detail = 'The passed in token has expired.'
222 | );
223 | }
224 |
225 | if (
226 | structKeyExists( payload, 'nbf' )
227 | && !verifyDateClaim( payload.nbf, claims.nbf, 1 )
228 | ) {
229 | throw(
230 | type = 'jwtcfml.NotBeforeException',
231 | message = 'Token is not valid',
232 | detail = 'The passed in token has not yet become valid.'
233 | );
234 | }
235 |
236 |
237 |
238 | if ( structKeyExists( claims, 'iss' ) ) {
239 | if ( !structKeyExists( payload, 'iss' ) || compare( payload.iss, claims.iss ) != 0 ) {
240 | throw(
241 | type = 'jwtcfml.InvalidIssuer',
242 | message = 'Token has an invalid issuer',
243 | detail = 'The passed in token either does not specify an issuer or the claimed issuer is not valid.'
244 | );
245 | }
246 | }
247 |
248 | if ( structKeyExists( claims, 'aud' ) ) {
249 | var audArray = isArray( claims.aud ) ? claims.aud : [ claims.aud ];
250 | if ( !structKeyExists( payload, 'aud' ) || !audArray.find( payload.aud ) ) {
251 | throw(
252 | type = 'jwtcfml.InvalidAudience',
253 | message = 'Token has an invalid audience',
254 | detail = 'The passed in token either does not specify an audience or the claimed audience is not valid.'
255 | );
256 | }
257 | }
258 | }
259 |
260 | private function verifyDateClaim( payloadDate, claim, failState ) {
261 | var pd = encodingUtils.convertUnixTimestampToDate( payloadDate );
262 | var cd = claim;
263 | if ( !isBoolean( cd ) || cd ) {
264 | if ( isNumeric( cd ) ) {
265 | cd = encodingUtils.convertUnixTimestampToDate( cd );
266 | } else if ( !isDate( cd ) ) {
267 | cd = now();
268 | }
269 | return dateCompare( pd, cd ) != failState;
270 | }
271 | return true;
272 | }
273 |
274 | private numeric function getJavaVersion() {
275 | var javaVersion = createObject( 'java', 'java.lang.System' ).getProperty( 'java.version' );
276 | if ( javaVersion.startswith( '1.' ) ) {
277 | return int( listGetAt( javaVersion, 2, '.' ) );
278 | }
279 | return int( listFirst( javaVersion, '.' ) );
280 | }
281 |
282 | }
283 |
--------------------------------------------------------------------------------
/models/encodingUtils.cfc:
--------------------------------------------------------------------------------
1 | component {
2 |
3 | public any function init() {
4 | variables.utcBaseDate = createObject( 'java', 'java.util.Date' ).init( javacast( 'long', 0 ) );
5 | variables.ECParameterSpecCache = { };
6 |
7 | var Base64 = createObject( 'java', 'java.util.Base64' );
8 | variables.base64UrlEncoder = Base64.getUrlEncoder().withoutPadding();
9 | variables.base64UrlDecoder = Base64.getUrlDecoder();
10 | }
11 |
12 | function convertDateToUnixTimestamp( required date dateToConvert ) {
13 | return dateDiff( 's', utcBaseDate, parseDateTime( dateToConvert ) );
14 | }
15 |
16 | function convertUnixTimestampToDate( required numeric timestamp ) {
17 | return dateAdd( 's', timestamp, utcBaseDate );
18 | }
19 |
20 | function base64UrlToBinary( base64url ) {
21 | return variables.base64UrlDecoder.decode( base64url );
22 | }
23 |
24 | function binaryToBase64Url( source ) {
25 | return variables.base64UrlEncoder.encodeToString( source );
26 | }
27 |
28 | /**
29 | * The INTEGER encoding for DER consists of a 02 tag a length encoding of the value
30 | * and then a signed, minimum sized, big endian encoding of the encoded number.
31 | *
32 | * - https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature
33 | */
34 | function derEncodeIntegerBytes( byteArray ) {
35 | // first remove any padding
36 | for ( var i = 1; i <= arrayLen( byteArray ); i++ ) {
37 | if ( byteArray[ i ] != 0 ) break;
38 | }
39 | var unpadded = arraySlice( byteArray, i );
40 |
41 | // add sign if negative
42 | if ( unpadded[ 1 ] < 0 ) {
43 | unpadded.prepend( 0 );
44 | }
45 |
46 | // if len > 127 the length encoding will be wrong, but that won't happen for the supported signature sizes
47 | var derEncoded = [ 2, unpadded.len() ];
48 |
49 | derEncoded.append( unpadded, true );
50 |
51 | return derEncoded;
52 | }
53 |
54 |
55 | /**
56 | * The SEQUENCE encoding is simply a tag set to the byte value 30, the
57 | * length encoding and then the concatenation of the two INTEGER
58 | * structures.
59 | *
60 | * https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature
61 | *
62 | * Also see:
63 | * https://crypto.stackexchange.com/questions/57731/ecdsa-signature-rs-to-asn1-der-encoding-question
64 | */
65 | function convertP1363ToDER( signature ) {
66 | var split = len( signature ) / 2;
67 | var r = derEncodeIntegerBytes( arraySlice( signature, 1, split ) );
68 | var s = derEncodeIntegerBytes( arraySlice( signature, split + 1, split ) );
69 |
70 | var DERSignature = [ 48 ];
71 |
72 | var length = r.len() + s.len();
73 |
74 | if ( length > 255 ) {
75 | throw(
76 | type = 'jwtcfml.InvalidSignature',
77 | message = 'Invalid P1363 key.',
78 | detail = 'The P1363 signature is too long.'
79 | );
80 | }
81 |
82 | /*
83 | The length is simply a single byte if it is smaller than 128 (or hex 80)
84 | of the size. If it is larger then it is two byte: one byte set to 81,
85 | which indicates that one length byte will follow, and one byte
86 | containing the actual value.
87 |
88 | https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature
89 | */
90 | if ( length > 127 ) {
91 | DERSignature.append( -127 );
92 | length -= 256;
93 | }
94 | DERSignature.append( length );
95 |
96 | DERSignature.append( r, true );
97 | DERSignature.append( s, true );
98 |
99 | return javacast( 'byte[]', DERSignature );
100 | }
101 |
102 | function convertDERtoP1363( required any signature, required string algorithm ) {
103 | // extract the two integers from the DER signature
104 | // assuming a 02 tag byte followed by a single length byte since we should not see
105 | // anything larger in the supported algorithms
106 | var start = 3;
107 | while ( signature[ start ] != 2 ) start++;
108 | var r = arraySlice( signature, start + 2, signature[ start + 1 ] );
109 | var s = arraySlice( signature, start + 2 + r.len() + 2 );
110 |
111 | if ( r[ 1 ] == 0 ) r = arraySlice( r, 2 );
112 | if ( s[ 1 ] == 0 ) s = arraySlice( s, 2 );
113 |
114 | var lengthMap = {
115 | ES256: 32,
116 | ES384: 48,
117 | ES512: 64
118 | };
119 |
120 | var P1363Signature = [ ];
121 |
122 | for ( var i = 1; i <= lengthMap[ algorithm ] - r.len(); i++ ) P1363Signature.append( 0 );
123 | P1363Signature.append( r, true );
124 |
125 | for ( var i = 1; i <= lengthMap[ algorithm ] - s.len(); i++ ) P1363Signature.append( 0 );
126 | P1363Signature.append( s, true );
127 |
128 | return javacast( 'byte[]', P1363Signature );
129 | }
130 |
131 | function parsePEMEncodedKey( required string pemKey ) {
132 | if ( reFind( '^-----BEGIN (RSA|EC) (PARAMETERS|PRIVATE)', pemKey ) ) {
133 | throw(
134 | type = 'jwtcfml.InvalidPrivateKey',
135 | message = 'Invalid private key format.',
136 | detail = 'Please encode your private key in PKCS8 format, e.g.: `openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pk8'
137 | )
138 | }
139 |
140 | var binaryKey = binaryDecode(
141 | trim( pemKey ).reReplace( '-----[A-Z\s]+-----', '', 'all' ).reReplace( '[\r\n]', '', 'all' ),
142 | 'base64'
143 | );
144 |
145 | if ( find( '-----BEGIN CERTIFICATE-----', pemKey ) ) {
146 | var bis = createObject( 'java', 'java.io.ByteArrayInputStream' ).init( binaryKey );
147 | return createObject( 'java', 'java.security.cert.CertificateFactory' )
148 | .getInstance( 'X.509' )
149 | .generateCertificate( bis )
150 | .getPublicKey();
151 | }
152 |
153 | if ( find( '-----BEGIN PUBLIC KEY-----', pemKey ) ) {
154 | var publicKeySpec = createObject( 'java', 'java.security.spec.X509EncodedKeySpec' ).init( binaryKey );
155 | try {
156 | return createObject( 'java', 'java.security.KeyFactory' )
157 | .getInstance( 'RSA' )
158 | .generatePublic( publicKeySpec );
159 | } catch ( any e ) {
160 | }
161 | try {
162 | return createObject( 'java', 'java.security.KeyFactory' )
163 | .getInstance( 'EC' )
164 | .generatePublic( publicKeySpec );
165 | } catch ( any e ) {
166 | }
167 | }
168 |
169 | if ( find( '-----BEGIN PRIVATE KEY-----', pemKey ) ) {
170 | var privateKeySpec = createObject( 'java', 'java.security.spec.PKCS8EncodedKeySpec' ).init( binaryKey );
171 | try {
172 | return createObject( 'java', 'java.security.KeyFactory' )
173 | .getInstance( 'RSA' )
174 | .generatePrivate( privateKeySpec );
175 | } catch ( any e ) {
176 | }
177 | try {
178 | return createObject( 'java', 'java.security.KeyFactory' )
179 | .getInstance( 'EC' )
180 | .generatePrivate( privateKeySpec );
181 | } catch ( any e ) {
182 | }
183 | }
184 |
185 | throw(
186 | type = 'jwtcfml.InvalidPEMKey',
187 | message = 'Invalid PEM key.',
188 | detail = 'Please ensure you are using an RSA or EC public or private key or certificate.'
189 | )
190 | }
191 |
192 | function parseJWK( required struct jwk ) {
193 | if ( jwk.kty == 'RSA' ) {
194 | if ( jwk.keyExists( 'd' ) ) {
195 | try {
196 | var bigInts = bigIntegers( jwk, [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi' ] );
197 | var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateCrtKeySpec' ).init(
198 | bigInts.n,
199 | bigInts.e,
200 | bigInts.d,
201 | bigInts.p,
202 | bigInts.q,
203 | bigInts.dp,
204 | bigInts.dq,
205 | bigInts.qi
206 | );
207 | var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' );
208 | return kf.generatePrivate( keySpec );
209 | } catch ( any e ) {
210 | }
211 |
212 | try {
213 | var bigInts = bigIntegers( jwk, [ 'n', 'd' ] );
214 | var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateKeySpec' ).init(
215 | bigInts.n,
216 | bigInts.d
217 | );
218 | var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' );
219 | return kf.generatePrivate( keySpec );
220 | } catch ( any e ) {
221 | }
222 | } else {
223 | try {
224 | var bigInts = bigIntegers( jwk, [ 'n', 'e' ] );
225 | var ks = createObject( 'java', 'java.security.spec.RSAPublicKeySpec' ).init( bigInts.n, bigInts.e );
226 | var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' );
227 | return kf.generatePublic( ks );
228 | } catch ( any e ) {
229 | }
230 | }
231 | }
232 |
233 | if ( jwk.kty == 'EC' ) {
234 | var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'EC' );
235 | var ECParameterSpec = getECParameterSpec( jwk.crv );
236 |
237 | if ( jwk.keyExists( 'd' ) ) {
238 | var bigInts = bigIntegers( jwk, [ 'd' ] );
239 | var ks = createObject( 'java', 'java.security.spec.ECPrivateKeySpec' ).init(
240 | bigInts.d,
241 | ECParameterSpec
242 | );
243 | return kf.generatePrivate( ks );
244 | } else {
245 | var bigInts = bigIntegers( jwk, [ 'x', 'y' ] );
246 | var ECPoint = createObject( 'java', 'java.security.spec.ECPoint' ).init( bigInts.x, bigInts.y );
247 | var ks = createObject( 'java', 'java.security.spec.ECPublicKeySpec' ).init( ECPoint, ECParameterSpec );
248 | return kf.generatePublic( ks );
249 | }
250 | }
251 |
252 | throw(
253 | type = 'jwtcfml.InvalidJWK',
254 | message = 'Invalid JWK key.',
255 | detail = 'Please ensure you are using an valid JWK RSA or EC public or private key.'
256 | )
257 | }
258 |
259 | private function bigIntegers( jwk, keys ) {
260 | var bigInts = { };
261 | for ( var key in keys ) {
262 | bigInts[ key ] = createObject( 'java', 'java.math.BigInteger' ).init( 1, base64UrlToBinary( jwk[ key ] ) );
263 | }
264 | return bigInts;
265 | }
266 |
267 | private function getECParameterSpec( crv ) {
268 | if ( !variables.ECParameterSpecCache.keyExists( crv ) ) {
269 | var kpg = createObject( 'java', 'java.security.KeyPairGenerator' ).getInstance( 'EC' );
270 | var ecgp = createObject( 'java', 'java.security.spec.ECGenParameterSpec' ).init(
271 | 'secp#crv.listLast( '-' )#r1'
272 | );
273 | kpg.initialize( ecgp );
274 | variables.ECParameterSpecCache[ crv ] = kpg
275 | .generateKeyPair()
276 | .getPublic()
277 | .getParams();
278 | }
279 | return variables.ECParameterSpecCache[ crv ];
280 | }
281 |
282 | }
283 |
--------------------------------------------------------------------------------
/tests/specs/jwtSpec.cfc:
--------------------------------------------------------------------------------
1 | component extends=testbox.system.BaseSpec {
2 |
3 | function run() {
4 | describe( 'The jwt component', function() {
5 | var jwt = new models.jwt();
6 |
7 | it( 'can return the unverified header', function() {
8 | var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o';
9 | var header = jwt.getHeader( token );
10 | expect( header ).toBe( {
11 | 'alg': 'HS256',
12 | 'typ': 'JWT'
13 | } );
14 | } );
15 |
16 | describe( 'The encode() method', function() {
17 | it( 'throws an error if the algorithm specified is not in the algorithm array', function() {
18 | var key = 'secret';
19 | expect( function() {
20 | payload = jwt.encode( { }, key, 'RS' );
21 | } ).toThrow( 'jwtcfml.InvalidAlgorithm' );
22 | } );
23 |
24 | it( 'supports HS algorithms', function() {
25 | var payload = {
26 | 'name': 'John Doe'
27 | };
28 | var key = 'secret';
29 | var token = jwt.encode( payload, key, 'HS256' );
30 | var expectedToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.GQIdMj0gO4DCPcon_oRn1nFMjfGzA4sOPRIIhRRorLs';
31 | expect( token ).toBe( expectedToken );
32 | } );
33 |
34 | it( 'supports RS algorithms', function() {
35 | var payload = {
36 | 'name': 'John Doe'
37 | };
38 | var expectedToken = fileRead( expandPath( '/sampleTokens/RS512Token.txt' ) ).trim();
39 |
40 | var key = fileRead( expandPath( '/sampleKeys/sampleRSA.key' ) );
41 | var token = jwt.encode( payload, key, 'RS512' );
42 | expect( token ).toBe( expectedToken );
43 |
44 | var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleRSAPrivate.json' ) ) );
45 | var token = jwt.encode( payload, key, 'RS512' );
46 | expect( token ).toBe( expectedToken );
47 | } );
48 |
49 | it( 'supports ES algorithms', function() {
50 | var payload = {
51 | 'name': 'John Doe'
52 | };
53 |
54 | var key = fileRead( expandPath( '/sampleKeys/sampleEC.key' ) );
55 | var token = jwt.encode( payload, key, 'ES256' );
56 |
57 | // EC signatures change every time so just decode and verify that
58 | var publickey = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) );
59 |
60 | var payload = jwt.decode( token, publickey, 'ES256' );
61 | expect( payload ).toBe( {
62 | 'name': 'John Doe'
63 | } );
64 |
65 | var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPrivate.json' ) ) );
66 | var token = jwt.encode( payload, key, 'ES256' );
67 | var payload = jwt.decode( token, publickey, 'ES256' );
68 | expect( payload ).toBe( {
69 | 'name': 'John Doe'
70 | } );
71 | } );
72 |
73 | it( 'supports using Java private key classes', function() {
74 | var payload = {
75 | 'name': 'John Doe'
76 | };
77 |
78 | var key = jwt.parsePEMEncodedKey( fileRead( expandPath( '/sampleKeys/sampleEC.key' ) ) );
79 | var token = jwt.encode( payload, key, 'ES256' );
80 |
81 | // EC signatures change every time so just decode and verify that
82 | var publickey = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) );
83 | var payload = jwt.decode( token, publickey, 'ES256' );
84 |
85 | expect( payload ).toBe( {
86 | 'name': 'John Doe'
87 | } );
88 |
89 | var key = jwt.parseJWK(
90 | deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPrivate.json' ) ) )
91 | );
92 | var token = jwt.encode( payload, key, 'ES256' );
93 |
94 | // EC signatures change every time so just decode and verify that
95 | var publickey = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) );
96 | var payload = jwt.decode( token, publickey, 'ES256' );
97 |
98 | expect( payload ).toBe( {
99 | 'name': 'John Doe'
100 | } );
101 | } );
102 |
103 | it( 'supports adding extra headers', function() {
104 | var payload = {
105 | 'name': 'John Doe'
106 | };
107 | var key = 'secret';
108 | var token = jwt.encode(
109 | payload,
110 | key,
111 | 'HS256',
112 | {
113 | 'kid': '123abc'
114 | }
115 | );
116 | var header = jwt.getHeader( token );
117 | expect( header ).toBe( {
118 | 'typ': 'JWT',
119 | 'alg': 'HS256',
120 | 'kid': '123abc'
121 | } );
122 | } );
123 |
124 | it( 'supports converting CFML dates to UNIX timestamps for "iat", "exp", and "nbf" claims', function() {
125 | var encodingUtils = new models.encodingUtils();
126 | var ts = now();
127 | var ut = encodingUtils.convertDateToUnixTimestamp( ts );
128 |
129 | var payload = {
130 | 'iat': ts,
131 | 'exp': ts,
132 | 'nbf': ts
133 | };
134 | var token = jwt.encode( payload, 'secret', 'HS256' );
135 |
136 | var decodedPayload = deserializeJSON(
137 | charsetEncode( encodingUtils.base64UrlToBinary( listGetAt( token, 2, '.' ) ), 'utf-8' )
138 | );
139 |
140 | expect( decodedPayload ).toBe( {
141 | 'iat': ut,
142 | 'exp': ut,
143 | 'nbf': ut
144 | } );
145 | } );
146 | } );
147 |
148 | describe( 'The decode() method', function() {
149 | it( 'throws an error if the algorithm specified in header is not in the algorithm array', function() {
150 | var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o';
151 | var key = 'secret';
152 | expect( function() {
153 | payload = jwt.decode( token, key, 'RS256' );
154 | } ).toThrow( 'jwtcfml.InvalidAlgorithm' );
155 | } );
156 |
157 | it( 'throws an error if the signature is invalid', function() {
158 | var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o';
159 | var key = 'secret2';
160 | expect( function() {
161 | payload = jwt.decode( token, key, 'HS256' );
162 | } ).toThrow( 'jwtcfml.InvalidSignature' );
163 | } );
164 |
165 |
166 | it( 'allows verification to be bypassed', function() {
167 | var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o';
168 | var key = 'secret2';
169 | expect( function() {
170 | payload = jwt.decode(
171 | token = token,
172 | key = key,
173 | algorithms = 'HS256',
174 | verify = false
175 | );
176 | } ).notToThrow( 'jwtcfml.InvalidSignature' );
177 | } );
178 |
179 | it( 'supports HS algorithms', function() {
180 | var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.GQIdMj0gO4DCPcon_oRn1nFMjfGzA4sOPRIIhRRorLs';
181 | var key = 'secret';
182 | var payload = jwt.decode( token, key, 'HS256' );
183 |
184 | expect( payload ).toBe( {
185 | 'name': 'John Doe'
186 | } );
187 | } );
188 |
189 | it( 'supports RS algorithms', function() {
190 | var token = fileRead( expandPath( '/sampleTokens/RS512Token.txt' ) ).trim();
191 |
192 | var key = fileRead( expandPath( '/sampleKeys/sampleRSA.pub' ) );
193 | var payload = jwt.decode( token, key, 'RS512' );
194 | expect( payload ).toBe( {
195 | 'name': 'John Doe'
196 | } );
197 |
198 | var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleRSAPublic.json' ) ) );
199 | var payload = jwt.decode( token, key, 'RS512' );
200 | expect( payload ).toBe( {
201 | 'name': 'John Doe'
202 | } );
203 | } );
204 |
205 | it( 'supports ES algorithms', function() {
206 | var token = fileRead( expandPath( '/sampleTokens/ES256Token.txt' ) ).trim();
207 |
208 | var key = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) );
209 | var payload = jwt.decode( token, key, 'ES256' );
210 | expect( payload ).toBe( {
211 | 'name': 'John Doe'
212 | } );
213 |
214 | var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPublic.json' ) ) );
215 | var payload = jwt.decode( token, key, 'ES256' );
216 | expect( payload ).toBe( {
217 | 'name': 'John Doe'
218 | } );
219 |
220 | var token = fileRead( expandPath( '/sampleTokens/ES384Token.txt' ) ).trim();
221 |
222 | var key = fileRead( expandPath( '/sampleKeys/sampleEC384.pub' ) );
223 | var payload = jwt.decode( token, key, 'ES384' );
224 | expect( payload ).toBe( {
225 | 'name': 'John Doe'
226 | } );
227 |
228 | var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleEC384Public.json' ) ) );
229 | var payload = jwt.decode( token, key, 'ES384' );
230 | expect( payload ).toBe( {
231 | 'name': 'John Doe'
232 | } );
233 | } );
234 |
235 | it( 'supports using PEM certificates when decoding', function() {
236 | var token = fileRead( expandPath( '/sampleTokens/RS512Token.txt' ) ).trim();
237 | var key = fileRead( expandPath( '/sampleKeys/sampleRSA.crt' ) );
238 | var payload = jwt.decode( token, key, 'RS512' );
239 |
240 | expect( payload ).toBe( {
241 | 'name': 'John Doe'
242 | } );
243 |
244 | var token = fileRead( expandPath( '/sampleTokens/ES256Token.txt' ) ).trim();
245 | var key = fileRead( expandPath( '/sampleKeys/sampleEC.crt' ) );
246 | var payload = jwt.decode( token, key, 'ES256' );
247 |
248 | expect( payload ).toBe( {
249 | 'name': 'John Doe'
250 | } );
251 | } );
252 |
253 | it( 'supports using java public key classes when decoding', function() {
254 | var token = fileRead( expandPath( '/sampleTokens/RS512Token.txt' ) ).trim();
255 | var key = jwt.parsePEMEncodedKey( fileRead( expandPath( '/sampleKeys/sampleRSA.crt' ) ) );
256 | var payload = jwt.decode( token, key, 'RS512' );
257 |
258 | expect( payload ).toBe( {
259 | 'name': 'John Doe'
260 | } );
261 |
262 | var token = fileRead( expandPath( '/sampleTokens/ES256Token.txt' ) ).trim();
263 |
264 | var key = jwt.parsePEMEncodedKey( fileRead( expandPath( '/sampleKeys/sampleEC.crt' ) ) );
265 | var payload = jwt.decode( token, key, 'ES256' );
266 | expect( payload ).toBe( {
267 | 'name': 'John Doe'
268 | } );
269 |
270 | var key = jwt.parseJWK(
271 | deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPublic.json' ) ) )
272 | );
273 | var payload = jwt.decode( token, key, 'ES256' );
274 | expect( payload ).toBe( {
275 | 'name': 'John Doe'
276 | } );
277 | } );
278 |
279 | it( 'verifies the "exp" claim', function() {
280 | var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjU0MjMwMDB9.0F2ysJpLbaf3hFAQ6zwZoQ1L2pBYgzdOHOdz0he5GWo';
281 | var key = 'secret';
282 | expect( function() {
283 | jwt.decode( token, key, 'HS256' );
284 | } ).toThrow( 'jwtcfml.ExpiredSignature' );
285 | } );
286 |
287 | it( 'verifies the "nbf" claim', function() {
288 | var payload = {
289 | nbf: dateAdd( 'h', 1, now() )
290 | };
291 | var token = jwt.encode( payload, 'secret', 'HS256' );
292 | expect( function() {
293 | jwt.decode( token, 'secret', 'HS256' );
294 | } ).toThrow( 'jwtcfml.NotBeforeException' );
295 | } );
296 |
297 | it( 'verifies the "iss" claim', function() {
298 | var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0VEVTVCJ9.WT1ydOldEYxXVxM91LHwk4gW1fwQMS9zJ3n9SbUbOwE';
299 | var key = 'secret';
300 | expect( function() {
301 | jwt.decode(
302 | token,
303 | key,
304 | 'HS256',
305 | {
306 | 'iss': 'testtest'
307 | }
308 | );
309 | } ).toThrow( 'jwtcfml.InvalidIssuer' );
310 | expect( function() {
311 | jwt.decode(
312 | token,
313 | key,
314 | 'HS256',
315 | {
316 | 'iss': 'testTEST'
317 | }
318 | );
319 | } ).notToThrow();
320 | } );
321 |
322 | it( 'verifies the "aud" claim', function() {
323 | var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhIn0.F5RHqgQAiCrHsrVEJO4ZQjQ5CY4L3AKH1nClXHa0JeU';
324 | var key = 'secret';
325 | expect( function() {
326 | jwt.decode(
327 | token,
328 | key,
329 | 'HS256',
330 | {
331 | 'aud': 'b'
332 | }
333 | );
334 | } ).toThrow( 'jwtcfml.InvalidAudience' );
335 | expect( function() {
336 | jwt.decode(
337 | token,
338 | key,
339 | 'HS256',
340 | {
341 | 'aud': [ 'a', 'b' ]
342 | }
343 | );
344 | } ).notToThrow();
345 | } );
346 |
347 | it( 'supports converting UNIX timestamps to CFML dates for "iat", "exp", and "nbf" claims', function() {
348 | var encodingUtils = new models.encodingUtils();
349 | var ts = now();
350 | var ut = encodingUtils.convertDateToUnixTimestamp( ts );
351 |
352 | var payload = {
353 | 'iat': ut,
354 | 'exp': ut,
355 | 'nbf': ut
356 | };
357 | var token = jwt.encode( payload, 'secret', 'HS256' );
358 | var decodedPayload = jwt.decode(
359 | token,
360 | 'secret',
361 | 'HS256',
362 | {
363 | exp: false
364 | }
365 | );
366 | for ( var key in payload ) {
367 | expect( decodedPayload[ key ] ).toBe( ts );
368 | }
369 | } );
370 | } );
371 | } );
372 | }
373 |
374 | }
375 |
--------------------------------------------------------------------------------