No description
  • JavaScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Azer 9fd8f9bac6 Require the Bearer prefix, hash the passwords, and clear the advisories
`/protected` accepted a token with no scheme at all. The middleware pulled
the token out with `authHeader.replace('Bearer ', '')`, and replace leaves a
header that never had the prefix untouched, so `Authorization: <jwt>` passed
exactly like `Authorization: Bearer <jwt>`. Confirmed against the running
server before and after; the test suite now asserts the refusal.

The sample stored its passwords as passwords. A sample is the first place
somebody copies a pattern from, and a plain-text password column is the wrong
pattern to hand them, so they are scrypt hashes now - `scrypt:<salt>:<hash>`,
with scrypt from node:crypto, which costs no dependency. Three things that
go with it:

- the asynchronous scrypt, because scryptSync blocks the event loop, and so
  the whole server, for the length of every login attempt
- crypto.timingSafeEqual, because a byte-by-byte === leaks through its own
  duration how much of the hash was right
- a hash computed even for an unknown username, against a record nobody has
  the password to, so response time does not say which accounts exist

npm audit went from 7 findings to zero. Two of them mattered here more than
most: a critical ReDoS in koa, and `jws: Improperly Verifies HMAC Signature`
in the jsonwebtoken tree - a signature-verification hole inside a sample
about verifying signatures. koa-router, which is unmaintained, was replaced
by @koa/router, and 15.7 rather than 14 because 14 has an access-control
bypass of its own.

Also:

- Unhandled errors reached Koa's default handler. There is an error
  middleware now, which keeps internals out of the response body and logs a
  4xx as one line while still giving a 5xx its stack.
- /login took `username` and `password` on trust. A non-string password -
  `{"password": {"$ne": null}}` - reached scrypt, and a missing body reached
  a destructure. Both are a 400.
- PORT and CORS_ORIGIN are read from the environment; the origin was pinned
  to localhost:3000, so the sample only worked for one front end.
- The server refuses to start when NODE_ENV=production and JWT_SECRET is
  unset. The development fallback exists so the sample runs, not so it ships.
- An occupied port ended in an unhandled exception dump.
- router.allowedMethods() is mounted, so a wrong method gets a 405 rather
  than a 404.

Added test/smoke.mjs, 14 checks that walk the whole flow and every refusal -
wrong password, unknown user, missing prefix, missing header, a token signed
with another key, an alg=none token, an expired token - plus a CI workflow
that runs it and checks the production guard. README now documents the
environment, how to try it, and what the sample is trying to get right,
including the one thing it still does not: nothing rate-limits /login.
2026-10-07 03:09:24 +04:00
.forgejo/workflows Require the Bearer prefix, hash the passwords, and clear the advisories 2026-10-07 03:09:24 +04:00
test Require the Bearer prefix, hash the passwords, and clear the advisories 2026-10-07 03:09:24 +04:00
.env.example Require the Bearer prefix, hash the passwords, and clear the advisories 2026-10-07 03:09:24 +04:00
.gitignore basic server init 2023-04-24 23:50:37 +04:00
index.js Require the Bearer prefix, hash the passwords, and clear the advisories 2026-10-07 03:09:24 +04:00
package-lock.json Require the Bearer prefix, hash the passwords, and clear the advisories 2026-10-07 03:09:24 +04:00
package.json Require the Bearer prefix, hash the passwords, and clear the advisories 2026-10-07 03:09:24 +04:00
README.md Require the Bearer prefix, hash the passwords, and clear the advisories 2026-10-07 03:09:24 +04:00

Koa-JWT-Example

just a sample of how JWT works via Koa.JS web framework

Table Of Contents

Built With

  • jsonwebtoken (the JWT library)
  • koa (Koa web framework)
  • koa-bodyparser (Koa middleware for parsing HTTP request body)
  • @koa/router (the Koa router; koa-router itself is unmaintained)
  • @koa/cors
  • node:crypto for password hashing, so that costs no dependency

Usage

npm ci
cp .env.example .env     # optional
npm start
npm test                 # walks the auth flow, including the refusals
Variable Default Meaning
JWT_SECRET insecure-development-key Signing key. Required when NODE_ENV=production, where the server refuses to start without it.
PORT 4000 Port to listen on.
CORS_ORIGIN http://localhost:3000 Origin allowed by CORS.

Try it

TOKEN=$(curl -s -H 'Content-Type: application/json'   -d '{"username":"azer","password":"password1"}'   localhost:4000/login | sed -E 's/.*"token":"([^"]+)".*//')

curl -s -H "Authorization: Bearer $TOKEN" localhost:4000/protected

What this sample tries to get right

A sample is the first place somebody copies a pattern from, so the patterns here are meant to be the ones worth copying:

  • Passwords are stored as scrypt hashes, as scrypt:<salt>:<hash>, never as the passwords. node:crypto provides scrypt, so this needs no dependency. The asynchronous form is used: scryptSync would block the event loop - the whole server - for the length of every hash.
  • The comparison is timingSafeEqual. A byte-by-byte === leaks, through how long it takes, how much of the hash was right.
  • An unknown username costs the same as a known one. A hash is computed either way, so response time does not reveal which accounts exist.
  • The verifying algorithm is named, not taken from the token's own header. A verifier that accepts alg: none is the classic way a JWT check gets talked out of checking anything; the test suite tries exactly that.
  • The Bearer prefix is required. authHeader.replace('Bearer ', '') leaves a header that never had the prefix untouched, which accepted Authorization: <token> as readily as the real thing.
  • The signing key comes from the environment, and the server refuses to start in production without one.

Still missing, because it needs more than a sample's worth of machinery: rate limiting on /login. Nothing here slows down an attacker working through a password list.

Contributing

Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.

  • If you have suggestions for adding or removing projects, feel free to open an issue to discuss it, or directly create a pull request after you edit the README.md file with necessary changes.
  • Please make sure you check your spelling and grammar.
  • Create individual PR for each suggestion.
  • Please also read through the Code Of Conduct before posting your first idea as well.

Creating A Pull Request

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request