|
1 | 1 | # Node JWT |
2 | 2 |
|
| 3 | +[](https://github.com/murraco/node-jwt/actions/workflows/ci.yml) |
| 4 | + |
| 5 | +A small JWT authentication service built with Express, Sequelize and MySQL. |
| 6 | + |
3 | 7 | # Stack |
4 | 8 |
|
5 | | - |
6 | | - |
7 | | - |
8 | | - |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | + |
9 | 13 |  |
10 | 14 |
|
| 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 | + |
11 | 138 | # File structure |
12 | 139 |
|
13 | 140 | ``` |
@@ -43,140 +170,18 @@ node-jwt/ |
43 | 170 | ├── test/ |
44 | 171 | │ ├── auth.test.js |
45 | 172 | │ └── 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 |
48 | 176 | ├── .gitignore * Example git ignore file |
| 177 | +├── eslint.config.js * ESLint flat configuration file |
49 | 178 | ├── index.js * Entry point of our Node's app |
50 | 179 | ├── LICENSE * MIT License |
51 | 180 | ├── package.json * Defines our JavaScript dependencies |
52 | 181 | ├── package-lock.json * Defines our exact JavaScript dependencies tree |
53 | 182 | └── README.md * This file |
54 | 183 | ``` |
55 | 184 |
|
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 | | - |
180 | 185 | # Contribution |
181 | 186 |
|
182 | 187 | - Report issues |
|
0 commit comments