Skip to content

Commit

Permalink
feat: added jwt authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
leeuwis committed Apr 16, 2021
1 parent a87f0b8 commit 310f24d
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 27 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
},
"dependencies": {
"cookie": "^0.4.1",
"jsonwebtoken": "^8.5.1",
"safe-compare": "^1.1.4"
},
"devDependencies": {
Expand All @@ -61,9 +62,11 @@
"@types/express": "4.17.11",
"@types/jest": "26.0.20",
"@types/jest-axe": "^3.5.1",
"@types/jsonwebtoken": "^8.5.1",
"@types/node": "14.14.21",
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0",
"@types/safe-compare": "^1.1.0",
"@types/testing-library__jest-dom": "5.9.5",
"@typescript-eslint/eslint-plugin": "4.13.0",
"@typescript-eslint/parser": "4.13.0",
Expand Down
35 changes: 21 additions & 14 deletions src/api/__tests__/loginHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ describe('[api] loginHandler', () => {
await loginHandler('password')(req as any, res as any);

expect(res._getStatusCode()).toBe(200);
expect(res._getHeaders()).toHaveProperty(
'set-cookie',
'next-password-protect=cGFzc3dvcmQ%3D; Path=/; HttpOnly',
);
expect(res._getHeaders()).toMatchObject({
'set-cookie': expect.stringMatching(
/^next-password-protect=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\..+\..+; Path=\/; HttpOnly$/,
),
});

jest.restoreAllMocks();
});
Expand All @@ -36,12 +37,17 @@ describe('[api] loginHandler', () => {
);

expect(res._getStatusCode()).toBe(200);
expect(res._getHeaders()).toHaveProperty(
'set-cookie',
`next-password-protect=cGFzc3dvcmQ%3D; Max-Age=${
maxAge / 1000
}; Path=/; Expires=${new Date(now + maxAge).toUTCString()}; HttpOnly`,
);
expect(res._getHeaders()).toMatchObject({
'set-cookie': expect.stringMatching(
new RegExp(
`^next-password-protect=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\..+\\..+; Max-Age=${
maxAge / 1000
}; Path=\\/; Expires=${new Date(
now + maxAge,
).toUTCString()}; HttpOnly$`,
),
),
});

jest.restoreAllMocks();
});
Expand All @@ -58,10 +64,11 @@ describe('[api] loginHandler', () => {
);

expect(res._getStatusCode()).toBe(200);
expect(res._getHeaders()).toHaveProperty(
'set-cookie',
`next-password-protect=cGFzc3dvcmQ%3D; Path=/; HttpOnly; Secure`,
);
expect(res._getHeaders()).toMatchObject({
'set-cookie': expect.stringMatching(
/^next-password-protect=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\..+\..+; Path=\/; HttpOnly; Secure$/,
),
});

jest.restoreAllMocks();
});
Expand Down
18 changes: 14 additions & 4 deletions src/api/__tests__/passwordCheckHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EventEmitter } from 'events';
import jwt from 'jsonwebtoken';
import { createMocks } from 'node-mocks-http';

import { passwordCheckHandler } from '../passwordCheckHandler';
Expand All @@ -9,7 +10,10 @@ describe('[api] passwordCheckHandler', () => {
{
method: 'GET',
headers: {
cookie: 'next-password-protect=cGFzc3dvcmQ%3D; Path=/; HttpOnly',
cookie: `next-password-protect=${jwt.sign(
{},
'password',
)}; Path=/; HttpOnly`,
},
},
{ eventEmitter: EventEmitter },
Expand Down Expand Up @@ -42,12 +46,15 @@ describe('[api] passwordCheckHandler', () => {
jest.restoreAllMocks();
});

it('should fail with incorrect cookie', async () => {
it('should fail with incorrect JWT', async () => {
const { req, res } = createMocks(
{
method: 'GET',
headers: {
cookie: 'next-password-protect=incorrect; Path=/; HttpOnly',
cookie: `next-password-protect=${jwt.sign(
{},
'incorrect',
)}; Path=/; HttpOnly`,
},
},
{ eventEmitter: EventEmitter },
Expand Down Expand Up @@ -78,7 +85,10 @@ describe('[api] passwordCheckHandler', () => {
{
method: 'GET',
headers: {
cookie: 'next-password-protect=cGFzc3dvcmQ%3D; Path=/; HttpOnly',
cookie: `next-password-protect=${jwt.sign(
{},
'password',
)}; Path=/; HttpOnly`,
},
},
{ eventEmitter: EventEmitter },
Expand Down
7 changes: 6 additions & 1 deletion src/api/loginHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import compare from 'safe-compare';

import { sendJson } from './sendJson';
Expand Down Expand Up @@ -33,7 +34,11 @@ export const loginHandler = (
setCookie(
res,
options?.cookieName || 'next-password-protect',
Buffer.from(password).toString('base64'),
/* NOTE: It's not usual to use the password as JWT secret, but since you already
* have access to the environment when you know the password, in this specific
* use case it doesn't add any value for an intruder if the secret is known.
*/
jwt.sign({}, password),
{
httpOnly: true,
sameSite: options?.cookieSameSite || false,
Expand Down
22 changes: 14 additions & 8 deletions src/api/passwordCheckHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cookie from 'cookie';
import { Request, Response } from 'express';
import compare from 'safe-compare';
import jwt from 'jsonwebtoken';

import { sendJson } from './sendJson';

Expand All @@ -27,17 +27,23 @@ export const passwordCheckHandler = (
const cookies = cookie.parse(req.headers.cookie);
const cookieName = options?.cookieName || 'next-password-protect';

if (
cookies?.[cookieName] &&
compare(cookies?.[cookieName], Buffer.from(password).toString('base64'))
) {
sendJson(res, 200);
return;
}
/* NOTE: It's not usual to use the password as JWT secret, but since you already
* have access to the environment when you know the password, in this specific
* use case it doesn't add any value for an intruder if the secret is known.
*/
jwt.verify(cookies?.[cookieName], password);

sendJson(res, 200);
return;
}

sendJson(res, 401);
} catch (err) {
if (err.name === 'JsonWebTokenError') {
sendJson(res, 401);
return;
}

sendJson(res, 500, { message: err?.message || 'An error has occured.' });
}
};
82 changes: 82 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=

"@types/jsonwebtoken@^8.5.1":
version "8.5.1"
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#56958cb2d80f6d74352bd2e501a018e2506a8a84"
integrity sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw==
dependencies:
"@types/node" "*"

"@types/mime@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
Expand Down Expand Up @@ -2582,6 +2589,11 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==

"@types/safe-compare@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@types/safe-compare/-/safe-compare-1.1.0.tgz#47ed9b9ca51a3a791b431cd59b28f47fa9bf1224"
integrity sha512-1ri+LJhh0gRxIa37IpGytdaW7yDEHeJniBSMD1BmitS07R1j63brcYCzry+l0WJvGdEKQNQ7DYXO2epgborWPw==

"@types/serve-static@*":
version "1.13.9"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e"
Expand Down Expand Up @@ -3781,6 +3793,11 @@ buffer-alloc@^1.2.0:
buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0"

[email protected]:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=

buffer-fill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
Expand Down Expand Up @@ -5218,6 +5235,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"

[email protected]:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"

[email protected], editor@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742"
Expand Down Expand Up @@ -7951,6 +7975,22 @@ jsonparse@^1.2.0, jsonparse@^1.3.1:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=

jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"

jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
Expand All @@ -7969,6 +8009,23 @@ jsprim@^1.2.2:
array-includes "^3.1.2"
object.assign "^4.1.2"

jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"

jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"

kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
Expand Down Expand Up @@ -8369,11 +8426,31 @@ lodash.escaperegexp@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=

lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=

lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=

lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=

lodash.ismatch@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=

lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=

lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
Expand All @@ -8394,6 +8471,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==

lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=

lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
Expand Down

0 comments on commit 310f24d

Please sign in to comment.