├── .gitignore ├── README.MD ├── config ├── module.js └── nginx.conf ├── html ├── private │ └── private_file.txt └── static │ └── static_file.txt ├── pull.sh ├── start.sh └── stop.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Proof of concept of NGINX + JWT Validation 2 | 3 | This is a proof of concept of JWT token validation with NGINX using NJS, a subset of Javascript that 4 | allows extending NGINX functionalities: https://nginx.org/en/docs/njs/ 5 | We will try to consume a protected static file, that will be accessible only when a valid token is provided. 6 | 7 | Please note: 8 | NGINX can already validate JWT Tokens, but only with the Plus subscription 9 | 10 | https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-jwt-authentication/ 11 | 12 | https://www.nginx.com/products/buy-nginx-plus/ 13 | 14 | ## Prerequisites 15 | - Docker 16 | - Bash 17 | - Web Browser 18 | 19 | ## Project startup 20 | 21 | Clone the repository, cd into and then 22 | 23 | bash pull.sh 24 | bash start.sh 25 | 26 | The container will start and attach to port 81. 27 | 28 | You must have a valid JWT token, for example the following one 29 | 30 | http://localhost:81/private/private_file.txt?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o 31 | 32 | will return `private file` 33 | 34 | but using a wrong token 35 | 36 | http://localhost:81/private/private_file.txt?token=xxx 37 | 38 | will return an error. 39 | 40 | Moreover, a static test file with no JWT validation is provided at 41 | 42 | http://localhost:81/static/static_file.txt 43 | 44 | The secret used to validate this token is "secret" (look in `config\module.js`) 45 | 46 | ## Stop 47 | 48 | bash stop.sh 49 | 50 | ## Benchmark 51 | 52 | A benchmark to measure the overhead of this kind of validation is provided here: https://github.com/lombax85/nginx-njs-benchmark 53 | 54 | ## Credits 55 | - The token validation has been done with a slightly modified version of https://github.com/hokaccha/node-jwt-simple 56 | -------------------------------------------------------------------------------- /config/module.js: -------------------------------------------------------------------------------- 1 | function js_function(r) { 2 | jwt.return = r; 3 | 4 | var token = r.args.token; 5 | 6 | try { 7 | var decoded = jwt.decode(token, "secret", false, 'HS256'); 8 | r.internalRedirect('@private'); 9 | } catch (e) { 10 | r.return(200, 'ERROR'); 11 | } 12 | } 13 | 14 | /* 15 | * jwt-simple 16 | * 17 | * JSON Web Token encode and decode module for node.js 18 | * 19 | * Copyright(c) 2011 Kazuhito Hokamura 20 | * MIT Licensed 21 | */ 22 | 23 | /** 24 | * module dependencies 25 | */ 26 | var crypto = require('crypto'); 27 | 28 | 29 | /** 30 | * support algorithm mapping 31 | */ 32 | var algorithmMap = { 33 | HS256: 'sha256', 34 | HS384: 'sha384', 35 | HS512: 'sha512', 36 | RS256: 'RSA-SHA256' 37 | }; 38 | 39 | /** 40 | * Map algorithm to hmac or sign type, to determine which crypto function to use 41 | */ 42 | var typeMap = { 43 | HS256: 'hmac', 44 | HS384: 'hmac', 45 | HS512: 'hmac', 46 | RS256: 'sign' 47 | }; 48 | 49 | 50 | /** 51 | * expose object 52 | */ 53 | var jwt = {}; 54 | 55 | /** 56 | * version 57 | */ 58 | jwt.version = '0.5.6'; 59 | 60 | /** 61 | * Decode jwt 62 | * 63 | * @param {Object} token 64 | * @param {String} key 65 | * @param {Boolean} [noVerify] 66 | * @param {String} [algorithm] 67 | * @return {Object} payload 68 | * @api public 69 | */ 70 | jwt.decode = function jwt_decode(token, key, noVerify, algorithm) { 71 | // check token 72 | if (!token) { 73 | throw new Error('No token supplied'); 74 | } 75 | // check segments 76 | var segments = token.split('.'); 77 | if (segments.length !== 3) { 78 | throw new Error('Not enough or too many segments'); 79 | } 80 | 81 | // All segment should be base64 82 | var headerSeg = segments[0]; 83 | var payloadSeg = segments[1]; 84 | var signatureSeg = segments[2]; 85 | 86 | // base64 decode and parse JSON 87 | //var header = ''; 88 | //var payload = ''; 89 | //jwt.return.return(200, payloadSeg); 90 | 91 | var h = base64urlDecode(headerSeg); 92 | var p = base64urlDecode(payloadSeg); 93 | 94 | 95 | while (h.charCodeAt((h.length-1)) === 0) { 96 | h = h.substring(0, h.length - 1); 97 | } 98 | 99 | while (p.charCodeAt((p.length-1)) === 0) { 100 | p = p.substring(0, p.length - 1); 101 | } 102 | 103 | 104 | var header = JSON.parse(h); 105 | var payload = JSON.parse(p); 106 | 107 | if (!noVerify) { 108 | if (!algorithm && /BEGIN( RSA)? PUBLIC KEY/.test(key.toString())) { 109 | algorithm = 'RS256'; 110 | } 111 | 112 | var signingMethod = algorithmMap[algorithm || header.alg]; 113 | var signingType = typeMap[algorithm || header.alg]; 114 | if (!signingMethod || !signingType) { 115 | throw new Error('Algorithm not supported'); 116 | } 117 | 118 | // verify signature. `sign` will return base64 string. 119 | var signingInput = [headerSeg, payloadSeg].join('.'); 120 | if (!verify(signingInput, key, signingMethod, signingType, signatureSeg)) { 121 | throw new Error('Signature verification failed'); 122 | } 123 | 124 | // Support for nbf and exp claims. 125 | // According to the RFC, they should be in seconds. 126 | if (payload.nbf && Date.now() < payload.nbf*1000) { 127 | throw new Error('Token not yet active'); 128 | } 129 | 130 | if (payload.exp && Date.now() > payload.exp*1000) { 131 | throw new Error('Token expired'); 132 | } 133 | } 134 | 135 | return payload; 136 | }; 137 | 138 | 139 | /** 140 | * Encode jwt 141 | * 142 | * @param {Object} payload 143 | * @param {String} key 144 | * @param {String} algorithm 145 | * @param {Object} options 146 | * @return {String} token 147 | * @api public 148 | */ 149 | jwt.encode = function jwt_encode(payload, key, algorithm, options) { 150 | // Check key 151 | if (!key) { 152 | throw new Error('Require key'); 153 | } 154 | 155 | // Check algorithm, default is HS256 156 | if (!algorithm) { 157 | algorithm = 'HS256'; 158 | } 159 | 160 | var signingMethod = algorithmMap[algorithm]; 161 | var signingType = typeMap[algorithm]; 162 | if (!signingMethod || !signingType) { 163 | throw new Error('Algorithm not supported'); 164 | } 165 | 166 | // header, typ is fixed value. 167 | var header = { typ: 'JWT', alg: algorithm }; 168 | if (options && options.header) { 169 | assignProperties(header, options.header); 170 | } 171 | 172 | // create segments, all segments should be base64 string 173 | var segments = []; 174 | segments.push(base64urlEncode(JSON.stringify(header))); 175 | segments.push(base64urlEncode(JSON.stringify(payload))); 176 | segments.push(sign(segments.join('.'), key, signingMethod, signingType)); 177 | 178 | return segments.join('.'); 179 | }; 180 | 181 | /** 182 | * private util functions 183 | */ 184 | 185 | function assignProperties(dest, source) { 186 | for (var attr in source) { 187 | if (source.hasOwnProperty(attr)) { 188 | dest[attr] = source[attr]; 189 | } 190 | } 191 | } 192 | 193 | function verify(input, key, method, type, signature) { 194 | 195 | if(type === "hmac") { 196 | return (signature === sign(input, key, method, type)); 197 | } 198 | else if(type == "sign") { 199 | return crypto.createVerify(method) 200 | .update(input) 201 | .verify(key, base64urlUnescape(signature), 'base64'); 202 | } 203 | else { 204 | throw new Error('Algorithm type not recognized'); 205 | } 206 | } 207 | 208 | function sign(input, key, method, type) { 209 | var base64str; 210 | if(type === "hmac") { 211 | base64str = crypto.createHmac(method, key).update(input).digest('base64'); 212 | } 213 | else if(type == "sign") { 214 | base64str = crypto.createSign(method).update(input).sign(key, 'base64'); 215 | } 216 | else { 217 | throw new Error('Algorithm type not recognized'); 218 | } 219 | 220 | var ret = base64urlEscape(base64str); 221 | 222 | //jwt.return.return(200, ret); 223 | 224 | return ret; 225 | } 226 | 227 | function base64urlDecode(str) { 228 | return String.bytesFrom(str, 'base64'); 229 | } 230 | 231 | function base64urlUnescape(str) { 232 | str += new Array(5 - str.length % 4).join('='); 233 | return str.replace(/\-/g, '+').replace(/_/g, '/'); 234 | } 235 | 236 | function base64urlEncode(str) { 237 | return str.toString('base64'); 238 | } 239 | 240 | function base64urlEscape(str) { 241 | 242 | str = str.replace(/\+/g, '-'); 243 | str = str.replace(/\//g, '_'); 244 | str = str.replace(/\=/g, ''); 245 | return str; 246 | } 247 | -------------------------------------------------------------------------------- /config/nginx.conf: -------------------------------------------------------------------------------- 1 | load_module modules/ngx_http_js_module.so; 2 | 3 | events { } 4 | 5 | http { 6 | 7 | js_include /etc/nginx/module.js; 8 | 9 | server { 10 | listen 81; 11 | 12 | # serve static files 13 | location /static { 14 | root /usr/share/nginx/html; 15 | expires 30d; 16 | } 17 | 18 | location /private { 19 | js_content js_function; 20 | } 21 | 22 | location @private { 23 | root /usr/share/nginx/html; 24 | expires 30d; 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /html/private/private_file.txt: -------------------------------------------------------------------------------- 1 | private file 2 | -------------------------------------------------------------------------------- /html/static/static_file.txt: -------------------------------------------------------------------------------- 1 | static file 2 | -------------------------------------------------------------------------------- /pull.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker pull nginx -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run --name test-nginx-container \ 3 | -v $(pwd)/config/nginx.conf:/etc/nginx/nginx.conf:ro \ 4 | -v $(pwd)/config/module.js:/etc/nginx/module.js:ro \ 5 | -v $(pwd)/html:/usr/share/nginx/html \ 6 | -p 81:81 -d nginx 7 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker stop test-nginx-container 3 | docker rm test-nginx-container --------------------------------------------------------------------------------