├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── __mock__
└── cron.ts
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── error.ts
├── index.ts
├── jwtAuth.ts
└── storage
│ ├── __mock__
│ └── storage.ts
│ ├── index.ts
│ ├── interface.ts
│ └── storage.ts
├── test
├── fileStorage.int.test.ts
├── index.test.ts
└── storage.test.ts
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | /lib
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true
5 | },
6 | extends: [
7 | "eslint:recommended",
8 | "plugin:prettier/recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended"
11 | ],
12 | plugins: ["@typescript-eslint"],
13 | globals: {
14 | Atomics: "readonly",
15 | SharedArrayBuffer: "readonly"
16 | },
17 | parserOptions: {
18 | ecmaVersion: 2018,
19 | sourceType: "module"
20 | },
21 | parser: "@typescript-eslint/parser",
22 | rules: {}
23 | };
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | /lib
4 | /coverage
5 | /authcerts
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 12
4 | - lts/*
5 | before_script:
6 | - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | sudo bash"
7 | script:
8 | - fossa init
9 | - fossa analyze
10 | test:
11 | - npm run test
12 | after_success:
13 | - codecov
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Hansen Wang
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/WangHansen/jwt-auth)
2 | [](https://codecov.io/gh/WangHansen/jwt-auth)
3 | [](https://opensource.org/licenses/MIT)
4 | [](https://app.fossa.com/projects/git%2Bgithub.com%2FWangHansen%2Fjwt-auth?ref=badge_shield)
5 |
6 |
7 |
8 |
9 |
12 |
13 |
JWT Auth
14 |
15 |
16 | A light weight authentication library that supports key rotation and revokation list.
17 |
18 |
19 |
20 |
21 |
22 | ## Table of Contents
23 |
24 | - [Another auth library?](#about-the-project)
25 | - [Getting Started](#getting-started)
26 | - [Prerequisites](#prerequisites)
27 | - [Installation](#installation)
28 | - [Usage](#usage)
29 | - [API](#api)
30 | - [Persistent Storage](#persistent-storage)
31 | - [File Storage](#file-storage)
32 | - [Write your own persistent storage](#write-your-own-persistent-storage)
33 | - [Contributing](#contributing)
34 | - [License](#license)
35 | - [Contact](#contact)
36 |
37 |
38 |
39 | ## Another auth library?
40 |
41 | There are a lot of authentication libraries out there that deals with JWT, probably the most popular one(the one that I used a lot in my project) is the passport-jwt library used together with passport. However, the library has the few problems:
42 | - Need to be used with passport.js
43 | > This may not be a problem to some people, but I find passport.js a bit difficult to use since it's a black box model (I don't understand the magic happening behind the scene).
44 | - Need to talk to DB
45 | > The [official example](http://www.passportjs.org/packages/passport-jwt/#configure-strategy) in documentation contains a query to db in order to authenticate the user, which I believe is against the natural, being stateless, of JWT.
46 | - Doesn't handle key rotation
47 | - Doesn't handle key revocation
48 |
49 | In order to address these problems, I decided to make this open source library.
50 |
51 |
52 |
53 | ## Getting Started
54 |
55 | ### Prerequisites
56 |
57 | I have this tested from Node version 12 and above, make sure you have the right version
58 |
59 | ### Installation
60 |
61 | Install with npm
62 |
63 | ```JS
64 | npm install --save @hansenw/jwt-auth
65 | ```
66 |
67 |
68 |
69 | ## Usage
70 |
71 | ### Simple Usage
72 | ```javascript
73 | // authService.js
74 | import JWTAuth from "@hansenw/jwt-auth";
75 |
76 | const JWT = new JWTAuth();
77 | export default JWT;
78 |
79 | // to use in other files
80 | import jwt from "./authService";
81 |
82 | // to generate a jwt token
83 | const token = jwt.sign({ /* some payload */ });
84 |
85 | // to verify
86 | try {
87 | const payload = jwt.verify(token);
88 | // ...
89 | } catch (e) {
90 | // cannot be verified
91 | }
92 |
93 | // to revoke
94 | await jwt.revoke(token);
95 | jwt.verify(token); // this will throw JWTRevoked
96 | ```
97 |
98 | ### With Express
99 | ```javascript
100 | import * as express from "express";
101 | import JWTAuth from "@hansenw/jwt-auth";
102 |
103 | const app = express();
104 | const jwt = new JWTAuth();
105 |
106 | app.post("/login", async (req, res, next) => {
107 | const { username, password } = req.body;
108 |
109 | // Your own logic .. to verify credentials
110 | const match = authenticate(username, password);
111 |
112 | if (match) {
113 | const jwtpayload = { username };
114 | const token = jwt.sign(payload);
115 | res.set({
116 | "Access-Control-Expose-Headers": "Authorization",
117 | Authorization: "Bearer " + token,
118 | })
119 | .json({
120 | message: "Login success",
121 | });
122 | } else {
123 | // handle failure logic
124 | }
125 | })
126 |
127 | // middleware for protecting api
128 | function authGuard(req, res, next) {
129 | // getting token from header
130 | const header = req.headers["authorization"];
131 | const token = header ? header.split(" ")[1] : "";
132 | if (!token) {
133 | return next(new Error("No auth token"));
134 | }
135 | // verify token validity
136 | try {
137 | const payload = jwt.verify(token);
138 | // if token is valid, attach the payload to req object
139 | req.payload = payload;
140 | } catch (e) {
141 | // token invalid, can be handled differently based on the error
142 | }
143 | }
144 |
145 | app.post("/protected", authGuard, async (req, res, next) => {
146 | // get JWT payload
147 | const payload = req.payload;
148 |
149 | // if user info is ever needed
150 | const user = await db.collection("user").find({ username: payload.username });
151 |
152 | res.json({ message: "Authorized user only" })
153 | })
154 |
155 | // start the express app
156 | app.listen(3000)
157 | ```
158 |
159 | ### Advanced Usage with TS
160 | > Customze what to store in the revocation list, be default revocation list contain items on type { jti: string, exp: number }
161 |
162 | ```typescript
163 | import JWTAuth, { RevocationListItem } from "@hansenw/jwt-auth";
164 |
165 | interface RevocListItem extends RevocationListItem {
166 | ip: string;
167 | }
168 |
169 | const jwt = new JWTAuth();
170 |
171 | const token = jwt.sign({ /* some payload */ });
172 |
173 | // to verify
174 | try {
175 | const payload = jwt.verify(token);
176 | // ...
177 | } catch (e) {
178 | // cannot be verified
179 | }
180 |
181 | // to revoke
182 | await jwt.revoke(token, (payload) => ({
183 | jti: payload.jti,
184 | exp: payload.exp,
185 | ip: req.ip,
186 | }));
187 | jwt.verify(token); // this will throw JWTRevoked
188 | ```
189 |
190 | ### Microservice
191 |
192 | If you want to build your own auth server or auth service within the microservices, check out this [jwt-jwks-client](https://github.com/WangHansen/jwt-jwks-client) library I made that can be used together with this one.
193 |
194 | #### server.ts
195 | ```typescript
196 | import * as express from "express";
197 | import JWTAuth from "@hansenw/jwt-auth";
198 |
199 | const app = express()
200 | const authService = new JwtAuth();
201 |
202 | app.post("/login", (req: Request, res: Response) => {
203 | // Replace with your own matching logic
204 | if (req.body.username === "admin" && req.body.password === "password") {
205 | const token = authService.sign({ userId: "admin" });
206 | return (
207 | res
208 | .set("authorization", token)
209 | .send("Authorized")
210 | );
211 | }
212 | res.status(401).send("Not authorized");
213 | });
214 |
215 | // Expose jwks through an API
216 | app.get("/jwks", (req: Request, res: Response) => {
217 | res.json(authService.JWKS(true));
218 | });
219 | ```
220 |
221 | #### Client
222 | ```ts
223 | import * as express from "express";
224 | import JwksClient from "jwt-jwks-client";
225 |
226 | const authClient = new JwksClient({
227 | jwksUri: "http://localhost:3000/jwks",
228 | secure: false,
229 | });
230 |
231 | app.get("/secret", async (req: Request, res: Response) => {
232 | const token = req.headers.authorization;
233 | if (token) {
234 | // Verify the token here
235 | await authClient.verify(token);
236 | return res.send("This is a secret page");
237 | }
238 | return res.send(`You are not authorized to see the secret page`);
239 | });
240 | ```
241 | See complete example [here](https://github.com/WangHansen/jwt-jwks-client/tree/master/example)
242 |
243 | ## API
244 |
245 | ### Constructor
246 | __Class: JWTAuth__
247 | ```javascript
248 | const jwt = new JWTAuth(options: JwtAuthOptions);
249 | ```
250 | `JwtAuthOptions`:
251 | - `algorithm?`: can be ['RSA' | 'EC' | 'OKP' | 'oct'], __Default__: "EC"
252 | - `crvOrSize?`: `` key size (in bits) or named curve ('crv') for "EC", __Default__: 2048 for RSA, 'P-256' for EC, 'Ed25519' for OKP and 256 for oct.
253 | - `amount?`: `` number of keys kept in rotation, __Default__: 3
254 | - `interval?`: `` [cron](https://github.com/kelektiv/node-cron#cron-ranges) expression for how often to generate a new key, __Default__: "00 00 */4 * * *": every 4 hour, generate a new token
255 | > Make sure the token expire time is less than the interval that a new token is generated
256 | - `signSkip?`: `` number of keys skipped when generating a new token, __Default__: 1
257 | > By default, there are 3 keys stored, and by setting this to 1, every time a new token is signed, only the last 2 keys will be used since the first key will be removed after the rotation.
258 | - `tokenAge?`: `` token expire time in zeit/ms, __Default__ '10m'
259 |
260 | ### Methods
261 | #### `jwt.sign(payload: object, options?: JWT.SignOptions)`
262 | Generate a new jwt token
263 | ```javascript
264 | const token = jwt.sign(payload, options?);
265 | ```
266 | - `payload`: `