├── .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 | --------------------------------------------------------------------------------