Table of Contents
What is authentication middleware in web apps
Looking to implement authentication middleware in your app? Most modern frameworks support middleware functions that let you intercept user requests, verify tokens, and control access—all in a centralized way.
In this blog, we’ll walk through how authentication middleware works and how to implement it effectively using code examples in Node.js and Python. Once you understand the basics, we’ll show how Descope’s tools can streamline the process with out-of-the-box support for token validation, role checks, and more.
What is authentication middleware in web apps
In the context of web development, authentication middleware is a function that intercepts incoming requests and runs logic before or after those requests reach your application’s core handlers. Middleware gives you a centralized way to manage access control, freeing up developer time to focus on business logic instead of duplicating auth checks across routes.
Most modern frontend and backend frameworks support this concept. You typically register a function—similar to a callback—that’s triggered with every request. This can happen before the request reaches a controller or after the response is generated.
Within this middleware function, you can inspect request headers (like Authorization) and validate session tokens or permissions before allowing access to protected routes.
Example: Authentication middleware in Node.js
Let’s look at how to build simple authentication middleware using Express in a Node.js app. This middleware intercepts requests, checks for an authorization header, and validates the session token before passing the request to the next handler.
var express = require(‘express’)
var app = express()
var validateJwt = function(token) {
// validation logic
return true;
}
var authMiddleware = function (req, res, next) {
req.isValidSession = validateJwt(req.headers.authorization)
next()
}
app.use(authMiddleware)
app.get (‘/’, function (req, res) {
var responseText = ‘Hello World!<br>’
responseText += ‘<small>Session Valid:‘+req.isValidSession + ‘</small>’
res.send (responseText)
})
app.listen(3000)
In this example, the authMiddleware function runs on every request. It extracts the token from the Authorization header, runs a validation check, and attaches the result (isValidSession) to the request object. This setup gives you a lightweight way to apply authentication logic across all routes in your Express app.
Adding Descope authentication to Node.js middleware
Now that we understand how middleware works, we can use it to implement robust authorization and authentication middleware by validating user sessions on every request.
Descope makes this easier by providing SDKs that handle session and token validation. Instead of repeating auth logic across your app, you can centralize it in a single middleware function. That way, each incoming request is automatically checked for a valid access token or refresh token before hitting protected routes.
Here’s how to build authentication middleware using the Descope Node.js SDK: [authentication.js]
import DescopeClient from '@descope/node-sdk';
export class DescopeMiddleware {
constructor(projectId) {
this.descopeClient = DescopeClient({ projectId: projectId});
}
validate(token) {
try {
const authInfo = await descopeSdk.validateSession(sessionToken);
console.log("Successfully validated user session:");
console.log(authInfo);
} catch (error) {
console.log ("Could not validate user session " + error);
}
}
}
[app.js]
import express from 'express';
import DescopeMiddleware from './authentication.js';
const app = express();
const authMiddleware = new DescopeMiddleware('__PROJECT_ID__');
app.use(authMiddleware.validate());
app.get('/', (req, res) => { res.send('main'); });
app.listen(3000, () => {
console.log("Express Running port 3000") });
This pattern helps you manage authentication in one place. It also improves maintainability by offloading session validation to Descope’s SDK, allowing you to focus on application logic.
Using Descope decorators in Python
Python decorators are a powerful way to wrap additional behavior around existing functions, making them especially useful for building authentication middleware in Python web frameworks.
A decorator takes a function as input, extends or modifies its behavior, and returns a new function. This allows you to centralize logic, such as logging, validation, or—in our case—authentication, without cluttering every route with repeated code.
Here’s a basic example of how decorators work:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
To use a decorator, you simply annotate a function with @decorator_name
. When you call that function, Python actually runs the wrapper logic defined inside the decorator. In this case, calling say_hello()
produces the following output:
> Something is happening before the function is called.
> Hello!
> Something is happening after the function is called.
This structure is ideal for building authentication middleware in frameworks like Flask, FastAPI, or Django, where decorators can be applied directly to route functions to enforce login or permission checks.
Example: Descope Python decorator
You could write your own authentication middleware using decorators in Flask, FastAPI, or Django. But to save time, Descope provides prebuilt decorators that handle session validation, role enforcement, and other identity checks out of the box.
Here’s an example of using Descope’s @descope_validate_auth
decorator to protect a route that requires authentication:
# This needs authentication
@APP.route("/private")
@descope_validate_auth(
descope_client
) # Can add permissions=["Perm 1"], roles=["Role 1"], tenant="t1" conditions
def private():
return Response("<h1>Restricted page, authentication needed.</h1>")
This decorator automatically checks the user’s JWT. If the token doesn’t meet the conditions—such as having the correct roles or permissions—Descope will return a 401 Unauthorized
response without running the route handler.
Descope also provides other decorators that extend the behavior of your routes. For example, the @descope_full_login
decorator can trigger a Descope Flow:
@APP.route("/login", methods=["GET"])
@descope_full_login(
project_id=PROJECT_ID,
flow_id="sign-up-or-in",
success_redirect_url="http://dev.localhost:9010/private",
)
def login():
# Nothing to do! this is the MAGIC!
pass
Need to handle logout? The @descope_logout
decorator clears the user’s session:
@APP.route("/logout")
@descope_logout(descope_client)
def logout():
return Response("<h1>Goodbye, logged out.</h1>")
With just a few lines of code, you can use decorators to handle user authentication and session flow without repeating logic across your app.
Authentication middleware made simpler with Descope
Authentication middleware lets you centralize and automate how your app handles access control. By intercepting requests and validating tokens before they hit protected routes, middleware simplifies security and improves the user experience.
Descope gives you tools to build and manage this layer more efficiently. Whether you prefer writing your own middleware or using prebuilt decorators, you can plug in Descope’s SDKs and flows to streamline session validation, role enforcement, and login logic.
Want to add authentication to your app without reinventing the wheel? Sign up for a Free Forever Descope account to get started. If you have questions or want to learn more about authentication best practices, book time with our auth experts.