Fastify is rapidly gaining traction among Node.js developers, and for good reason! If you're looking for a high-performance and low-overhead way to build web applications and APIs, Fastify is an excellent choice. Its plugin architecture is powerful, but securing those APIs remains a critical concern for developers.
To help address this, we are introducing the
SDK. This new package brings seamless Auth0 integration into your Fastify projects, simplifying API endpoint protection.auth0-fastify-api
So, What is Fastify, Anyway?
For those who might be new to it, Fastify is a web framework for Node.js focused on delivering a strong developer experience with minimal overhead, built around a robust plugin system. Key features include:
- High Performance: Fastify is known as one of the fastest Node.js frameworks, utilizing optimizations like schema-based request/response serialization.
- Extensibility: A rich ecosystem of plugins allows developers to easily add needed functionality.
- Developer Experience: Features like built-in logging, developer-friendly error handling, and first-class TypeScript support contribute to a smoother development process.
- Schema-Powered: Use JSON Schema to validate routes and serialize outputs, leading to more predictable and robust applications.
- Helpful CLI: The
tool streamlines common development tasks like running the app and generating project scaffolds (See fastify-cli on GitHub).fastify-cli
- NestJS Integration: Fastify's performance can also be leveraged within the popular NestJS framework by using it as the underlying HTTP provider (Learn More).
Introducing auth0-fastify-api
auth0-fastify-api
This new SDK is specifically designed to secure Fastify APIs. It functions as a dedicated Fastify plugin to simplify the validation of access tokens issued by Auth0.
Here's what the SDK offers:
- Simple Integration: Sets up as a standard Fastify plugin.
- Automatic Token Validation: Handles the validation of incoming JWT access tokens (sent as Bearer tokens) against your Auth0 tenant configuration.
- Route Protection: Provides a
decorator for easily securing specific routes.requireAuth
- Scope Enforcement: Allows specification of required scopes (permissions) needed to access certain endpoints.
- User Information Access: Makes the validated token payload available on
.request.user
- Token Access Utility: Includes
for accessing the raw token string if necessary.request.getToken()
- Standard Error Handling: Sends standards-compliant
headers on authentication/authorization errors.WWW-Authenticate
Getting Started: Quick Setup
Integrating the SDK involves two main steps:
- Install the SDK:
npm install @auth0/auth0-fastify-api # or yarn add @auth0/auth0-fastify-api
- Register the Plugin:
In your main Fastify application file, register the plugin, providing your Auth0 domain and API audience:
import Fastify from "fastify"; import Auth0 from "@auth0/auth0-fastify-api"; const fastify = Fastify({ logger: true }); fastify.register(Auth0, { domain: "YOUR_AUTH0_DOMAIN", // For example: your-tenant.auth0.com audience: "YOUR_API_AUDIENCE", // The Identifier of your API in Auth0 }); // ... rest of your setup
Remember to replace the placeholder values with your actual Auth0 domain and API identifier.
A Functional Example
Let's look at a functional example that demonstrates setting up a basic server, loading configuration from the environment, and protecting a route.
Create a
fastify
directory anywhere in your system and initialize a Node.js project within it:npm init -y
Within that directory, create a
server.ts
file and follow the instructions in this section to build it up.First, import the necessary modules and use
dotenv
to load configurations like your Auth0 domain and audience from a .env
file, which avoids hardcoding sensitive credentials.import Fastify from "fastify"; import Auth0 from "@auth0/auth0-fastify-api"; import * as dotenv from "dotenv"; dotenv.config(); const fastify = Fastify({ logger: true, }); if (!process.env.AUTH0_DOMAIN || !process.env.AUTH0_AUDIENCE) { fastify.log.error("Auth0 domain or audience not configured in .env file"); process.exit(1); }
It's good to check that the environment variables were loaded correctly.
Next, register the
Auth0
plugin, passing the configuration loaded from the environment variables:fastify.register(Auth0, { domain: process.env.AUTH0_DOMAIN, audience: process.env.AUTH0_AUDIENCE, });
To ensure the
Auth0
plugin has finished loading before defining routes that use its decorators, such as requireAuth
, register your protected routes inside an anonymous plugin function:fastify.register(() => { fastify.get("/", { preHandler: fastify.requireAuth() }, (_, reply) => { reply.send({ ok: true }); }); });
Finally, start the server, listening on the port
3000
:fastify.listen({ port: 3000 }, function (err, address) { if (err) { fastify.log.error(err); process.exit(1); } fastify.log.info(`Server is now listening on ${address}`); });
You can run this short example by following these steps:
Install dependencies:
npm install fastify @auth0/auth0-fastify-api dotenv
Create a
.env
file with your Auth0 details:AUTH0_DOMAIN=your-tenant.auth0.com AUTH0_AUDIENCE=your-api-identifier
You can follow the steps on the "Register APIs" document to learn how to create an API in the Auth0 Dashboard and get the required values: the Auth0 Domain, which is your tenant domain, and the Auth0 Audience, which is a unique identifier for your API.
Run the server:
npx ts-node server.ts
This provides a basic but functional starting point that loads configuration securely and correctly waits for the Auth0 plugin before defining protected routes.
Securing A Route with requireAuth
requireAuth
As you saw earlier, to protect an individual API endpoint, use the
decorator provided by the SDK. Add it as a requireAuth
preHandler
in your route definition:fastify.get( "/private", { preHandler: fastify.requireAuth() }, async (request, reply) => { const user = request.user; return { message: "This is a protected route", user }; } );
Fastify will only reach the body of the route event handler if the token sent in the request authorization header is valid. The decoded token claims of the token are available in the
request.user
object.If a request is made to
/private
without a valid Bearer token in the Authorization
header, the SDK automatically rejects it with a 401 Unauthorized
status.Securing Multiple Routes
Applying the
preHandler
to every route can be repetitive if multiple routes require the same protection. Fastify's plugin system and encapsulation offer an efficient way to apply an authorization guard at the router level to protect multiple routes at once.Group related routes within their own plugin and apply the
hook once using requireAuth
within that plugin's context. All routes defined within that plugin will then inherit the hook.fastify.addHook
Let's see this concept in action.
1. Create a routes plugin:
Within an asynchronous Fastify plugin, you use
fastify.addHook("preHandler", fastify.requireAuth())
to run the authentication check before any route defined within this plugin. Routes defined after this hook (/profile
, /orders
, etc.) are automatically protected.// private-routes.ts import { FastifyPluginAsync } from "fastify"; const privateRoutes: FastifyPluginAsync = async (fastify, opts) => { // Apply the hook once for all routes in this plugin fastify.addHook("preHandler", fastify.requireAuth()); fastify.get("/profile", async (request, reply) => { // Token is already validated by the hook return { user: request.user }; }); fastify.get("/orders", async (request, reply) => { // Hook handles main auth; specific checks can still happen here if (!request.user.scope?.includes("read:orders")) { return reply.code(403).send({ error: "insufficient_scope" }); } return { orders: [ /* ... */ ], user_id: request.user.sub, }; }); // Define more protected routes here... }; export default privateRoutes;
2. Register the plugin in your main server file:
In your main server setup (likely within the
fastify.register(async function(fastify){...})
block), import the privateRoutes
plugin. Use fastify.register
to load it, passing the prefix
option (e.g., { prefix: "/api/v1" }
) to mount all routes from the plugin under that path.import Fastify from "fastify"; import Auth0 from "@auth0/auth0-fastify-api"; import * as dotenv from "dotenv"; import privateRoutes from "./private-routes"; // Import the routes plugin dotenv.config(); const fastify = Fastify(); fastify.register(Auth0, { domain: process.env.AUTH0_DOMAIN, audience: process.env.AUTH0_AUDIENCE, }); fastify.register(async function (fastify) { if (!process.env.AUTH0_DOMAIN || !process.env.AUTH0_AUDIENCE) { fastify.log.error("Auth0 domain or audience not configured in .env file"); process.exit(1); } // Register the protected routes plugin under the /api/v1 prefix fastify.register(privateRoutes, { prefix: "/api/v1" }); // Routes defined outside that plugin don't inherit its hooks // and need their own protection fastify.get("/", { preHandler: fastify.requireAuth() }, (_, reply) => { reply.send({ ok: true }); }); }); fastify.listen({ port: process.env.PORT || 3000 });
Now, requests to
/api/v1/profile
, /api/v1/orders
, or any other route inside private-routes.ts
are automatically protected by the requireAuth
hook applied at the plugin level.Enforcing Scopes
If you need to ensure the user has been granted specific permissions (scopes) in their access token, use the
scopes
property of the requireAuth
decorator:const requiredScopes = ["read:messages", "write:messages"]; fastify.post( "/messages", { preHandler: fastify.requireAuth({ scopes: requiredScopes }) }, async (request, reply) => { return { message: "Message created successfully", user: request.user }; } );
Only requests with valid tokens containing both
read:messages
and write:messages
scopes will reach the route event handler body.If you don't have multiple scopes to check, you can also assign
scopes
a string as its value. For a single scope, you could use the following:const requiredScopes = "read:messages";
If the validated token doesn't include all the specified scopes, the SDK will return a
403 Forbidden
error with an insufficient_scope
code.Accessing User Information
Once a token is validated by
requireAuth
, its decoded payload (claims) is attached to the Fastify request object at request.user
. This typically includes information like the user ID (sub
), audience (aud
), issuer (iss
), and granted scopes (scope
).fastify.get( "/profile", { preHandler: fastify.requireAuth() }, async (request, reply) => { // Access the user's unique identifier (subject) const userId = request.user.sub; // Access the scopes granted in this token const scopes = request.user.scope; return { userId, scopes, fullProfile: request.user }; } );
Conclusion
The
auth0-fastify-api
SDK provides a straightforward way to secure your Fastify APIs by integrating Auth0 token validation. It offers simple decorators for protecting routes and checking scopes, allowing you to focus on your application's core logic.We encourage you to try it out in your next Fastify project and share your feedback! Check out the source code and examples to learn more.
Happy coding!
About the author
Dan Arias
Staff Developer Advocate
The majority of my engineering work revolves around AWS, React, and Node, but my research and content development involves a wide range of topics such as Golang, performance, and cryptography. Additionally, I am one of the core maintainers of this blog. Running a blog at scale with over 600,000 unique visitors per month is quite challenging!
I was an Auth0 customer before I became an employee, and I've always loved how much easier it is to implement authentication with Auth0. Curious to try it out? Sign up for a free account ⚡️.View profile