Skip to content

Commit 7db1d64

Browse files
committed
Harden JWT auth flow and modernize project stack
1 parent 61bdb85 commit 7db1d64

21 files changed

Lines changed: 1812 additions & 3091 deletions

.env.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copy to .env and fill in. .env is gitignored.
2+
# In production every one of JWT_SECRET, DB_USER and DB_PASSWORD is mandatory --
3+
# the app throws on startup if any is missing.
4+
5+
NODE_ENV=development
6+
PORT=8000
7+
8+
# Signing key for issued JWTs. Generate one with:
9+
# node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
10+
JWT_SECRET=
11+
JWT_DURATION=2 hours
12+
13+
DB_HOST=localhost
14+
DB_PORT=3306
15+
DB_NAME=jwt_dev
16+
DB_USER=
17+
DB_PASSWORD=

.eslintrc.json

Lines changed: 0 additions & 18 deletions
This file was deleted.

.github/dependabot.yml

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ updates:
1616
patterns: ["*"]
1717
ignore:
1818
# The eslint / eslint-config-airbnb-base pins that used to live here are gone:
19-
# Phase 1a dropped airbnb-base entirely in favour of flat-config eslint 10, so
20-
# eslint majors are free to flow again.
19+
# airbnb-base was dropped entirely for flat-config eslint 10, so eslint majors
20+
# are free to flow again.
2121
#
2222
# joi is the constrained one now. express-validation 4 declares a peer of
2323
# joi ^17.6.0, so a bump to joi 18 reintroduces exactly the class of `npm ci`
@@ -28,14 +28,5 @@ updates:
2828
- dependency-name: "joi"
2929
update-types: ["version-update:semver-major"]
3030

31-
# Valid as of Phase 1a: .github/workflows/ci.yml now exists. Declaring this ecosystem
32-
# with no workflows directory is what made Dependabot error on every run.
33-
- package-ecosystem: "github-actions"
34-
directory: "/"
35-
schedule:
36-
interval: "weekly"
37-
day: "monday"
38-
open-pull-requests-limit: 2
39-
groups:
40-
github-actions:
41-
patterns: ["*"]
31+
# NOTE: no "github-actions" ecosystem block here on purpose. Dependabot errors on every
32+
# run if that ecosystem is declared while the repo has no .github/workflows/ directory.

README.md

Lines changed: 135 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,140 @@
11
# Node JWT
22

3+
[![CI](https://github.com/murraco/node-jwt/actions/workflows/ci.yml/badge.svg)](https://github.com/murraco/node-jwt/actions/workflows/ci.yml)
4+
5+
A small JWT authentication service built with Express, Sequelize and MySQL.
6+
37
# Stack
48

5-
![](https://img.shields.io/badge/node_8-✓-blue.svg)
6-
![](https://img.shields.io/badge/ES6-✓-blue.svg)
7-
![](https://img.shields.io/badge/express-✓-blue.svg)
8-
![](https://img.shields.io/badge/sequelize-✓-blue.svg)
9+
![](https://img.shields.io/badge/node_20+-✓-blue.svg)
10+
![](https://img.shields.io/badge/express_4-✓-blue.svg)
11+
![](https://img.shields.io/badge/sequelize_6-✓-blue.svg)
12+
![](https://img.shields.io/badge/joi-✓-blue.svg)
913
![](https://img.shields.io/badge/mocha-✓-blue.svg)
1014

15+
## ⚠️ Breaking changes
16+
17+
If you are upgrading from an older clone, three things changed:
18+
19+
1. **Credentials moved from the query string to the request body.** `POST /auth` and
20+
`POST /users` used to read `?username=…&password=…`. Query strings are recorded in
21+
access logs, proxy logs and browser history, so passwords are now read from the JSON
22+
body only. A query-string login returns `400`.
23+
2. **Configuration moved to environment variables.** `config/env/*.js` no longer contains
24+
a committed secret. Copy `.env.example` and supply your own. In production the app
25+
**throws on startup** if `JWT_SECRET`, `DB_USER` or `DB_PASSWORD` is missing.
26+
3. **`PUT /users/:userId` only accepts `username`, and only from the account's owner.**
27+
It previously wrote the entire request body to the row, and any authenticated user
28+
could target any id.
29+
30+
If you have an existing deployment, treat the old `$eCrEt` signing key as compromised —
31+
it was public in this repository's history. Rotating it invalidates all issued tokens.
32+
33+
# Quick start
34+
35+
```bash
36+
git clone https://github.com/murraco/node-jwt && cd node-jwt
37+
cp .env.example .env # then fill in JWT_SECRET, DB_USER, DB_PASSWORD
38+
npm ci
39+
```
40+
41+
Start MySQL and create the databases:
42+
43+
```bash
44+
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root mysql:8
45+
docker exec -i $(docker ps -qf ancestor=mysql:8) mysql -uroot -proot <<'SQL'
46+
CREATE DATABASE IF NOT EXISTS jwt;
47+
CREATE DATABASE IF NOT EXISTS jwt_dev;
48+
CREATE DATABASE IF NOT EXISTS jwt_test;
49+
SQL
50+
```
51+
52+
Run it:
53+
54+
```bash
55+
npm start # node index.js
56+
npm run dev # nodemon, live reload
57+
npm test # mocha
58+
npm run lint # eslint
59+
```
60+
61+
Check it is up — `http://localhost:8000/api-status` should return `{ "status": "ok" }`.
62+
Set `PORT` to use a different port.
63+
64+
# Endpoints
65+
66+
| Method | Path | Auth | Body |
67+
|---|---|---|---|
68+
| `GET` | `/api-status` |||
69+
| `POST` | `/users` || `username`, `password` |
70+
| `GET` | `/users` | Bearer ||
71+
| `GET` | `/users/:userId` | Bearer ||
72+
| `PUT` | `/users/:userId` | Bearer, owner only | `username` |
73+
| `DELETE` | `/users/:userId` | Bearer, owner only ||
74+
| `POST` | `/auth` || `username`, `password` |
75+
| `POST` | `/auth/refresh` || `username`, `refresh_token` |
76+
77+
Register a user:
78+
79+
```bash
80+
curl -X POST http://localhost:8000/users \
81+
-H 'Content-Type: application/json' \
82+
-d '{"username":"admin","password":"admin1"}'
83+
```
84+
85+
Sign in — returns a `token` and a `refresh_token`:
86+
87+
```bash
88+
curl -X POST http://localhost:8000/auth \
89+
-H 'Content-Type: application/json' \
90+
-d '{"username":"admin","password":"admin1"}'
91+
```
92+
93+
Use the token:
94+
95+
```bash
96+
curl http://localhost:8000/users -H 'Authorization: Bearer <JWT_TOKEN>'
97+
```
98+
99+
```json
100+
[
101+
{
102+
"id": 1,
103+
"username": "admin",
104+
"created_at": "2026-07-31T21:42:01.000Z",
105+
"updated_at": "2026-07-31T21:52:05.000Z"
106+
}
107+
]
108+
```
109+
110+
Refresh tokens rotate: every issued JWT replaces the stored `refresh_token`, so a given
111+
`refresh_token` can be redeemed exactly once.
112+
113+
# Configuration
114+
115+
Every value is read from the environment — see `.env.example`.
116+
117+
| Variable | Default (dev/test) | Required in production |
118+
|---|---|---|
119+
| `JWT_SECRET` | insecure placeholder | **yes** |
120+
| `JWT_DURATION` | `2 hours` | no |
121+
| `DB_HOST` | `localhost` | no |
122+
| `DB_PORT` | `3306` | no |
123+
| `DB_NAME` | `jwt_dev` / `jwt_test` | no |
124+
| `DB_USER` | `root` | **yes** |
125+
| `DB_PASSWORD` | `root` | **yes** |
126+
| `PORT` | `8000` | no |
127+
128+
Generate a signing key with:
129+
130+
```bash
131+
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
132+
```
133+
134+
# Introduction (https://jwt.io)
135+
136+
I have a great introduction to JWT in one of my other repositories, click [here](https://github.com/murraco/spring-boot-jwt#introduction-httpsjwtio) to take a look!
137+
11138
# File structure
12139

13140
```
@@ -43,140 +170,18 @@ node-jwt/
43170
├── test/
44171
│ ├── auth.test.js
45172
│ └── user.test.js
46-
47-
├── .eslintrc * ESLint configuration file
173+
174+
├── .env.example * Template for local configuration
175+
├── .github/workflows/ci.yml * Lint + tests on Node 20 and 22 against MySQL 8
48176
├── .gitignore * Example git ignore file
177+
├── eslint.config.js * ESLint flat configuration file
49178
├── index.js * Entry point of our Node's app
50179
├── LICENSE * MIT License
51180
├── package.json * Defines our JavaScript dependencies
52181
├── package-lock.json * Defines our exact JavaScript dependencies tree
53182
└── README.md * This file
54183
```
55184

56-
# Introduction (https://jwt.io)
57-
58-
I have a great introduction to JWT in one of my other repositories, click [here](https://github.com/murraco/spring-boot-jwt#introduction-httpsjwtio) to take a look!
59-
60-
## How to use this code?
61-
62-
1. Make sure you have the latest stable version of Node.js installed
63-
64-
```
65-
$ sudo npm cache clean -f
66-
$ sudo npm install -g n
67-
$ sudo n stable
68-
```
69-
70-
2. Configure your database and jsonwebtoken in `config/env`. E.g.:
71-
72-
```javascript
73-
module.exports = {
74-
mysql: {
75-
host: 'localhost',
76-
port: 3306,
77-
database: 'jwt_dev',
78-
username: 'root',
79-
password: 'root',
80-
},
81-
jwt: {
82-
jwtSecret: '$eCrEt',
83-
jwtDuration: '2 hours',
84-
},
85-
};
86-
```
87-
88-
3. Fork this repository and clone it
89-
90-
```
91-
$ git clone https://github.com/<your-user>/node-jwt
92-
```
93-
94-
4. Navigate into the folder
95-
96-
```
97-
$ cd node-jwt
98-
```
99-
5. Install NPM dependencies
100-
101-
```
102-
$ npm install
103-
```
104-
105-
6. Make sure you have a MySQL DB up and running, if you don't, using docker is the easiest way
106-
107-
```
108-
$ docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql
109-
```
110-
Login into the container, update the root user and create databases
111-
112-
```
113-
$ docker exec -it <CONTAINER ID> mysql -uroot -proot
114-
$ ALTER USER root IDENTIFIED WITH mysql_native_password BY 'root';
115-
$ CREATE DATABASE jwt;
116-
$ CREATE DATABASE jwt_dev;
117-
$ CREATE DATABASE jwt_test;
118-
```
119-
120-
7. Run the project
121-
122-
```
123-
$ node index.js
124-
```
125-
126-
8. Or use `nodemon` for live-reload
127-
128-
```
129-
$ npm start
130-
```
131-
132-
> `npm start` will run `nodemon index.js`.
133-
134-
9. Navigate to `http://localhost:8000/api-status` in your browser to check you're seing the following response
135-
136-
```javascript
137-
{ "status": "ok" }
138-
```
139-
140-
> The port can be changed by the setting the environment variable `PORT`
141-
142-
10. If you want to execute the tests
143-
144-
```
145-
$ npm test
146-
```
147-
148-
> `npm test` will run `mocha`.
149-
150-
11. If you want to test it manually you can do it with the following commands
151-
152-
Register a new user:
153-
```
154-
curl -X POST 'http://localhost:8000/users?username=admin2&password=admin'
155-
```
156-
157-
Sign in with the new user credentials:
158-
```
159-
curl -X POST 'http://localhost:8000/auth?username=admin&password=admin'
160-
```
161-
162-
Copy the token and send a request to get all current users:
163-
```
164-
curl -X GET http://localhost:8000/users -H 'Authorization: Bearer <JWT_TOKEN>
165-
```
166-
167-
12. And that's it, congrats! You should get a similar response to this one, meaning that you're now authenticated
168-
169-
```json
170-
[
171-
{
172-
"id": 1,
173-
"username": "admin",
174-
"createdAt": "2020-07-21T21:42:01.000Z",
175-
"updatedAt": "2020-07-21T21:52:05.000Z"
176-
}
177-
]
178-
```
179-
180185
# Contribution
181186

182187
- Report issues

0 commit comments

Comments
 (0)