Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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/jwt

Add 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-signing

The 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.

Thanks for reading!

Donate to the site — we work hard for you.