├── .dockerignore ├── .eslintrc.json ├── create_keys.sh ├── lib └── keys.js ├── Dockerfile ├── middleware └── invalidTokenHandler.js ├── index.js ├── jwtRS256.key.pub ├── package.json ├── config └── config.js ├── LICENSE ├── .gitignore ├── k8s-deployment.yaml ├── jwtRS256.key ├── README.md └── routes └── routes.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /create_keys.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | key_name="jwtRS256" 4 | ssh-keygen -t rsa -b 2048 -f "${key_name}.key" 5 | openssl rsa -in "${key_name}.key" -pubout -outform PEM -out "${key_name}.key.pub" -------------------------------------------------------------------------------- /lib/keys.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const privateKey = fs.readFileSync('./jwtRS256.key'); 4 | const publicKey = fs.readFileSync('./jwtRS256.key.pub'); 5 | 6 | module.exports = { 7 | privateKey, 8 | publicKey, 9 | }; 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | ENV INSTALL_DIR=/usr/src/app 3 | EXPOSE 8000 4 | RUN mkdir -p $INSTALL_DIR && \ 5 | chown -R node $INSTALL_DIR 6 | USER node 7 | COPY . $INSTALL_DIR 8 | WORKDIR $INSTALL_DIR 9 | RUN npm ci --only=production 10 | CMD [ "npm", "start" ] 11 | -------------------------------------------------------------------------------- /middleware/invalidTokenHandler.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const handleInvalidToken = ((err, req, res, next) => { 3 | if (err) { 4 | console.error(err); 5 | } 6 | if (err.name === 'UnauthorizedError') { 7 | res.status(401).json({ status: 'invalid token' }); 8 | } 9 | next(); 10 | }); 11 | 12 | module.exports = handleInvalidToken; 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const helmet = require('helmet'); 3 | 4 | const app = express(); 5 | const { PORT } = process.env; 6 | const port = PORT || 8000; // Default to 8000 if env not set 7 | const routes = require('./routes/routes'); 8 | 9 | app.use('/', [helmet(), routes]); 10 | 11 | app.listen(port, () => console.log(`Listening on port ${port}`)); 12 | -------------------------------------------------------------------------------- /jwtRS256.key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFO+qXP+p+Y9ul0fdzzn 3 | IwoNuCV0ijs9TZZWwZJZ9UvXnHVNtWuR2yp8msuE0EFjR/aGTaKyHR8e0c7cySn5 4 | SVnLB2IxSuWHLGH09q2Vt/cj/Vnxuy/xSGZH17eioS9n+VQEFo8LlIF3JNm4j35B 5 | nI2IiptlelzpFG/vib7ZtZUCyqczuj8z76PEe2Vo9uRpex/IeaBiWhROxVSPUaYl 6 | KzwY/Ak9/scGdhszF0KxnRBuVEkEeZJ1NlKt5XGnCNVWgWdtsDMHlF9YLwxdqNaV 7 | xjF9oEJCw/9UTq+lo00tFV9saZgmfskk3nPE6rXjuOv/+FpQ3uTX9hF70Ir+K1Z0 8 | yQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-jwt-auth", 3 | "version": "1.2.0", 4 | "description": "Backend to supply/verify json web tokens", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "start": "node index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "Caleb Lemoine", 12 | "license": "MIT", 13 | "dependencies": { 14 | "express": "^4.17.1", 15 | "express-basic-auth": "^1.2.0", 16 | "express-jwt": "^6.0.0", 17 | "helmet": "^4.2.0", 18 | "jsonwebtoken": "^8.5.1", 19 | "swagger-jsdoc": "^6.0.0-rc.3", 20 | "swagger-ui-express": "^4.1.5" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^7.15.0", 24 | "eslint-config-airbnb-base": "^14.2.1", 25 | "eslint-plugin-import": "^2.22.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | const { version } = require('../package.json'); 2 | 3 | module.exports = { 4 | basicAuthConfig: { 5 | challenge: true, 6 | users: { 7 | admin: 'admin', 8 | guest: 'password', 9 | }, 10 | unauthorizedResponse: { status: 'Unauthorized' }, 11 | }, 12 | jwtConfig: { 13 | expiresIn: '1h', 14 | algorithm: 'RS256', 15 | }, 16 | swaggerConfig: { 17 | definition: { 18 | openapi: '3.0.3', 19 | info: { 20 | title: 'Express-JWT', 21 | version, 22 | }, 23 | basePath: '/', 24 | components: { 25 | securitySchemes: { 26 | basicAuth: { 27 | type: 'http', 28 | scheme: 'basic', 29 | }, 30 | bearerAuth: { 31 | type: 'http', 32 | scheme: 'bearer', 33 | bearerFormat: 'JWT', 34 | }, 35 | }, 36 | }, 37 | }, 38 | apis: ['./routes/routes.js'], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Caleb Lemoine 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 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 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /k8s-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: express-jwt-deployment 5 | labels: 6 | app: express 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: express 12 | template: 13 | metadata: 14 | labels: 15 | app: express 16 | spec: 17 | containers: 18 | - name: express-jwt 19 | image: circa10a/express-jwt 20 | readinessProbe: 21 | httpGet: 22 | path: /healthCheck 23 | port: express-port 24 | initialDelaySeconds: 5 25 | periodSeconds: 15 26 | livenessProbe: 27 | httpGet: 28 | path: /healthCheck 29 | port: express-port 30 | initialDelaySeconds: 5 31 | periodSeconds: 15 32 | ports: 33 | - name: express-port 34 | containerPort: 8000 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: express-jwt-service 40 | spec: 41 | selector: 42 | app: express 43 | ports: 44 | - protocol: TCP 45 | port: 80 46 | targetPort: express-port 47 | --- 48 | apiVersion: extensions/v1beta1 49 | kind: Ingress 50 | metadata: 51 | name: express-jwt-ingress 52 | spec: 53 | backend: 54 | serviceName: express-jwt-service 55 | servicePort: 80 -------------------------------------------------------------------------------- /jwtRS256.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAvFO+qXP+p+Y9ul0fdzznIwoNuCV0ijs9TZZWwZJZ9UvXnHVN 3 | tWuR2yp8msuE0EFjR/aGTaKyHR8e0c7cySn5SVnLB2IxSuWHLGH09q2Vt/cj/Vnx 4 | uy/xSGZH17eioS9n+VQEFo8LlIF3JNm4j35BnI2IiptlelzpFG/vib7ZtZUCyqcz 5 | uj8z76PEe2Vo9uRpex/IeaBiWhROxVSPUaYlKzwY/Ak9/scGdhszF0KxnRBuVEkE 6 | eZJ1NlKt5XGnCNVWgWdtsDMHlF9YLwxdqNaVxjF9oEJCw/9UTq+lo00tFV9saZgm 7 | fskk3nPE6rXjuOv/+FpQ3uTX9hF70Ir+K1Z0yQIDAQABAoIBABq+ndX8tg5sARol 8 | PWG2kXCFbftXvlwfG46YKgvlV8chFSaP9hAsjZMdToLBIkRc9Nd0aBBAaaD/GWZc 9 | RTiAMHm9Obr7s9paDD8+TgZ2EkwE2eSR0GVv4okQlWVKG9teGxrqVoZJnDBjzmK6 10 | VJ50JKcx5lEgzmF5LlSLrO3X350MpJzY71jqXvxRaSSsGXKHeLl2H3LNWxolEBj5 11 | Bbgy2tlRgmPYtapeBotZPqmwlgUVNnmqCy3yvnKd3xgjNrZEWOs/jA3OWEMdWuVa 12 | 46iHpFYwgw2KXzgOLgBArYPfVSjfkZPugOHZdIp8FTTDh8q0gfQT0A0KrWR6wLl2 13 | RmFjpu0CgYEA84lWUXnosXktcqBo42lZ/mlnl58Zj1ffv76Ox3+06n58MNbFOKT6 14 | nQYCfAqBXiZfQlqqJ2b3Pav3r4DaLOhRkPRj+oSHsE7yqKYa+IcVfTcTOh1RVirZ 15 | VygmcvWqaItyCtlg9z7xbBIvSHVSGGmTPKQtjnc1gtqF9jnVN1B+ywMCgYEAxfcW 16 | 1ySXiXU2qGM3xEb2Sl3AMpasntDMt1l6+OKRV2Lw0iF+n1sSgAKZqedXBcuY5V6/ 17 | Gugjjd9XO7tBVPPz/eUFOr7mHqadSe9hweYOv+I+/Ulp/Erh4cwo0pyPBHUc0R1Y 18 | 959Hoj6X8stal4Jj1HL176ER/DQMjDY36a/ZcUMCgYBfB5c0IdLn9bYDRY7INmLU 19 | gILYyk9p/MslghEqza1l10dUs2mv6ciVHzQ2/M5wU57WQJSm3WnamdEDnc8EuP6j 20 | BopAxhhAdv3/Sxm5ItUC0EvjYG9NpVi7xsICF9SQCOHOU/afY+NG3W2v43/OYNwA 21 | TLwuHFw4HFcrtnN5qyJeywKBgF2g43iVoeYtPdCePH3kYhACDyjeAn6KmogNFhAo 22 | eD0wWl3H4a9Uz4cjs7Gb7JidFo9FJHWBXW94NjXV9qxLRRbL/pEuQIA2pSWWxlGC 23 | kW9HfislUa81a2fzu0sBKNMe4KY2jyFuf97IY+09KHeH/9c3GAJh17PEmjqmWgN4 24 | XAspAoGAVmURFYePI7TQJbn3TtoEkOeFhVVxxcw97EVeOqQeA/DuIVV9mF7i+jJV 25 | cZSjhToIhFjr4XzyU+aXGTh393Z/6JG9KKE4zV53ik4aOlbc7uDvUzgN4YfmwxTD 26 | 7F0WxS4Bw8PE6FgOqKw0g92B+9qlKh9kRWkKGwyYimc3wGKDknI= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express-JWT 2 | 3 | An example API for creating/verifying json web tokens. 4 | 5 | > Yes, the private key is in the repo. Not secure. I know. 6 | 7 | ## Usage 8 | 9 | ### Install Dependencies 10 | 11 | ``` 12 | npm install 13 | ``` 14 | 15 | ### Start 16 | 17 | ``` 18 | npm start 19 | ``` 20 | 21 | ### Docker 22 | 23 | ```shell 24 | docker run -d --name express-jwt -p 8000:8000 circa10a/express-jwt 25 | ``` 26 | 27 | ### Kubernetes 28 | 29 | ``` 30 | kubectl apply -f https://raw.githubusercontent.com/circa10a/express-jwt/master/k8s-deployment.yaml 31 | ``` 32 | 33 | Then navigate to http://localhost:8000/ to see the swagger api docs. 34 | 35 | ## Configuration 36 | 37 | ### Port 38 | 39 | Default listens on port `8000`, but can be changed by specifying a `PORT` environment variable. 40 | 41 | ### Basic auth users 42 | 43 | To obtain your JWT, you must authenticate against `/login` with Basic Auth. 44 | 45 | There are 2 hardcoded sample users in `config/config.js` that can be used. You can also easily append to the object for more fake users. 46 | 47 | **Users** 48 | 49 | | user | password | base64 | 50 | |---|---|---| 51 | | admin | admin | YWRtaW46YWRtaW4= | 52 | | guest | password | Z3Vlc3Q6cGFzc3dvcmQ= | 53 | 54 | ### JWT Expiration 55 | 56 | In `config/config.js` the default expiration time of a JWT is `1h` 57 | 58 | ## Obtain token 59 | 60 | ```shell 61 | curl -H "Authorization: Basic YWRtaW46YWRtaW4=" http://localhost:8000/login 62 | ``` 63 | 64 | ## Auth with token 65 | 66 | You can use the token previously acquired via curl or here's a token with no expiration you can test with: 67 | 68 | ```shell 69 | curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1NjIzNDc0MzR9.g0-jxWgWVc98C6EzEWYoWxyIXVY3xmzgIZfD32PBZfrwrVrTiAqP69IrJ3DKBseeVgf2dwOm4ennwpakHXv-xxfZyMoM8-nfwJardv0Pr4bToBhGwxJhe-g1Hy7ygID5XpqQok9zY_R-0vZn-o-opi9VZYvTft9ZBAPEdj9oPZrRk_LfrrMQjO-oK9BiNQTjZm0rzFsqetk8FmqKwtb-TDPmmkgS0remsbsJzyvAi2x6r7fosljM2t0vjxdGzumbU4pxuSsQUjoRDzPG0VAH2rKNHECFqmCWJ8myIBOobYYAt7TIW0TzzJkyXb9amfDjy1IBlZyvwEznTUT_XBh6hQ" http://localhost:8000/protected 70 | ``` -------------------------------------------------------------------------------- /routes/routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const expressJwt = require('express-jwt'); 3 | const jwt = require('jsonwebtoken'); 4 | const basicAuth = require('express-basic-auth'); 5 | 6 | const swaggerUi = require('swagger-ui-express'); 7 | const swaggerJSDoc = require('swagger-jsdoc'); 8 | 9 | const invalidTokenHandler = require('../middleware/invalidTokenHandler'); 10 | const { privateKey, publicKey } = require('../lib/keys'); 11 | const { jwtConfig, basicAuthConfig, swaggerConfig } = require('../config/config'); 12 | 13 | const swaggerSpec = swaggerJSDoc(swaggerConfig); 14 | 15 | const router = express.Router(); 16 | 17 | // Ensure user gets to api docs 18 | router.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); 19 | 20 | router.get('/', (req, res) => { 21 | res.redirect('/api-docs'); 22 | }); 23 | 24 | router.get('/healthCheck', (req, res) => { 25 | res.sendStatus(200); 26 | }); 27 | 28 | /** 29 | * @swagger 30 | * /login: 31 | * get: 32 | * summary: Login to the application via basic auth(use admin:admin) 33 | * tags: [Login (Get JWT)] 34 | * security: 35 | * - basicAuth: [] 36 | * responses: 37 | * '200': 38 | * description: returns json with user and token 39 | * content: 40 | * application/json: 41 | * schema: 42 | * type: object 43 | * properties: 44 | * user: 45 | * type: string 46 | * description: user authenticated with 47 | * token: 48 | * type: string 49 | * description: JWT 50 | * '401': 51 | * description: returns error in json 52 | * content: 53 | * application/json: 54 | * schema: 55 | * type: object 56 | * properties: 57 | * status: 58 | * type: string 59 | * description: authentication status 60 | */ 61 | router.get('/login', basicAuth(basicAuthConfig), (req, res) => { 62 | const { user } = req.auth; 63 | const token = jwt.sign({ user, data: 'some example data' }, privateKey, jwtConfig); // config => expire time, algorithm 64 | res.json({ user, token }); 65 | }); 66 | 67 | /** 68 | * @swagger 69 | * /protected: 70 | * get: 71 | * summary: Authenticate with the application using JWT (bearer auth) 72 | * tags: [Protected route (Use JWT)] 73 | * security: 74 | * - bearerAuth: [] 75 | * responses: 76 | * '200': 77 | * description: returns authentication status 78 | * content: 79 | * application/json: 80 | * schema: 81 | * type: object 82 | * properties: 83 | * status: 84 | * type: string 85 | * description: authorization status 86 | * jwtData: 87 | * type: object 88 | * description: jwt data 89 | * properties: 90 | * user: 91 | * type: string 92 | * description: username 93 | * data: 94 | * type: string 95 | * description: example data sent back from server 96 | * iat: 97 | * type: integer 98 | * description: issued at timestamp 99 | * exp: 100 | * type: integer 101 | * desscription: expires at timestamp 102 | * '401': 103 | * description: returns authentication status 104 | * content: 105 | * application/json: 106 | * schema: 107 | * type: object 108 | * properties: 109 | * status: 110 | * type: string 111 | * description: authorization/token validity status 112 | */ 113 | router.get('/protected', expressJwt({ secret: publicKey, algorithms: [jwtConfig.algorithm] }), invalidTokenHandler, (req, res) => { 114 | if (req.user) { 115 | res.status(200).json({ status: 'Authorized', jwtData: req.user }); 116 | } 117 | }); 118 | 119 | module.exports = router; 120 | --------------------------------------------------------------------------------