Lesson 56-Fastify Authentication and Authorization

Fastify with JWT Integration

Preparation

Ensure Fastify and the fastify-jwt plugin are installed:

npm install fastify @fastify/jwt

Configuring the JWT Plugin

Register the fastify-jwt plugin with a secret key for signing and verifying JWTs:

const fastify = require('fastify')({ logger: true });

fastify.register(require('@fastify/jwt'), {
    secret: 'your-secret-key'
});

Creating User and Login Endpoint

Create a simple user model and login endpoint, assuming a user database exists:

const users = [
    { username: 'john', password: 'password123' },
    { username: 'jane', password: 'secret456' }
];

fastify.post('/login', async (request, reply) => {
    const { username, password } = request.body;
    const user = users.find(u => u.username === username && u.password === password);
    if (!user) {
        throw new Error('Invalid credentials');
    }

    const token = await reply.jwtSign({ sub: user.username });
    return { token };
});

Protecting Routes

Use the authenticate method to protect routes:

fastify.get('/protected', { preValidation: [fastify.authenticate] }, async (request, reply) => {
    return { user: request.user.sub };
});

Parsing JWT

The fastify-jwt plugin parses JWTs and attaches the result to request.user:

fastify.get('/profile', { preValidation: [fastify.authenticate] }, async (request, reply) => {
    return { username: request.user.sub, email: request.user.email };
});

Error Handling

Handle invalid or expired JWTs with a global error handler:

fastify.setErrorHandler((error, request, reply) => {
    if (error.statusCode === 401) {
        reply.code(401).send({ error: 'Unauthorized' });
    } else {
        reply.code(500).send({ error: 'Internal server error' });
    }
});

Complete Example

A full example of JWT integration:

const fastify = require('fastify')({ logger: true });

fastify.register(require('@fastify/jwt'), {
    secret: 'your-secret-key'
});

const users = [
    { username: 'john', password: 'password123', email: 'john@example.com' },
    { username: 'jane', password: 'secret456', email: 'jane@example.com' }
];

fastify.post('/login', async (request, reply) => {
    const { username, password } = request.body;
    const user = users.find(u => u.username === username && u.password === password);
    if (!user) {
        throw new Error('Invalid credentials');
    }

    const token = await reply.jwtSign({ sub: user.username, email: user.email });
    return { token };
});

fastify.get('/protected', { preValidation: [fastify.authenticate] }, async (request, reply) => {
    return { user: request.user.sub };
});

fastify.setErrorHandler((error, request, reply) => {
    if (error.statusCode === 401) {
        reply.code(401).send({ error: 'Unauthorized' });
    } else {
        reply.code(500).send({ error: 'Internal server error' });
    }
});

const start = async () => {
    try {
        await fastify.listen({ port: 3000 });
    } catch (err) {
        fastify.log.error(err);
        process.exit(1);
    }
};
start();

Fastify with OAuth2 Integration

Installing Required Packages

Install packages for OAuth2, such as fastify-oauth2 or node-oauth2-server. Here, we use node-oauth2-server to build an OAuth2 server:

npm install node-oauth2-server fastify @fastify/plugin

Configuring OAuth2 Server

Set up an OAuth2 server with models for User, Client, AccessToken, and RefreshToken:

const Fastify = require('fastify');
const OAuth2Server = require('node-oauth2-server');
const fastifyPlugin = require('@fastify/plugin');

const User = require('./models/user');
const Client = require('./models/client');
const AccessToken = require('./models/access_token');
const RefreshToken = require('./models/refresh_token');

const fastify = Fastify();

const oauth = new OAuth2Server({
    model: {
        getClient: Client.getClient,
        saveAuthorizationCode: Client.saveAuthorizationCode,
        getAuthorizationCode: Client.getAuthorizationCode,
        getAccessToken: AccessToken.getAccessToken,
        saveToken: AccessToken.saveToken,
        getUser: User.getUser,
        getRefreshToken: RefreshToken.getRefreshToken,
        saveRefreshToken: RefreshToken.saveRefreshToken
    },
    grants: ['authorization_code', 'refresh_token'],
    debug: true
});

fastify.register(fastifyPlugin(async (instance) => {
    instance.decorate('oauth', oauth);
}));

fastify.get('/oauth/authorize', async (request, reply) => {
    return oauth.authorize(request, reply);
});

fastify.post('/oauth/token', async (request, reply) => {
    return oauth.token(request, reply);
});

const start = async () => {
    try {
        await fastify.listen({ port: 3000 });
    } catch (err) {
        fastify.log.error(err);
        process.exit(1);
    }
};
start();

Implementing Authorization Logic

Implement user authorization, typically involving a UI for consent:

fastify.get('/oauth/authorize', async (request, reply) => {
    const { client_id, redirect_uri } = request.query;
    // Render UI for user consent (hypothetical)
    if (userConsents) {
        const code = await oauth.authorize({
            client: { id: client_id },
            redirectUri: redirect_uri,
            user: request.user,
            responseType: 'code'
        });
        reply.redirect(`${redirect_uri}?code=${code}`);
    } else {
        reply.code(403).send({ error: 'Consent denied' });
    }
});

Using OAuth2 Client

Use Fastify as an OAuth2 client to obtain access tokens:

const fastify = require('fastify')();
const { OAuth2Client } = require('simple-oauth2');

const client = new OAuth2Client({
    client: {
        id: 'your-client-id',
        secret: 'your-client-secret'
    },
    auth: {
        tokenHost: 'https://provider.example.com',
        authorizePath: '/oauth/authorize',
        tokenPath: '/oauth/token'
    }
});

fastify.get('/login', async (request, reply) => {
    const authorizationUri = client.authorizationCode.authorizeURL({
        redirect_uri: 'http://localhost:3000/callback',
        scope: 'read write'
    });
    reply.redirect(authorizationUri);
});

fastify.get('/callback', async (request, reply) => {
    const { code } = request.query;
    try {
        const token = await client.authorizationCode.getToken({
            code,
            redirect_uri: 'http://localhost:3000/callback'
        });
        return { token };
    } catch (err) {
        reply.code(500).send({ error: 'Failed to obtain token' });
    }
});

Protecting Resources

Verify access tokens to protect resources:

fastify.get('/protected-resource', async (request, reply) => {
    const accessToken = request.headers.authorization?.split(' ')[1];
    const token = await AccessToken.findOne({ token: accessToken });
    if (!token) {
        reply.code(401).send({ error: 'Unauthorized' });
    } else {
        reply.send({ resource: 'Protected resource' });
    }
});

Share your love