Authentication with JWT
Only the blog's author should write and delete posts. An API keeps no sessions, so after login the server gives the client a token — a JWT (JSON Web Token), a signed string with user data. The client sends the token with every request, and the server checks the signature.
Installing and settings
npm install @fastify/jwtAdd the author's name and password to .env and to the @fastify/env schema from the configuration lesson:
ADMIN_NAME=anna
ADMIN_PASSWORD=a-very-long-password
JWT_SECRET=a-long-random-string-for-signingThe auth plugin
Create src/plugins/auth.js:
// src/plugins/auth.js
import fp from 'fastify-plugin';
import fastifyJwt from '@fastify/jwt';
async function auth(app) {
app.register(fastifyJwt, {
secret: app.config.JWT_SECRET,
sign: { expiresIn: '1h' },
});
app.decorate('authenticate', async (request) => {
await request.jwtVerify();
});
}
export default fp(auth);@fastify/jwt adds app.jwt.sign() to issue a token and request.jwtVerify() to check one. If the token is missing or wrong, jwtVerify() throws an error with code 401. Register the plugin in src/app.js after @fastify/env.
Logging in
Create src/routes/auth.js and register it in src/app.js:
// src/routes/auth.js
export default async function authRoutes(app) {
app.post('/login', async (request) => {
const { name, password } = request.body;
if (name !== app.config.ADMIN_NAME || password !== app.config.ADMIN_PASSWORD) {
throw app.httpErrors.unauthorized('Wrong name or password');
}
const token = app.jwt.sign({ name });
return { token };
});
}curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"name":"anna","password":"a-very-long-password"}'The server returns {"token":"eyJhbGciOi..."} — a token valid for one hour.
Protecting routes
Add app.authenticate to the onRequest hook of the routes that change data:
// src/routes/posts.js
app.post('/posts', {
onRequest: [app.authenticate],
schema: { body: postInput },
}, async (request, reply) => {
request.log.info({ author: request.user.name }, 'New post');
// ...create the post as in the CRUD lesson
});After a successful check the token data is in request.user. A request without a token gets 401, and with a token it passes:
curl -X POST http://localhost:3000/posts \
-H "Authorization: Bearer eyJhbGciOi..." \
-H "Content-Type: application/json" \
-d '{"title":"A protected post","body":"Text"}'Add onRequest to the PUT and DELETE routes the same way. Reading posts stays open.
Common mistake
A password in .env is fine only for a learning blog with one author. When there are many users, their passwords are stored in the database only as a hash — for example, with the bcrypt or argon2 packages — and never as plain text.