├── .editorconfig ├── .gitignore ├── .istanbul.yml ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── jwtgen.js ├── package.json └── test └── bin ├── assets └── key.pem └── jwtgen.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | # top-most EditorConfig file 3 | # every file 4 | # Unix-style newlines 5 | # newline ending 6 | # character set is utf-8 7 | # use spaces 8 | # set the indent size to 4 9 | 10 | root = true 11 | 12 | [*] 13 | end_of_line = lf 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | charset = utf-8 17 | indent_style = space 18 | indent_size = 4 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | vandium.json 11 | 12 | .env 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 31 | node_modules 32 | 33 | # Debug log from npm 34 | npm-debug.log 35 | 36 | # Mac specific 37 | .DS_Store 38 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | include-all-sources: true 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "undef": true, 4 | "unused": true, 5 | "asi": true, 6 | "globals": { 7 | "Promise": true 8 | }, 9 | "mocha": true, 10 | "predef": [ "-Promise" ], 11 | "esversion": 6 12 | } 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !bin/** 3 | !LICENSE 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4.3.2" 5 | install: 6 | - npm install 7 | script: npm run coverage 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.2.0 (2017-06-14) 4 | 5 | ### Fixes: 6 | 7 | * Fixed issue with reading in private key for RS256 8 | 9 | ### Internal: 10 | 11 | * Better error handling 12 | * Tests use actual private key file 13 | * Updated dependencies 14 | 15 | ## 2.1.0 (2017-04-19) 16 | 17 | ### New: 18 | 19 | * Added support for array and object claims 20 | 21 | ## 2.0.0 (2016-11-17) 22 | 23 | ### New: 24 | 25 | * Added support for JWT headers 26 | 27 | ### Internal: 28 | 29 | * Updated version of Mocha to 3 30 | 31 | ## 1.0.0 (2016-03-09) 32 | 33 | Initial Release 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, 2017, Vandium Software Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Vandium Software Inc. nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL VANDIUM SOFTWARE INC. BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/vandium-io/jwtgen.svg?branch=master)](https://travis-ci.org/vandium-io/jwtgen) 2 | [![npm version](https://badge.fury.io/js/jwtgen.svg)](https://badge.fury.io/js/jwtgen) 3 | 4 | # jwtgen 5 | 6 | Command line tool that generates JWT tokens that helps in the testing of applications. 7 | 8 | ## Features 9 | * Signatures for HMAC-SHA256, HMAC-SHA384, HMAC-SHA512 and RSA-SHA256 10 | * Issued at (iat) and expiry (exp) values can be configured as offsets 11 | * Claims can be encoded as JSON or as separate arguments 12 | 13 | ## Installation 14 | 15 | Install via npm. 16 | 17 | npm install -g jwtgen 18 | 19 | ## Getting Started 20 | 21 | The following command will generate a JWT using HMAC-SHA256, a shared secret of `my-secret`, expires in 1 hour and contains user id of `user123` as the `iss` value. 22 | 23 | ``` 24 | jwtgen -a HS256 -s "my-secret" -c "iss=user123" -e 3600 25 | 26 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ1c2VyMTIzNCIsImlhdCI6MTQ 27 | 1NzU1NTQwNSwiZXhwIjoxNDU3NTU5MDA1fQ.nixEkSKDkru92TBsxdzR8GLANIGQrkRa7E21 28 | C-luNg 29 | ``` 30 | 31 | If the same command is run with the `-v` option, a more verbose output is displayed. 32 | 33 | ``` 34 | jwtgen -a HS256 -s "my-secret" -c "iss=user123" -e 3600 -v 35 | 36 | algorithm: HS256 37 | 38 | claims: 39 | { 40 | "iss": "user1234", 41 | "iat": 1457555405, 42 | "exp": 1457559005 43 | } 44 | 45 | token: 46 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ1c2VyMTIzNCIsImlhdCI6MTQ 47 | 1NzU1NTQwNSwiZXhwIjoxNDU3NTU5MDA1fQ.nixEkSKDkru92TBsxdzR8GLANIGQrkRa7E21 48 | C-luNg 49 | ``` 50 | 51 | ## Expired Tokens 52 | 53 | Expired tokens can be generated by specifying an offset to the `iat` value. The following example issues the token 1 hour (3600 seconds) in the past and expires at the generated time. 54 | 55 | ``` 56 | jwtgen -a HS256 -s "my-secret" -c "iss=user1234" -i=-3600 -e 3600 -v 57 | algorithm: HS256 58 | 59 | claims: 60 | { 61 | "iss": "user1234", 62 | "iat": 1457552128, 63 | "exp": 1457555728 64 | } 65 | 66 | token: 67 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ1c2VyMTIzNCIsImlhdCI6MTQ 68 | 1NzU1MjEyOCwiZXhwIjoxNDU3NTU1NzI4fQ.1SoNk-bCy8l3stfN8q4yrjBjbQkaRWP8AMyP 69 | joDDeHE 70 | ``` 71 | 72 | ## Tokens Not Valid Yet 73 | 74 | Tokens that are not yet valid can be generated by specifying the `iat` value directly as in the following example: 75 | 76 | ``` 77 | jwtgen -a HS256 -s "my-secret" -c "iss=user1234" -i=1557555728 -e 3600 -v 78 | algorithm: HS256 79 | 80 | claims: 81 | { 82 | "iss": "user1234", 83 | "iat": 1557555728, 84 | "exp": 1557559328 85 | } 86 | 87 | token: 88 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ1c2VyMTIzNCIsImlhdCI6MTU 89 | 1NzU1NTcyOCwiZXhwIjoxNTU3NTU5MzI4fQ.kFt-wgNGIQmB4z-G47yQqGfPPW1FSeyKTFdl 90 | 8h5elOQ 91 | ``` 92 | 93 | ## Usage 94 | 95 | Running the `--help` command will display a list of options that can be used. 96 | 97 | ``` 98 | jwtgen --help 99 | 100 | Usage: jwtgen [options] 101 | 102 | Options: 103 | -a, --algorithm algorithm 104 | [required] [choices: "HS256", "HS384", "HS512", "RS256"] 105 | -s, --secret secret value for HMAC algorithm [string] 106 | -p, --private private key file (required for RS256 algorithm) [string] 107 | -c, --claim claim in the form [key=value] [string] 108 | --claims JSON string containing claims [string] 109 | -h, --header header in the form [key=value] [string] 110 | --headers JSON string containing headers [string] 111 | -i, --iat issued at (iat) in seconds from the UNIX epoch [default: now] 112 | -e, --exp expiry date in seconds from issued at (iat) time 113 | -v, --verbose verbose output [boolean] 114 | --help Show help [boolean] 115 | ``` 116 | 117 | ## License 118 | 119 | [BSD-3-Clause](https://en.wikipedia.org/wiki/BSD_licenses) 120 | 121 | Copyright (c) 2016, Vandium Software Inc. 122 | All rights reserved. 123 | 124 | Redistribution and use in source and binary forms, with or without 125 | modification, are permitted provided that the following conditions are met: 126 | * Redistributions of source code must retain the above copyright 127 | notice, this list of conditions and the following disclaimer. 128 | * Redistributions in binary form must reproduce the above copyright 129 | notice, this list of conditions and the following disclaimer in the 130 | documentation and/or other materials provided with the distribution. 131 | * Neither the name of Vandium Software Inc. nor the 132 | names of its contributors may be used to endorse or promote products 133 | derived from this software without specific prior written permission. 134 | 135 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 136 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 137 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 138 | DISCLAIMED. IN NO EVENT SHALL VANDIUM SOFTWARE INC. BE LIABLE FOR ANY 139 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 140 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 141 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 142 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 143 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 144 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 145 | -------------------------------------------------------------------------------- /bin/jwtgen.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const _ = require( 'lodash' ); 6 | 7 | const yargs = require( 'yargs' ); 8 | 9 | const jwtBuilder = require( 'jwt-builder' ); 10 | 11 | const argv = yargs.usage( 'Usage: $0 [options]' ) 12 | 13 | .demand( 'a' ) 14 | .describe( 'a', 'algorithm' ) 15 | .choices( 'a', [ 'HS256', 'HS384', 'HS512', 'RS256' ] ) 16 | .alias( 'a', 'algorithm' ) 17 | 18 | .describe( 's', 'secret value for HMAC algorithm' ) 19 | .alias( 's', 'secret' ) 20 | .string( 's' ) 21 | 22 | .describe( 'p', 'private key file (required for RS256 algorithm)' ) 23 | .alias( 'p', 'private' ) 24 | .string( 'p' ) 25 | 26 | .describe( 'c', 'claim in the form [key=value]' ) 27 | .alias( 'c', 'claim' ) 28 | .string( 'c' ) 29 | 30 | .describe( 'claims', 'JSON string containing claims' ) 31 | .string( 'claims' ) 32 | 33 | .describe( 'h', 'header in the form [key=value]' ) 34 | .alias( 'h', 'header' ) 35 | .string( 'h' ) 36 | 37 | .describe( 'headers', 'JSON string containing additional headers' ) 38 | .string( 'headers' ) 39 | 40 | .describe( 'i', 'issued at (iat) in seconds from the UNIX epoch' ) 41 | .alias( 'i', 'iat' ) 42 | .default( 'i', Math.floor(Date.now()/1000), 'now' ) 43 | 44 | .describe( 'e', 'expiry date in seconds from issued at (iat) time' ) 45 | .alias( 'e', 'exp' ) 46 | .number( 'e ' ) 47 | 48 | .describe( 'v', 'verbose output' ) 49 | .alias( 'v', 'verbose' ) 50 | .boolean( 'v' ) 51 | 52 | .help( 'help' ) 53 | .argv; 54 | 55 | 56 | function exitError( message ) { 57 | 58 | yargs.showHelp(); 59 | 60 | console.error( message ); 61 | 62 | process.exit( 1 ); 63 | } 64 | 65 | function log( message ) { 66 | 67 | if( argv.v ) { 68 | 69 | console.log( message ); 70 | } 71 | } 72 | 73 | function buildClaims() { 74 | 75 | if( argv.claims ) { 76 | 77 | return JSON.parse( argv.claims ); 78 | } 79 | 80 | let claimsList = []; 81 | 82 | let claims = {}; 83 | 84 | if( argv.c ) { 85 | 86 | if( _.isArray( argv.c ) ) { 87 | 88 | claimsList = argv.c; 89 | } 90 | else { 91 | 92 | claimsList = [ argv.c ]; 93 | } 94 | 95 | claimsList.forEach( function( claim ) { 96 | 97 | let parts = claim.split( '=' ); 98 | 99 | if( parts.length !== 2 ) { 100 | 101 | throw new Error( 'invalid claim: ' + claim ); 102 | } 103 | 104 | try { 105 | 106 | claims[ parts[0].trim() ] = JSON.parse( parts[1].trim() ); 107 | } 108 | catch( e ) { 109 | 110 | claims[ parts[0].trim() ] = parts[1].trim(); 111 | } 112 | }); 113 | } 114 | 115 | return claims; 116 | } 117 | 118 | function buildHeaders() { 119 | 120 | if( argv.headers ) { 121 | 122 | return JSON.parse( argv.headers ); 123 | } 124 | 125 | var headersList = []; 126 | 127 | if( _.isArray( argv.h ) ) { 128 | 129 | headersList = argv.h; 130 | } 131 | else if( argv.h ) { 132 | 133 | headersList = [ argv.h ]; 134 | } 135 | 136 | var headers = {}; 137 | 138 | _.forEach( headersList, function( header ) { 139 | 140 | var parts = _.split( header, '=' ); 141 | 142 | if( parts.length !== 2 ) { 143 | 144 | throw new Error( 'invalid header: ' + header ); 145 | } 146 | 147 | headers[ parts[0].trim() ] = parts[1].trim(); 148 | }); 149 | 150 | return headers; 151 | } 152 | 153 | let builder = jwtBuilder(); 154 | 155 | try { 156 | 157 | builder.headers( buildHeaders() ); 158 | 159 | builder.claims( buildClaims() ); 160 | } 161 | catch( err ) { 162 | 163 | return exitError( err.message ); 164 | } 165 | 166 | builder.iat( argv.i ); 167 | 168 | if( argv.e ) { 169 | 170 | builder.exp( argv.e ); 171 | } 172 | 173 | builder.algorithm( argv.a ); 174 | 175 | if( argv.a === 'RS256' ) { 176 | 177 | if( !argv.p ) { 178 | 179 | return exitError( 'private key missing' ); 180 | } 181 | 182 | builder.privateKeyFromFile( argv.p ); 183 | } 184 | else { 185 | 186 | if( !argv.s ) { 187 | 188 | return exitError( 'secret value missing' ); 189 | } 190 | 191 | builder.secret( argv.s ); 192 | } 193 | 194 | let token = builder.build(); 195 | 196 | let claims = JSON.parse( new Buffer( token.split( '.' )[1], 'base64' ).toString() ); 197 | 198 | let headers = JSON.parse( new Buffer( token.split( '.')[0], 'base64' ).toString() ); 199 | 200 | log( 'algorithm: ' + argv.a ); 201 | 202 | log( '' ); 203 | 204 | log( 'claims: ' ); 205 | log( JSON.stringify( claims, null, 2 ) ); 206 | 207 | log( '' ); 208 | 209 | log( 'headers: ' ); 210 | log( JSON.stringify( headers, null, 2 ) ); 211 | 212 | log( '' ); 213 | 214 | log( 'token:' ); 215 | 216 | console.log( token ); 217 | 218 | process.exit( 0 ); 219 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jwtgen", 3 | "version": "2.2.0", 4 | "description": "JWT key generator", 5 | "keywords": [ 6 | "JWT", 7 | "generator", 8 | "JSON", 9 | "token", 10 | "web" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/vandium-io/jwtgen.git" 15 | }, 16 | "engines": { 17 | "node": ">=4.3.2" 18 | }, 19 | "main": "./bin/index.js", 20 | "scripts": { 21 | "test": "./node_modules/.bin/mocha --recursive", 22 | "coverage": "./node_modules/.bin/istanbul cover node_modules/mocha/bin/_mocha -- -R spec --recursive" 23 | }, 24 | "bin": { 25 | "jwtgen": "./bin/jwtgen.js" 26 | }, 27 | "author": "vandium software", 28 | "license": "BSD-3-Clause", 29 | "dependencies": { 30 | "jwt-builder": "^1.1.0", 31 | "lodash": "^4.6.1", 32 | "yargs": "^8.0.2" 33 | }, 34 | "devDependencies": { 35 | "app-root-path": "^2.0.1", 36 | "chai": "^4.0.2", 37 | "istanbul": "^0.4.3", 38 | "mocha": "^3.2.0", 39 | "proxyquire": "^1.7.10", 40 | "sinon": "^2.3.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/bin/assets/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA1CRTbfZR0GjVUN6CFd5WHWwlO9+DK9GhiJ9vDjd1lNslxyc4 3 | iPerz0iwd25oiWX/gCePl5ga9ti2nQ4Vp3y6K1pXrHW4JuI+B0+Ov1Hi6RUoz2ti 4 | ylXo+zMmGHke8tHT1ToLJMD230BpiRhnfcoeXD4dzvUEVADipnfRfwk5Chq09LtV 5 | IVfH4XzIvCusKsiOBJ38YQHW/yYBlbk/ABTynagADSCj2rbkQmm7VOaNjLHek3Jh 6 | 8cx4zlmufw16oI3NjoRwYJx3BUOKku4YFad2lpgiLqIDlfoYxPh/yt50ogxQ3JCf 7 | RXJjIm+2L1KrJykfycf+poKGmNZcpJ46TM/O/wIDAQABAoIBAQCb2dAdIfA7Lzk/ 8 | /ygMjtovJWs5UFyajZihuZeqFx5f7JwVcn/7SBFF6H4UT92my3NooCfC1DFDFjUa 9 | ruVDtcND5Ewy3A0dK+ssDcuuTKdqW2wu7hKW2YSfP5WGNz2AYJzCsrDLIKR9oWGn 10 | IXGunRWu6MeXarPyTvA0IiSbspfgDiVe0iQDdvQ5yi8jaE++A/jHdKfbEldvj/M9 11 | 4JpWpm2sEygUc7/YgMhftIvSs9sSBWH4VOLOqhtULDcbaXHiBHClrBr8eNvPDRB6 12 | 4oXuWZRJ3KMUkVzMcIWQz6zbWFhQJ0ef+meCp/Ru+NbCM1SFFPrpDlVByn6Rsa/c 13 | RE1980YBAoGBAPvX88ePckJegVybty+hEA/0Oe0uLza3mFzKzMWFN3GjaaWFVp/p 14 | aKwrR1tMnc14loMjUjqwG5rA/GTsGlZELL35W3WWrB1VkHNLHvCdFOe930mgLsiC 15 | NFCWDME0LqK/FDv6R5OXC9/kjxDrSUuHIfAkHoHggyOfCKcHwvxJtU8BAoGBANek 16 | ofpxpL2AZVagdep8zUBUr6ECalBCJ4lAUzRAJ9b7woTK2/rptEJe9nV0FBSwfVGM 17 | v3/QgIJEjsxcBut+F8RiDcz6t+MYZ1H1sVI53wIqbJrkvlUH4ySHLWP7DOQuDwEq 18 | vDympU0dUdzp7vRgxhbq1ejhB+L88yuf+KmqQx3/AoGBALl5n/ZW9OVgSJF1tQIE 19 | 8pltWsQNAYSgzjt4uDq4E89DdGkMvHFlK0uxTAo3cPiEWQUXnFXQoWPlwXxqTjM+ 20 | Xl1DKlJ9tyCbhZkDuDOo3F6X/bxxlkLhcbnv6FDaJ4aAh1xsxQ4zbfFkmODzB7Xy 21 | PdJoJ3era6luVKb/FmFLv5YBAoGAYvjVuvtzr+IBINwuonu7PH5hyVUxdDqYqL7U 22 | MVQMtPgB1C7C/gtLR3cMSOYI3WIPcmFV9xS8Fo3euF64clcCE4kpal3cDifCK/TG 23 | +MVE3FqaVBEOZjpjfv1n0M6FoSiejFuP34pgrd76Fplrqc/MuvH3UZYOp5iPBwO5 24 | /iz99wMCgYAS2ARx+s2/QGgJqz4LUItxghSbdOfG/jkFTBi77ZMuBOelPEAK+4WI 25 | yrumX1fIM4AYl9/jnuM7hzfK2s0Z4YS2yh6XIeH49PxGQc3z8Fia8Jfq0kE47elf 26 | 579C2dgHwK3FvDJCf4nLYaDwv98Sl5l2DnvccKwYyi8/oISvdH/s7Q== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/bin/jwtgen.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* jshint expr: true */ 3 | 4 | const expect = require( 'chai' ).expect; 5 | 6 | const proxyquire = require( 'proxyquire' ).noCallThru(); 7 | 8 | const sinon = require( 'sinon' ); 9 | 10 | const appRoot = require( 'app-root-path' ); 11 | 12 | function decodeToken( token ) { 13 | 14 | let claims = JSON.parse( new Buffer( token.split( '.' )[1], 'base64' ).toString() ); 15 | 16 | let headers = JSON.parse( new Buffer( token.split( '.')[0], 'base64' ).toString() ); 17 | 18 | return { 19 | 20 | claims, 21 | headers 22 | }; 23 | } 24 | 25 | describe( 'bin/jwtgen', function() { 26 | 27 | let yargsStub; 28 | 29 | let processExitStub; 30 | 31 | let consoleLogStub; 32 | 33 | let consoleErrorStub; 34 | 35 | beforeEach( function() { 36 | 37 | processExitStub = sinon.stub( process, 'exit' ); 38 | 39 | yargsStub = {}; 40 | 41 | yargsStub.usage = sinon.stub().returns( yargsStub ); 42 | yargsStub.command = sinon.stub().returns( yargsStub ); 43 | yargsStub.strict = sinon.stub().returns( yargsStub ); 44 | yargsStub.demand = sinon.stub().returns( yargsStub ); 45 | yargsStub.describe = sinon.stub().returns( yargsStub ); 46 | yargsStub.choices = sinon.stub().returns( yargsStub ); 47 | yargsStub.alias = sinon.stub().returns( yargsStub ); 48 | yargsStub.string = sinon.stub().returns( yargsStub ); 49 | yargsStub.number = sinon.stub().returns( yargsStub ); 50 | yargsStub.boolean = sinon.stub().returns( yargsStub ); 51 | yargsStub.default = sinon.stub().returns( yargsStub ); 52 | yargsStub.help = sinon.stub().returns( yargsStub ); 53 | yargsStub.showHelp = sinon.stub() 54 | }); 55 | 56 | it( 'normal operation', function() { 57 | 58 | yargsStub.argv = { 59 | 60 | a: 'HS256', 61 | s: 'my-secret' 62 | }; 63 | 64 | let consoleLogStub = sinon.stub( console, 'log' ); 65 | 66 | proxyquire( '../../bin/jwtgen', { 67 | 68 | 'yargs': yargsStub 69 | }); 70 | 71 | console.log.restore(); 72 | 73 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 74 | 75 | expect( token.headers ).to.eql( { typ: 'JWT', alg: 'HS256' } ); 76 | expect( token.claims.iat ).to.equal( Math.floor( Date.now() / 1000 ) ); 77 | }); 78 | 79 | it( 'expiry date', function() { 80 | 81 | yargsStub.argv = { 82 | 83 | a: 'HS256', 84 | s: 'my-secret', 85 | e: 3600 86 | }; 87 | 88 | let consoleLogStub = sinon.stub( console, 'log' ); 89 | 90 | proxyquire( '../../bin/jwtgen', { 91 | 92 | 'yargs': yargsStub 93 | }); 94 | 95 | console.log.restore(); 96 | 97 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 98 | 99 | expect( token.claims.exp - token.claims.iat ).to.equal( 3600 ); 100 | }); 101 | 102 | it( 'RS256', function() { 103 | 104 | yargsStub.argv = { 105 | 106 | a: 'RS256', 107 | p: appRoot + '/test/bin/assets/key.pem' 108 | }; 109 | 110 | let consoleLogStub = sinon.stub( console, 'log' ); 111 | 112 | proxyquire( '../../bin/jwtgen', { 113 | 114 | 'yargs': yargsStub 115 | }); 116 | 117 | console.log.restore(); 118 | 119 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 120 | 121 | expect( token.headers ).to.eql( { typ: 'JWT', alg: 'RS256' } ); 122 | expect( token.claims.iat ).to.equal( Math.floor( Date.now() / 1000 ) ); 123 | }); 124 | 125 | it( 'error: RS256 with no private key', function() { 126 | 127 | yargsStub.argv = { 128 | 129 | a: 'RS256' 130 | }; 131 | 132 | let consoleErrorStub = sinon.stub( console, 'error' ); 133 | 134 | proxyquire( '../../bin/jwtgen', { 135 | 136 | 'yargs': yargsStub 137 | }); 138 | 139 | console.error.restore(); 140 | 141 | expect( consoleErrorStub.withArgs( 'private key missing' ).calledOnce ).to.be.true; 142 | expect( yargsStub.showHelp.calledOnce ).to.be.true; 143 | expect( processExitStub.withArgs( 1 ).calledOnce ).to.be.true; 144 | }); 145 | 146 | it( 'error: no secret', function() { 147 | 148 | yargsStub.argv = { 149 | 150 | a: 'HS256' 151 | }; 152 | 153 | let consoleErrorStub = sinon.stub( console, 'error' ); 154 | 155 | proxyquire( '../../bin/jwtgen', { 156 | 157 | 'yargs': yargsStub 158 | }); 159 | 160 | console.error.restore(); 161 | 162 | expect( consoleErrorStub.withArgs( 'secret value missing' ).calledOnce ).to.be.true; 163 | expect( yargsStub.showHelp.calledOnce ).to.be.true; 164 | expect( processExitStub.withArgs( 1 ).calledOnce ).to.be.true; 165 | }); 166 | 167 | it( 'verbose operation', function() { 168 | 169 | yargsStub.argv = { 170 | 171 | a: 'HS256', 172 | s: 'my-secret', 173 | v: {} 174 | }; 175 | 176 | consoleLogStub = sinon.stub( console, 'log' ); 177 | 178 | proxyquire( '../../bin/jwtgen', { 179 | 180 | 'yargs': yargsStub 181 | }); 182 | 183 | console.log.restore(); 184 | 185 | expect( consoleLogStub.callCount ).to.equal( 10 ); 186 | }); 187 | 188 | it( 'single claim', function() { 189 | 190 | yargsStub.argv = { 191 | 192 | a: 'HS256', 193 | s: 'my-secret', 194 | c: 'iss=user123' 195 | }; 196 | 197 | let consoleLogStub = sinon.stub( console, 'log' ); 198 | 199 | proxyquire( '../../bin/jwtgen', { 200 | 201 | 'yargs': yargsStub 202 | }); 203 | 204 | console.log.restore(); 205 | 206 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 207 | 208 | expect( token.claims.iss ).to.equal( 'user123' ); 209 | }); 210 | 211 | it( 'array of claims', function(){ 212 | 213 | yargsStub.argv = { 214 | 215 | a: 'HS256', 216 | s: 'my-secret', 217 | c: [ 'iss=user123', 'nonce=random' ] 218 | }; 219 | 220 | let consoleLogStub = sinon.stub( console, 'log' ); 221 | 222 | proxyquire( '../../bin/jwtgen', { 223 | 224 | 'yargs': yargsStub 225 | }); 226 | 227 | console.log.restore(); 228 | 229 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 230 | 231 | expect( token.claims.iss ).to.equal( 'user123' ); 232 | expect( token.claims.nonce ).to.equal( 'random' ); 233 | }); 234 | 235 | it( 'JSON object of claims', function() { 236 | 237 | yargsStub.argv = { 238 | 239 | a: 'HS256', 240 | s: 'my-secret', 241 | claims: JSON.stringify( { iss: 'user123' } ) 242 | }; 243 | 244 | let consoleLogStub = sinon.stub( console, 'log' ); 245 | 246 | proxyquire( '../../bin/jwtgen', { 247 | 248 | 'yargs': yargsStub 249 | }); 250 | 251 | console.log.restore(); 252 | 253 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 254 | 255 | expect( token.claims.iss ).to.equal( 'user123' ); 256 | }); 257 | 258 | it( 'claim that is an array', function() { 259 | 260 | yargsStub.argv = { 261 | 262 | a: 'HS256', 263 | s: 'my-secret', 264 | c: 'roles=["ROLE_ADMIN","ROLE_USER"]' 265 | }; 266 | 267 | let consoleLogStub = sinon.stub( console, 'log' ); 268 | 269 | proxyquire( '../../bin/jwtgen', { 270 | 271 | 'yargs': yargsStub 272 | }); 273 | 274 | console.log.restore(); 275 | 276 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 277 | 278 | expect( token.claims.roles ).to.eql( [ "ROLE_ADMIN", "ROLE_USER" ] ); 279 | }); 280 | 281 | it( 'claim that is an object', function() { 282 | 283 | yargsStub.argv = { 284 | 285 | a: 'HS256', 286 | s: 'my-secret', 287 | c: 'roles={"type":"ROLE_ADMIN","name":"myRole"}' 288 | }; 289 | 290 | let consoleLogStub = sinon.stub( console, 'log' ); 291 | 292 | proxyquire( '../../bin/jwtgen', { 293 | 294 | 'yargs': yargsStub 295 | }); 296 | 297 | console.log.restore(); 298 | 299 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 300 | 301 | expect( token.claims.roles ).to.eql( { "type": "ROLE_ADMIN", "name": "myRole" } ); 302 | }); 303 | 304 | it( 'error: invalid claim', function() { 305 | 306 | let claim = 'noEqualsSign'; 307 | 308 | yargsStub.argv = { 309 | 310 | a: 'HS256', 311 | s: 'my-secret', 312 | c: claim 313 | }; 314 | 315 | let consoleErrorStub = sinon.stub( console, 'error' ); 316 | 317 | proxyquire( '../../bin/jwtgen', { 318 | 319 | 'yargs': yargsStub 320 | }); 321 | 322 | console.error.restore(); 323 | 324 | expect( consoleErrorStub.withArgs( 'invalid claim: ' + claim ).calledOnce ).to.be.true; 325 | expect( yargsStub.showHelp.calledOnce ).to.be.true; 326 | expect( processExitStub.withArgs( 1 ).calledOnce ).to.be.true; 327 | }); 328 | 329 | it( 'single header', function() { 330 | 331 | yargsStub.argv = { 332 | 333 | a: 'HS256', 334 | s: 'my-secret', 335 | h: 'kid=2016-11-17' 336 | }; 337 | 338 | let consoleLogStub = sinon.stub( console, 'log' ); 339 | 340 | proxyquire( '../../bin/jwtgen', { 341 | 342 | 'yargs': yargsStub 343 | }); 344 | 345 | console.log.restore(); 346 | 347 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 348 | 349 | expect( token.headers.kid ).to.equal( '2016-11-17' ); 350 | }); 351 | 352 | it( 'array of headers', function() { 353 | 354 | yargsStub.argv = { 355 | 356 | a: 'HS256', 357 | s: 'my-secret', 358 | h: [ 'kid=2016-11-17', 'otherHeader=somethingElse' ] 359 | }; 360 | 361 | let consoleLogStub = sinon.stub( console, 'log' ); 362 | 363 | proxyquire( '../../bin/jwtgen', { 364 | 365 | 'yargs': yargsStub 366 | }); 367 | 368 | console.log.restore(); 369 | 370 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 371 | 372 | expect( token.headers.kid ).to.equal( '2016-11-17' ); 373 | expect( token.headers.otherHeader ).to.equal( 'somethingElse' ); 374 | }); 375 | 376 | it( 'JSON object of headers', function() { 377 | 378 | yargsStub.argv = { 379 | 380 | a: 'HS256', 381 | s: 'my-secret', 382 | headers: JSON.stringify( { kid: '2016-11-17' } ) 383 | }; 384 | 385 | let consoleLogStub = sinon.stub( console, 'log' ); 386 | 387 | proxyquire( '../../bin/jwtgen', { 388 | 389 | 'yargs': yargsStub 390 | }); 391 | 392 | console.log.restore(); 393 | 394 | let token = decodeToken( consoleLogStub.firstCall.args[ 0 ] ); 395 | 396 | expect( token.headers.kid ).to.equal( '2016-11-17' ); 397 | }); 398 | 399 | it( 'error: invalid header', function() { 400 | 401 | let header = 'noEqualsSign'; 402 | 403 | yargsStub.argv = { 404 | 405 | a: 'HS256', 406 | s: 'my-secret', 407 | h: header 408 | }; 409 | 410 | let consoleErrorStub = sinon.stub( console, 'error' ); 411 | 412 | proxyquire( '../../bin/jwtgen', { 413 | 414 | 'yargs': yargsStub 415 | }); 416 | 417 | console.error.restore(); 418 | 419 | expect( consoleErrorStub.withArgs( 'invalid header: ' + header ).calledOnce ).to.be.true; 420 | expect( yargsStub.showHelp.calledOnce ).to.be.true; 421 | expect( processExitStub.withArgs( 1 ).calledOnce ).to.be.true; 422 | }); 423 | 424 | afterEach( function() { 425 | 426 | process.exit.restore(); 427 | 428 | if( console.log.restore ) { 429 | 430 | console.log.restore(); 431 | } 432 | 433 | if( console.error.restore ){ 434 | 435 | console.error.restore(); 436 | } 437 | }); 438 | }); 439 | --------------------------------------------------------------------------------