├── .gitignore ├── .travis.yml ├── LICENSE ├── box.json ├── jwt.cfc ├── readme.md └── tests ├── Application.cfc ├── index.cfm └── specs └── jwtTest.cfc /.gitignore: -------------------------------------------------------------------------------- 1 | testbox -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: required 3 | jdk: 4 | - openjdk11 5 | cache: 6 | directories: 7 | - $HOME/.CommandBox 8 | env: 9 | matrix: 10 | - ENGINE=lucee@5 11 | - ENGINE=adobe@2016 12 | - ENGINE=adobe@2018 13 | before_install: 14 | - sudo apt-key adv --keyserver keys.gnupg.net --recv 6DA70622 15 | - sudo echo "deb http://downloads.ortussolutions.com/debs/noarch /" | sudo tee -a 16 | /etc/apt/sources.list.d/commandbox.list 17 | install: 18 | - sudo apt-get update && sudo apt-get --assume-yes install commandbox 19 | - box install 20 | before_script: 21 | - box server start cfengine=$ENGINE port=8500 22 | script: 23 | - box testbox run runner="http://127.0.0.1:8500/tests/index.cfm" 24 | notifications: 25 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jason Steinshouer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"CF JWT Simple", 3 | "slug":"cf-jwt-simple", 4 | "shortDescription":"CFML component for encoding and decoding JSON Web Tokens (JWT)", 5 | "description":"", 6 | "version":"1.3.0", 7 | "author":"Jason Steinshouer ", 8 | "location":"https://github.com/jsteinshouer/cf-jwt-simple/archive/master.zip", 9 | "repository":{ 10 | "type":"git", 11 | "url":"https://github.com/jsteinshouer/cf-jwt-simple" 12 | }, 13 | "keywords":[ 14 | "jwt", 15 | "JSON Web Token" 16 | ], 17 | "license":[ 18 | { 19 | "type":"MIT", 20 | "url":"http://opensource.org/licenses/MIT" 21 | } 22 | ], 23 | "bugs":"https://github.com/jsteinshouer/cf-jwt-simple/issues", 24 | "private":false, 25 | "ignore":[ 26 | "**/.*", 27 | "tests", 28 | "readme.md" 29 | ], 30 | "devDependencies":{ 31 | "testbox":"^2.4.0+80" 32 | }, 33 | "installPaths":{ 34 | "testbox":"testbox/" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /jwt.cfc: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # CF-JWT-Simple 2 | 3 | ## Description 4 | 5 | CFML Component for encoding and decoding [JSON Web Tokens (JWT)](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html). 6 | 7 | This is a port of the node.js project [node-jwt-simple](https://github.com/hokaccha/node-jwt-simple) to cfml. It currently supports HS256, HS384, and HS512 signing algorithms. 8 | 9 | 10 | ## Usage 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ## Support for registered claims 20 | 21 | This CFC supports the `nbf` and `exp` registered claims that can be part of the payload. Verification of the token will fail if the token is not yet active or if the token is expired according to the `nbf` and `exp` claims. They should be numeric dates in Unix epoch time according to the JWT spec. 22 | 23 | To ignore the `exp` claim during verification, pass `ignoreExpiration=true` when instantiating the CFC. For example: 24 | 25 | 26 | 27 | This CFC also supports the `aud` and `iss` registered claims during verification. If you don't pass `audience` or `issuer` during instantiation, the claims will be ignored during verification. If you do pass them, they'll be included during the verification process. Here's an example: 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright Since 2005 Ortus Solutions, Corp 3 | * www.coldbox.org | www.luismajano.com | www.ortussolutions.com | www.gocontentbox.org 4 | ************************************************************************************** 5 | */ 6 | component{ 7 | this.name = "A TestBox Runner Suite " & hash( getCurrentTemplatePath() ); 8 | // any other application.cfc stuff goes below: 9 | this.sessionManagement = false; 10 | 11 | // any mappings go here, we create one that points to the root called test. 12 | this.mappings[ "/test" ] = getDirectoryFromPath( getCurrentTemplatePath() ); 13 | rootPath = REReplaceNoCase( this.mappings[ "/test" ], "tests(\\|/)", "" ); 14 | this.mappings["/root"] = rootPath; 15 | 16 | // request start 17 | public boolean function onRequestStart( String targetPage ){ 18 | 19 | return true; 20 | } 21 | } -------------------------------------------------------------------------------- /tests/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Lucee Version #server.lucee.version# 13 | 14 | Adobe CF Version #server.coldfusion.productVersion# 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/specs/jwtTest.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * cf-jwt-simple TestBox Suite 3 | */ 4 | component extends="testbox.system.BaseSpec"{ 5 | 6 | /*********************************** LIFE CYCLE Methods ***********************************/ 7 | 8 | // executes before all suites+specs in the run() method 9 | function beforeAll(){ 10 | } 11 | 12 | // executes after all suites+specs in the run() method 13 | function afterAll(){ 14 | } 15 | 16 | function run( testResults, testBox ){ 17 | describe( "cf-jwt-simple TestBox Suite", function(){ 18 | 19 | beforeEach(function( currentSpec ){ 20 | myPrivateKey = "abcdefg"; 21 | jwt = new root.jwt(myPrivateKey); 22 | 23 | /* Test tokens can be generated at https://jwt.io/ and Epoch time from http://www.epochconverter.com/*/ 24 | testData = { 25 | payload = { 26 | "sub": "1234567890", 27 | "name": "John Doe", 28 | "admin": true 29 | }, 30 | // Uses payload 31 | validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.e0CFuBLfhSbH7bQIVrIODvMIcdiKBpmk0TVcWE288dQ", 32 | invalidFormatToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0cyI6IkZlYnJ1YXJ5LCAwNSAyMDE0IDEyOjA4OjA1IiwidXNlcmlkIjoiamRvZSJ9", 33 | // Payload signed with invalid signature 34 | invalidToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.ruc_ziwPAc2QnO2zrrrEL_Fn-SSjtczeW4SeQGcjUn0", 35 | /* 36 | exp: 2037-12-31 17:00:00 GMT Last year that we can convert using dateAdd in ACF 37 | { 38 | "iss": "http://myapi", 39 | "aud": "clientid", 40 | "exp" : 2145891600 41 | "sub": "1234567890", 42 | "name": "John Doe", 43 | "admin": true 44 | } */ 45 | tokenWithClaims = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbXlhcGkiLCJhdWQiOiJjbGllbnRpZCIsImV4cCI6MjE0NTg5MTYwMCwic3ViIjoiMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlfQ.EGZZwFvl9q_44Pq5wH18FZ_R4r7FsXegkf_onRvQqU8", 46 | //exp: 1999-01-01 00:00:00 47 | expiredTokenWithClaims = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbXlhcGkiLCJhdWQiOiJjbGllbnRpZCIsImV4cCI6OTE1MTQ4ODAwLCJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.HZXXIsXFO6yp8SDlL91PpuPVo_fbMXxKzOj4lCNkaV8", 48 | //nbf: 1999-01-01 00:00:00 GMT 49 | validNotBefore = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjkxNTE0ODgwMCwic3ViIjoieHl6In0.-KjrG0ktPVz-RrEhf79NCiWASljbA--wYjK2ykC_bbw", 50 | //nbf: 2999-01-01 00:00:00 GMT 51 | invalidNotBefore = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjMyNDcyMTQ0MDAwLCJzdWIiOiJ4eXoifQ.I13RhKA9iflSJ2xLHxgUARYe7IRuf7J_MlGFkNKj3cQ" 52 | }; 53 | 54 | }); 55 | 56 | describe( "encode()", function(){ 57 | 58 | it( "should return a valid JWT formatted token", function(){ 59 | var token = jwt.encode(testData.payload); 60 | 61 | expect( listLen(token,".") ).toBe(3); 62 | }); 63 | 64 | }); 65 | 66 | describe( "decode()", function(){ 67 | 68 | it( "should return data decoded from JWT in a struct", function(){ 69 | 70 | var result = jwt.decode(testData.validToken); 71 | 72 | expect( result ).toBeStruct(); 73 | expect( result.sub ).toBe(1234567890); 74 | expect( result.name ).toBe("John Doe"); 75 | }); 76 | 77 | it( "should throw an error for token with an invalid format", function(){ 78 | 79 | expect( function(){ 80 | jwt.decode(testData.invalidFormatToken); 81 | } ).toThrow( type="Invalid Token", regex = "Token should contain 3 segments" ); 82 | 83 | }); 84 | 85 | it( "should throw an error for token signed with the wrong key", function(){ 86 | 87 | expect( function(){ 88 | jwt.decode(testData.invalidToken); 89 | } ).toThrow( type="Invalid Token", regex = "Signature verification failed: Invalid key" ); 90 | 91 | }); 92 | 93 | it( "should verify token nbf (Not Before) claim ", function(){ 94 | var data = jwt.decode(testData.validNotBefore); 95 | expect( data.sub ).toBe("xyz"); 96 | expect( DateAdd("s",data.nbf,DateConvert("utc2Local","January 1 1970 00:00"))).toBeLT(now()); 97 | }); 98 | 99 | it( "should fail if nbf is prior to current date and time", function(){ 100 | 101 | expect( function(){ 102 | jwt.decode(testData.expiredTokenWithClaims); 103 | } ).toThrow( type="Invalid Token", regex = "Signature verification failed: Token expired" ); 104 | 105 | }); 106 | 107 | it( "should verify token is not expired", function(){ 108 | var data = jwt.decode(testData.tokenWithClaims); 109 | expect( data.name ).toBe("John Doe"); 110 | }); 111 | 112 | it( "should fail for an expired token", function(){ 113 | 114 | expect( function(){ 115 | jwt.decode(testData.expiredTokenWithClaims); 116 | } ).toThrow( type="Invalid Token", regex = "Signature verification failed: Token expired" ); 117 | 118 | }); 119 | 120 | it( "should not fail for an expired token when ignoreExpiration is true", function(){ 121 | 122 | var jwt = new root.jwt(key="abcdefg",ignoreExpiration=true); 123 | 124 | var data = local.jwt.decode(testData.expiredTokenWithClaims); 125 | expect( data.name ).toBe("John Doe"); 126 | 127 | }); 128 | 129 | it( "should verify the issuer if provided", function(){ 130 | var jwt = new root.jwt(key="abcdefg",issuer="http://myapi"); 131 | var data = local.jwt.decode(testData.tokenWithClaims); 132 | expect( data.iss ).toBe("http://myapi"); 133 | }); 134 | 135 | it( "should throw an error if issuer does not match", function(){ 136 | 137 | var jwt = new root.jwt(key="abcdefg",issuer="http://test.issuer.com"); 138 | expect( function(){ 139 | jwt.decode(testData.tokenWithClaims); 140 | } ).toThrow( type="Invalid Token", regex = "Signature verification failed: Issuer does not match" ); 141 | 142 | }); 143 | 144 | it( "should verify the audience if provided", function(){ 145 | var jwt = new root.jwt(key="abcdefg",audience="clientid"); 146 | var data = local.jwt.decode(testData.tokenWithClaims); 147 | expect( data.aud ).toBe("clientid"); 148 | }); 149 | 150 | it( "should throw an error if audience does not match", function(){ 151 | 152 | var jwt = new root.jwt(key="abcdefg",audience="xyz"); 153 | expect( function(){ 154 | jwt.decode(testData.tokenWithClaims); 155 | } ).toThrow( type="Invalid Token", regex = "Signature verification failed: Audience does not match" ); 156 | 157 | }); 158 | 159 | 160 | 161 | }); 162 | 163 | describe( "verify()", function(){ 164 | 165 | it( "should return true for a valid token", function(){ 166 | var result = jwt.verify(testData.validToken); 167 | 168 | expect( result ).toBeTrue(); 169 | }); 170 | 171 | it( "should return false for an invalid token", function(){ 172 | var result = jwt.verify(testData.invalidToken); 173 | 174 | expect( result ).toBeFalse(); 175 | }); 176 | 177 | }); 178 | 179 | 180 | }); 181 | } 182 | 183 | } --------------------------------------------------------------------------------