├── .gitignore ├── wallaby.conf.js ├── package.json ├── src └── index.js └── tests └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /wallaby.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = wallaby => ({ 2 | files: ["src/**/*.js"], 3 | tests: ["tests/**/*.js"], 4 | env: { 5 | type: "node", 6 | }, 7 | compilers: { 8 | "**/*.js": wallaby.compilers.babel({ 9 | presets: ["@ava/babel-preset-stage-4"], 10 | plugins: ["@babel/plugin-proposal-object-rest-spread"], 11 | }), 12 | }, 13 | debug: true, 14 | testFramework: "ava", 15 | }); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-jwt-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "ava" 8 | }, 9 | "lint-staged": { 10 | "*.js": [ 11 | "eslint" 12 | ] 13 | }, 14 | "keywords": [], 15 | "author": "Illya Klymov { 12 | if (!this.token) { 13 | return config; 14 | } 15 | 16 | const newConfig = { 17 | headers: {}, 18 | ...config, 19 | }; 20 | 21 | newConfig.headers.Authorization = `Bearer ${this.token}`; 22 | return newConfig; 23 | }, 24 | e => Promise.reject(e) 25 | ); 26 | 27 | this.client.interceptors.response.use( 28 | r => r, 29 | async error => { 30 | if ( 31 | !this.refreshToken || 32 | error.response.status !== 401 || 33 | error.config.retry 34 | ) { 35 | throw error; 36 | } 37 | 38 | if (!this.refreshRequest) { 39 | this.refreshRequest = this.client.post("/auth/refresh", { 40 | refreshToken: this.refreshToken, 41 | }); 42 | } 43 | const { data } = await this.refreshRequest; 44 | this.token = data.token; 45 | this.refreshToken = data.refreshToken; 46 | const newRequest = { 47 | ...error.config, 48 | retry: true, 49 | }; 50 | 51 | return this.client(newRequest); 52 | } 53 | ); 54 | } 55 | 56 | async login({ login, password }) { 57 | const { data } = await this.client.post("/auth/login", { login, password }); 58 | this.token = data.token; 59 | this.refreshToken = data.refreshToken; 60 | } 61 | 62 | logout() { 63 | this.token = null; 64 | this.refreshToken = null; 65 | } 66 | 67 | getUsers() { 68 | return this.client("/users").then(({ data }) => data); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | import { test } from "ava"; 2 | import axios from "axios"; 3 | import MockAdapter from "axios-mock-adapter"; 4 | 5 | import Api from "../src"; 6 | 7 | test.beforeEach(t => { 8 | const client = axios.create(); 9 | t.context.mock = new MockAdapter(client); 10 | t.context.api = new Api({ client }); 11 | }); 12 | 13 | test("Login captures token information", async t => { 14 | const { mock, api } = t.context; 15 | const LOGIN_REQUEST = { 16 | login: "foo", 17 | password: "foo", 18 | }; 19 | const LOGIN_RESPONSE = { 20 | token: "TOKEN", 21 | refreshToken: "REFRESH_TOKEN", 22 | }; 23 | 24 | mock.onPost("/auth/login", LOGIN_REQUEST).reply(200, LOGIN_RESPONSE); 25 | mock.onGet("/users").reply(200, []); 26 | 27 | await api.login(LOGIN_REQUEST); 28 | await api.getUsers(); 29 | 30 | t.is(mock.history.get.length, 1); 31 | t.is( 32 | mock.history.get[0].headers.Authorization, 33 | `Bearer ${LOGIN_RESPONSE.token}` 34 | ); 35 | }); 36 | 37 | test("Logout removes token information", async t => { 38 | const { mock, api } = t.context; 39 | const LOGIN_REQUEST = { 40 | login: "foo", 41 | password: "foo", 42 | }; 43 | const LOGIN_RESPONSE = { 44 | token: "TOKEN", 45 | refreshToken: "REFRESH_TOKEN", 46 | }; 47 | 48 | mock.onPost("/auth/login", LOGIN_REQUEST).reply(200, LOGIN_RESPONSE); 49 | mock.onGet("/users").reply(200, []); 50 | 51 | await api.login(LOGIN_REQUEST); 52 | await api.logout(); 53 | await api.getUsers(); 54 | 55 | t.is(mock.history.get.length, 1); 56 | t.falsy(mock.history.get[0].headers.Authorization); 57 | }); 58 | 59 | test("Correctly retries request when got 401 with new token", async t => { 60 | const { mock, api } = t.context; 61 | const LOGIN_REQUEST = { 62 | login: "foo", 63 | password: "foo", 64 | }; 65 | const LOGIN_RESPONSE = { 66 | token: "TOKEN", 67 | refreshToken: "REFRESH_TOKEN", 68 | }; 69 | 70 | const REFRESH_REQUEST = { 71 | refreshToken: LOGIN_RESPONSE.refreshToken, 72 | }; 73 | const REFRESH_RESPONSE = { 74 | token: "TOKEN2", 75 | refreshToken: "REFRESH_TOKEN2", 76 | }; 77 | 78 | mock.onPost("/auth/login", LOGIN_REQUEST).reply(200, LOGIN_RESPONSE); 79 | mock 80 | .onPost("/auth/refresh", REFRESH_REQUEST) 81 | .replyOnce(200, REFRESH_RESPONSE); 82 | mock.onGet("/users").reply(config => { 83 | const { Authorization: auth } = config.headers; 84 | if (auth === `Bearer ${LOGIN_RESPONSE.token}`) { 85 | return [401]; 86 | } 87 | if (auth === `Bearer ${REFRESH_RESPONSE.token}`) { 88 | return [200, []]; 89 | } 90 | return [404]; 91 | }); 92 | 93 | await api.login(LOGIN_REQUEST); 94 | await api.getUsers(); 95 | t.is(mock.history.get.length, 2); 96 | t.is( 97 | mock.history.get[1].headers.Authorization, 98 | `Bearer ${REFRESH_RESPONSE.token}` 99 | ); 100 | }); 101 | 102 | test("Correctly fails request when got non-401 error", async t => { 103 | const { mock, api } = t.context; 104 | mock.onGet("/users").reply(404); 105 | await t.throws(async () => { 106 | await api.getUsers(); 107 | }); 108 | }); 109 | 110 | test("Does not consumes token more than once", async t => { 111 | const { mock, api } = t.context; 112 | const LOGIN_REQUEST = { 113 | login: "foo", 114 | password: "foo", 115 | }; 116 | const LOGIN_RESPONSE = { 117 | token: "TOKEN", 118 | refreshToken: "REFRESH_TOKEN", 119 | }; 120 | 121 | const REFRESH_REQUEST = { 122 | refreshToken: LOGIN_RESPONSE.refreshToken, 123 | }; 124 | const REFRESH_RESPONSE = { 125 | token: "TOKEN2", 126 | refreshToken: "REFRESH_TOKEN2", 127 | }; 128 | 129 | mock.onPost("/auth/login", LOGIN_REQUEST).reply(200, LOGIN_RESPONSE); 130 | mock 131 | .onPost("/auth/refresh", REFRESH_REQUEST) 132 | .replyOnce(200, REFRESH_RESPONSE); 133 | 134 | mock.onGet("/users").reply(config => { 135 | const { Authorization: auth } = config.headers; 136 | if (auth === `Bearer ${LOGIN_RESPONSE.token}`) { 137 | return [401]; 138 | } 139 | if (auth === `Bearer ${REFRESH_RESPONSE.token}`) { 140 | return [200, []]; 141 | } 142 | return [404]; 143 | }); 144 | 145 | await api.login(LOGIN_REQUEST); 146 | await Promise.all([api.getUsers(), api.getUsers()]); 147 | t.is( 148 | mock.history.post.filter(({ url }) => url === "/auth/refresh").length, 149 | 1 150 | ); 151 | }); 152 | --------------------------------------------------------------------------------