diff --git a/content/benefits-of-multi-factor-authentication/index.md b/content/benefits-of-multi-factor-authentication/index.md index f9c81520..a2101f95 100644 --- a/content/benefits-of-multi-factor-authentication/index.md +++ b/content/benefits-of-multi-factor-authentication/index.md @@ -10,16 +10,18 @@ author: "Mostafa Ibrahim" ## Table of Contents -1. [What Is Multi-Factor Authentication (MFA)?](#what-is-multi-factor-authentication-mfa) -2. [Types of Multi-Factor Authentication](#types-of-multi-factor-authentication) -3. [10 Benefits of Multi-Factor Authentication](#10-benefits-of-multi-factor-authentication) -4. [Problems With Traditional Security Mechanisms and How MFA Solves Them](#problems-with-traditional-security-mechanisms-and-how-mfa-solves-them) -5. [Designing Multi-Factor Authentication Without Sacrificing Good UX](#designing-multi-factor-authentication-without-sacrificing-good-ux) -6. [Real-world Use Cases of Multi-Factor Authentication](#real-world-use-cases-of-multi-factor-authentication) -7. [The Future of Secure Access: What to Expect for MFA?](#the-future-of-secure-access-what-to-expect-for-mfa) -8. [Getting Started with MFA in 2024](#getting-started-with-mfa-in-2024) -9. [Conclusion](#conclusion) - +- [Introduction](#introduction) +- [What Is Multi-Factor Authentication (MFA)?](#what-is-multi-factor-authentication-mfa) +- [Types of Multi-Factor Authentication](#types-of-multi-factor-authentication) +- [10 Benefits of Multi-Factor Authentication](#10-benefits-of-multi-factor-authentication) +- [Problems With Traditional Security Mechanisms and How MFA Solves Them](#problems-with-traditional-security-mechanisms-and-how-mfa-solves-them) +- [Designing Multi-Factor Authentication Without Sacrificing Good UX](#designing-multi-factor-authentication-without-sacrificing-good-ux) +- [Real-world Use Cases of Multi-Factor Authentication](#real-world-use-cases-of-multi-factor-authentication) +- [The Future of Secure Access: What to Expect for MFA?](#the-future-of-secure-access-what-to-expect-for-mfa) +- [Getting Started with MFA in 2024](#getting-started-with-mfa-in-2024) +- [Conclusion](#conclusion) + +## Introduction In 2022, over [80% of data breaches](https://www.verizon.com/business/en-gb/resources/2022-data-breach-investigations-report-dbir.pdf) were attributed to compromised passwords. @@ -33,7 +35,7 @@ Multi-Factor Authentication is a security mechanism that requires users to provi ### Types of Multi-Factor authentication -With Multi-factor authentication, the user would have to prove their identity through multiple forms of identification. The basic idea is that adding challenges to the authentication flow exponentially increases the difficulty of the account being compromised. +With Multi-factor authentication, the user would have to prove their identity through multiple forms of identification. The basic idea is that adding challenges to the authentication flow exponentially increases the difficulty of the account being compromised. These additional forms of authentication can be based of the following types: @@ -43,7 +45,7 @@ These additional forms of authentication can be based of the following types: The implementation of additional factors is a tradeoff between security and user experience. While not always true, higher security leads to a more cumbersome user experience. We’ll evaluate the security and UX tradeoffs associated with different authentication factors -### Common MFA methods include: +### Common MFA methods include 1. **SMS or email-based one-time passwords (OTP)**: The system sends a unique code via text message or email. While convenient, this method is vulnerable to interception. 2. **Authenticator apps generating time-based one-time passwords (TOTP)**: These apps generate short-lived codes on the user's device. They're more secure than SMS but require the user to have a compatible device. @@ -111,14 +113,14 @@ Traditional security mechanisms, primarily relying on username and password comb - **Phishing Vulnerability:** Traditional systems are susceptible to phishing attacks that trick users into revealing their credentials. MFA, especially when using hardware keys or biometrics, offers robust protection against phishing. Even if a user falls for a phishing attempt, the attacker won't have the additional factors needed to access the account. - **Lack of User Verification:** Password-only systems cannot verify if the person entering the correct credentials is actually the authorized user. MFA addresses this by incorporating factors that are inherently tied to the user, such as biometrics or possession-based factors like a smartphone or hardware key. -### MFA addresses these issues by adding additional layers of security and verification: +### MFA addresses these issues by adding additional layers of security and verification - **Biometric Factors:** Fingerprints, facial recognition, or iris scans provide a highly secure and user-friendly authentication method. These are extremely difficult to spoof, especially when combined with other factors. - **Hardware Keys:** USB or NFC-based security keys offer a physical factor that must be present for authentication. These are highly resistant to remote attacks and phishing attempts. - **Time-based One-Time Passwords (TOTP):** Apps like Google Authenticator generate temporary codes that change every 30 seconds, adding a dynamic layer of security. - **Push Notifications:** Services can send authentication requests directly to a user's registered device, allowing for quick and secure approval of login attempts. -### It's important to note that while MFA significantly enhances security, it's not a silver bullet. Good cybersecurity practices remain crucial: +### It's important to note that while MFA significantly enhances security, it's not a silver bullet. Good cybersecurity practices remain crucial - Using password managers to generate and store strong, unique passwords for each account. - Staying vigilant about clicking links and checking domain names before entering credentials or performing sensitive actions. @@ -139,12 +141,11 @@ While security is paramount, it's crucial to implement MFA in a way that doesn't By focusing on these aspects, organizations can implement robust MFA systems that enhance security without frustrating users. - ## Real-world Use Cases of Multi-Factor Authentication -A good example of an MFA is corporate data protection. Companies use Security Assertion Markup Language (SAML) for Single Sign-On (SSO) authentication to allow employees to access multiple applications with one set of credentials. +A good example of an MFA is corporate data protection. Companies use Security Assertion Markup Language (SAML) for Single Sign-On (SSO) authentication to allow employees to access multiple applications with one set of credentials. -Integrating MFA with SAML adds an extra layer of security. When employees access resources, they not only enter their credentials but also authenticate via a second factor. For example, an SMS or email passcode, or biometric verification. Adding a second factor ensures that only authorized personnel can access sensitive corporate data. +Integrating MFA with SAML adds an extra layer of security. When employees access resources, they not only enter their credentials but also authenticate via a second factor. For example, an SMS or email passcode, or biometric verification. Adding a second factor ensures that only authorized personnel can access sensitive corporate data. The same is true for [Lightweight Directory Access Protocol (LDAP)](https://supertokens.com/blog/what-is-ldap). Many organizations use LDAP to store and manage user access to different systems. Integrating MFA with LDAP means that when users try to access a system, they must provide additional authentication like an SMS or email passcode. Integrating LDAP with MFA is particularly useful in large organizations with complex access control requirements. @@ -160,7 +161,6 @@ While biometric factors are already a part of MFA, they are expensive. However, One downside of MFA is the friction it can add to the authentication experience. Multiple factors increase the likelihood of the user dropping off during the login process. Adaptive and Contextual MFA analyze patterns such as device use, location, and access times to dynamically increase the number of factors a user must go through to authenticate. If the system sees that the user is accessing his account from his personal computer from his usual IP address he may be presented with a single factor during authentication. If the user tries to authenticate from a different device and timezone, then additional factors may be provided to prove the user's identity. - In short, the future of MFA is expected to be more integrated with a strong focus on context and biometrics. ## Getting Started with MFA in 2024 @@ -188,6 +188,6 @@ npx create-supertokens-app@latest --recipe=multifactorauth You can find the complete documentation for [SuperTokens MFA here](https://supertokens.com/docs/mfa/introduction). ## Conclusion -Traditional authentication methods have become susceptible to cyber attacks. MFA acts as a roadblock, making it exponentially harder for an attacker to compromise an account. For this reason, many companies have made MFA a requirement. -SuperTokens is on a mission to make it easier for developers to add MFA capabilities to their applications. +Traditional authentication methods have become susceptible to cyber attacks. MFA acts as a roadblock, making it exponentially harder for an attacker to compromise an account. For this reason, many companies have made MFA a requirement. +SuperTokens is on a mission to make it easier for developers to add MFA capabilities to their applications. diff --git a/content/revoking-access-with-a-jwt-blacklist/index.md b/content/revoking-access-with-a-jwt-blacklist/index.md index 1e9ddf6b..76c4533f 100644 --- a/content/revoking-access-with-a-jwt-blacklist/index.md +++ b/content/revoking-access-with-a-jwt-blacklist/index.md @@ -1,244 +1,451 @@ --- -title: Revoking Access to JWT tokens with a Blacklist/Deny List -date: "2022-02-10" +title: 7 Ways To Revoke JWT Tokens +date: "2024-08-15" description: "Learn how to maintain a JWT token blacklist / deny list using an in-memory data cache" cover: "revoking-access-with-a-jwt-blacklist.png" category: "programming" -author: "Advait Ruia" +author: "Dejan Lukic" --- -Depending on who you listen to, JWTs are either a panacea for all your authentication problems or should be avoided like the plague. We're in two minds here at SuperTokens. +## Table of Contents -## What is a JWT token? -A JWT, or JSON Web Token, is a string / token issued by the server that asserts properties contained in its "payload". Its most common use case is for authentication (OAuth 2.0 + Open ID Connect) and session management. +1. [Introduction](#why-revoking-jwt-tokens-can-be-challenging) +2. [A Word or Two About JSON Web Tokens (JWTs)](#a-word-or-two-about-json-web-tokens-jwts) +3. [7 Ways to Revoke JWT Tokens](#7-ways-to-revoke-jwt-tokens) +4. [How to Make Access Token Management Easier](#how-to-make-access-token-management-easier) +5. [Risks of Improper Access Token Management](#risks-of-improper-access-token-management) +6. [Conclusion](#conclusion) -As the name suggests, a JWT can contain any information inside it in JSON form. This is also known as "JWT claims". For example, for session management, the JSON would at least need to contain the logged in user's `userId`: -```json -{ - "userId": "...", - "expiry": 1646472008501, - ... +## Why Revoking JWT Tokens Can Be Challenging + +JSON Web Tokens (JWT) are a fundamental part of modern web authentication and authorization systems, particularly in applications where secure and efficient user authentication is critical. Due to their stateless nature, JWTs are widely adopted, but managing and revoking these tokens presents unique challenges that developers must carefully address to maintain the security and integrity of their systems. + +This article delves into seven effective strategies for revoking JWT tokens, ensuring secure access management while navigating the complexities associated with JWTs. + +### A Word or Two About JSON Web Tokens (JWTs) + +JWT tokens, or JSON Web Tokens, are self-contained tokens used in authentication systems to securely transmit information between parties. These tokens include a payload that carries claims, such as the user’s ID, roles, or other relevant details about the user's data, and are signed using a secret key. + +The signed JWT is then sent to the client and used in subsequent requests, typically passed in the Authorization header. This stateless nature makes JWTs a popular choice for authentication in distributed systems, as it removes the need for maintaining a server-side session state. + +However, the statelessness of JWTs also poses significant challenges when it comes to revocation. Unlike traditional session-based authentication, where sessions can be invalidated by removing them from the server, JWTs remain valid until their expiration (`exp`) time, which is embedded within the token’s payload. + +This expiry time ensures that the token is valid only for a specified period, but it can be problematic when a user’s account is compromised. Since the token cannot be easily invalidated across all endpoints without a centralized mechanism, any delay in detecting and revoking the token can result in unauthorized access to the user's data. + +Additionally, in distributed systems where multiple services or microservices rely on the same JWT access token, revocation becomes even more complex. Each service must recognize and enforce the revocation, which can be challenging without a shared state or revocation strategy. This complexity is further compounded when integrating with identity providers that issue JWTs as part of OAuth 2.0 or OpenID Connect protocols. It is recommended to refer to platform-specific documentation (docs) or GitHub repositories to implement best practices for revoking JWTs in such relatively complex environments. + +## 7 Ways to Revoke JWT Tokens + +Effectively revoking JWT tokens requires implementing strategies that accommodate their stateless design while ensuring security. Here are seven methods to revoke JWT tokens, each with its pros and cons. + +### Token Blacklisting + +**How It Works:** + +Token blacklisting is a widely used method to revoke JWT tokens. This approach involves maintaining a server-side blacklist containing identifiers, such as the `jti` claim or a user ID, of tokens that should be considered invalid. + +When a token is presented at an API endpoint, the server checks the blacklist to determine whether the token has been revoked. This list can be stored in a database or in an in-memory store like Redis, which allows for quick lookups. + +**Pros:** + +- **Granular Control:** Token blacklisting provides the ability to revoke specific tokens without affecting others, offering fine-grained control over user sessions. +- **Compatibility:** This method can be integrated with various backend systems and identity providers, making it a versatile solution for developers who follow guidelines and examples from GitHub or similar platforms. + +**Cons:** + +- **Scalability Issues:** As the number of blacklisted tokens increases, the performance of blacklist lookups may degrade, especially if stored in a database. +- **Stateful Requirement:** Blacklisting necessitates maintaining a server-side state, which conflicts with the stateless nature of JSON Web Tokens. + +**Example in Node.js:** + +```javascript +const redis = require('redis'); +const client = redis.createClient(); + +function blacklistToken(jti) { + client.set(jti, 'revoked', 'EX', 3600); // Expiry set for 1 hour +} + +function isTokenRevoked(jti, callback) { + client.get(jti, (err, reply) => { + callback(reply === 'revoked'); + }); +} + +// Middleware to check token blacklist +function checkBlacklist(req, res, next) { + const token = req.headers['authorization']; + const secret = process.env.JWT_SECRET; + const decoded = jwt.verify(token, secret); + + isTokenRevoked(decoded.jti, (isRevoked) => { + if (isRevoked) { + return res.status(401).send('Token has been revoked'); + } + next(); + }); } ``` -If the JWT containing this information needs to expire, the JSON can also contain the token's expiry time (as shown above). There is also a [convention for the names](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1) of the keys to these fields. In our example above those are: -- `userId` -> `sub` -- `expiry` -> `exp` +In this example, a JWT token's `jti` (JWT ID) is stored in Redis when the token is revoked. The middleware checks if the token's `jti` exists in Redis before processing the request. -Taking an example of the login process, a JSON containing the above information will be sent to the client, and then on each request, the client can send this JSON back to the server. This way, the server knows which userID is querying its APIs. If the JSON has expired, then the server can reject the request, and the user must login again. +### Token Expiration and Short Lifespan -However, from a security point of view, it's very easy for the client to change the `userId` value in the JSON and spoof being another user. To prevent this, the server sends the original JSON's "signature" along with the JSON. This "signature" is created using a secret that is known only by the server, so the client cannot create a signature of a JSON by itself. Therefore, if the client changes the JSON, the server will fail to match the original JSON's signature with the incoming / changed JSON's signature (this is known as verifying the signature) and can then reject the request. +**How It Works:** -There are several algorithms that a server can use to generate a signature for a JSON: -- HMAC + SHA256 -- RSASSA-PKCS1-v1_5 + SHA256 -- ECDSA + P-256 + SHA256 +Setting a short lifespan (the `exp` parameter) for JWT tokens can mitigate the risks associated with needing to revoke them. By configuring JWTs with a short expiration time, you reduce the window of opportunity for an attacker to use a compromised token. -The signing method chosen to create the signature must somehow be encoded in the JWT so that the same method is used when verifying the signature. +Once the token reaches its expiry, it becomes invalid, and the user must obtain a new JWT access token, typically through re-authentication or using a refresh token. -All in all, a JWT contains three parts: -- The header string containing information about the signing algorithm used. -- The body string containing the actual JSON. -- The signature string that can be used to verify that the JWT has not been changed by the client. +**Pros:** -These three sections are concatenated with a `.` separator to form the full JWT Token. An example JWT can be later seen in this blog post. +- **Reduced Exposure:** Short-lived tokens minimize the risk associated with compromised tokens by ensuring they are only valid for a limited time. +- **Maintains Statelessness:** This approach retains the stateless nature of JWTs as no server-side storage is required for token management. -## Advantages of JWT Tokens -The JWT approach certainly has its advantages over opaque tokens. JWTs are: +**Cons:** -- **Self-contained**: The JWT can contain the user's details (not just a session ID, like a cookie but other custom data such as user name and even permissions), together with the token's expiry time so you don't need to query a database for that information. This is completely unlike an opaque token which, by its very nature, is just a meaningless jumble of alphanumeric characters. -- **Secure**: JWTs are digitally signed using either a secret (HMAC) or a public/private key pair (RSA or ECDSA) which safeguards them from being modified by the client or an attacker. -- **Stored only on the client**: You generate JWTs on the server and send them to the client. The client then submits the JWT with every request. This saves database space. -- **Efficient**: It’s quick to verify a JWT, because it doesn’t require a database lookup. +- **User Experience Impact:** Users may need to authenticate more frequently, potentially leading to a frustrating user experience. +- **No Immediate Revocation:** This method does not provide a way to immediately revoke tokens, which can be a drawback if a user's account is compromised. -## Disadvantages of JWT Tokens -The fact that JWTs are only stored client-side leads to a fundamental disadvantage with JWTs. And that is: how do you revoke a user's access? +**Example in Node.js:** -Sure, JWTs have an expiry time and this can be as short-lived as you like. As soon as the access token expires however, the JWT is invalid and the client must re-authenticate with your server. This, of course, has a negative effect on the user experience.. +```ts +const jwt = require('jsonwebtoken'); -But suppose the user intentionally logs out of your system? Or you want to kick them out because you fear that security has been compromised? You can't[1]: if they still have an unexpired token, they still have access. +function generateToken(user) { + const payload = { userId: user.id, roles: user.roles }; + return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' }); +} +``` -There is a solution to this problem, but it does involve some extra work on your part. It is a technique that requires maintaining a JWT blacklist/deny list. In this article we'll show you what a JWT blacklist/deny list is, how to implement one, and discuss whether it is a good solution to this problem or not. +In this example, JWTs are generated with a 15-minute expiration time. Users must frequently refresh their tokens, reducing the risk associated with compromised tokens. -## What is a JWT blacklist/deny list? -A JWT blacklist/deny list is a list of tokens that should no longer grant access to your system. +### Rotating Secrets -Where you maintain this list is up to you. You could use a traditional database, but a much better approach is to use an in-memory data cache, like Redis. An in-memory data cache provides much faster and more predictable seek times than data stored on disk. +**How It Works:** -That’s what we’ll use in this example, and we’ll code our solution using Node.js and Express. If that’s not your chosen technology stack then fear not: the fundamental approach is the same regardless of how you choose to build it. +JWT tokens are signed with a secret key that verifies their authenticity. By periodically rotating this secret key, you can invalidate all existing tokens signed with the old key. This method is particularly useful when you need to revoke a large number of tokens simultaneously, such as after a security incident or when changing your authentication strategy. -## OK, so how do I do it? -First, you’ll need to instantiate your Express server application and set up Redis so that you can maintain a list of active JWTs: +**Pros:** -```js -import express from "express"; -import bodyparser from "body-parser"; -import jwt from "jsonwebtoken"; -import redis from "redis"; +- **High Security:** Regularly rotating secrets add an extra layer of security, making it more difficult for attackers to exploit a compromised key. +- **Wide Impact:** Secret rotation invalidates all tokens at once, which is effective in scenarios where a large-scale revocation is necessary. -const JWT_SECRET = "Ultra-secure-secret"; +**Cons:** -const app = express(); -app.use(bodyparser.urlencoded({ extended: false })); -app.use(bodyparser.json()); +- **Complex Key Management:** Rotating secrets requires careful management to avoid invalidating legitimate tokens inadvertently. +- **Token Recreation:** After a key rotation, clients must obtain new JWTs signed with the updated key, which can complicate client-side and backend interactions. -let redisClient = null; +**Note:** Doing secret management on your own is challenging and can open you to security vulnerabilities, so most auth services handle this on your behalf. -(async () => { - redisClient = redis.createClient(); +### Token Versioning - redisClient.on("error", (error) => { - console.log(error); - }); - redisClient.on("connect", () => { - console.log("Redis connected!"); - }); +**How It Works:** - await redisClient.connect(); -})(); +Token versioning involves assigning a version number to each JWT issued, which is then stored in the database alongside the user's account information. When a token needs to be revoked, the version number is incremented in the database, rendering all previous tokens invalid. The next time the user tries to authenticate, they must present a token with the updated version. -// listen for requests -const listener = app.listen(3000, () => { - console.log("Server running"); -}); -``` +**Pros:** -To test the storage and subsequent revocation of a JWT, you’ll need a way to create a user and generate a JWT for that user. Let’s add the `createUser` endpoint for this. You can make a POST request to this endpoint with the name of the new user: +- **Selective Revocation:** Token versioning allows for targeted revocation based on the user's account status or other criteria. +- **Flexibility:** This method can be integrated with various identity providers and authentication systems, offering a flexible solution. -```js -import jwt from "jsonwebtoken"; +**Cons:** -app.post("/createUser", (request, response) => { - const token = generateAccessToken({ username: request.body.username }); - response.json(token); -}); +- **Increased Complexity:** Managing token versions adds complexity to the backend, requiring additional logic to validate token versions. +- **Database Dependency:** This approach relies on database lookups to validate token versions, which can introduce latency, especially if not optimized for in-memory checks. -const generateAccessToken = (username) => { - return jwt.sign(username, JWT_SECRET, { expiresIn: "3600s" }); -}; -``` +**Example in Node.js (+ MongoDB):** -Hit that endpoint, and you’ll see a JWT being issued for the user: +```ts +const mongoose = require('mongoose'); -```bash -curl --location --request POST 'http://localhost:3000/createUser' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "username": "Derek" -}' +// Define User schema with tokenVersion field +const UserSchema = new mongoose.Schema({ + email: String, + password: String, + tokenVersion: { type: Number, default: 0 } +}); -"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkRlcmVrIiwiaWF0IjoxNjQxMzA3MTgxLCJleHAiOjE2NDEzMTA3ODF9.3yrIJpWMS952QVakifjviiTs0ANJOmxZDovQO0N5brQ" -``` +const User = mongoose.model('User', UserSchema); + +function generateToken(user) { + const payload = { userId: user.id, tokenVersion: user.tokenVersion }; + return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' }); +} + +async function invalidateUserTokens(userId) { + await User.findByIdAndUpdate(userId, { $inc: { tokenVersion: 1 } }); +} -If you paste that token into the [online debugger](https://jwt.io), you’ll see that the JWT has the user name encoded within it, along with other details such as the time it was issued (`iat`) and the time it expires (`exp`): +// Middleware to check token version +async function checkTokenVersion(req, res, next) { + const token = req.headers['authorization']; + const decoded = jwt.verify(token, process.env.JWT_SECRET); -```json -{ - "username": "Derek", - "iat": 1641307181, - "exp": 1641310781 + const user = await User.findById(decoded.userId); + if (user.tokenVersion !== decoded.tokenVersion) { + return res.status(401).send('Token version is invalid'); + } + next(); } ``` +In this example, the user's `tokenVersion` is stored in the database. When a token is revoked, the `tokenVersion` is incremented, and the middleware checks if the presented token is still valid. -The next thing you need to do is authenticate the user with that JWT whenever that user attempts to do anything with your application. This is a three-step process. First, you’ll want to ensure that the JWT has been supplied. Then, you’ll want to test whether the JWT supplied is in the blacklist/deny list stored in Redis. If it’s not, then you’ll finally want to verify that the JWT is valid. +### User Logout and Forced Token Invalidation -We can encode these steps as Express middleware: +**How It Works:** -```js -// JWT middleware -const authenticateToken = async (request, response, next) => { - const authHeader = request.headers["authorization"]; - const token = authHeader && authHeader.split(" ")[1]; +When a user logs out, their JWT token can be invalidated by marking it as such on the server or by expiring the session. This method ensures that the token cannot be used to access protected resources after logout. In systems that support OAuth 2.0 or OpenID Connect, this might involve notifying the identity provider to revoke the token at the source. - // token provided? - if (token == null) { - return response.status(401).send({ - message: "No token provided", - }); - } +**Pros:** - // token in deny list? - const inDenyList = await redisClient.get(`bl_${token}`); - if (inDenyList) { - return response.status(401).send({ - message: "JWT Rejected", - }); - } +- **Immediate Effect:** The token is immediately invalidated, providing a prompt security response. +- **User-Controlled:** Users can take control of their session's security by actively logging out. - // token valid? - jwt.verify(token, JWT_SECRET, (error, user) => { - if (error) { - return response.status(401).send({ - status: "error", - message: error.message, - }); - } +**Cons:** - request.userId = user.username; - request.tokenExp = user.exp; - request.token = token; +- **Statefulness:** Forced token invalidation requires maintaining a server-side state, which conflicts with the stateless nature of JWTs. +- **Implementation Complexity:** Implementing this method can be challenging in systems that rely on stateless JWTs, especially in distributed environments with multiple endpoints. - next(); - }); -}; -``` +**Example: User Logout and Forced Token Invalidation in Node.js** -Now what will happen is that any time the user you created earlier attempts to visit your home route (`/`), the JWT they supply will be checked against the blacklist/deny list first and then verified before being granted access. +```ts +const express = require('express'); +const jwt = require('jwt-simple'); +const redis = require('redis'); +const bodyParser = require('body-parser'); +const nodemailer = require('nodemailer'); +const app = express(); + +app.use(bodyParser.json()); +const redisClient = redis.createClient(); +const SECRET_KEY = 'your_secret_key'; // Replace with your actual secret key +const PORT = 3000; + +// Configure email transport for notifications +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: 'your_email@gmail.com', // Replace with your email + pass: 'your_email_password' // Replace with your email password + } +}); -That’s all well and good, but at the moment you have no way to revoke a JWT, so there’s nothing in the blacklist/deny list. +// Middleware to verify JWT and check session +function verifyToken(req, res, next) { + const token = req.headers['authorization']; + if (!token) { + return res.status(401).send('No token provided'); + } + try { + const decoded = jwt.decode(token, SECRET_KEY); + const sessionId = decoded.sessionId; + redisClient.get(sessionId, (err, sessionData) => { + if (err || !sessionData) { + return res.status(401).send('Invalid token'); + } + req.user = decoded; // Attach user info to request + next(); + }); + } catch (e) { + res.status(401).send('Failed to authenticate token'); + } +} -Let’s create another route that will simulate a user logging out. When this endpoint is hit, the user’s JWT is persisted to Redis, using the format `bl_` for the key, and the value being the actual token. We’ll set the key to expire when the token itself expires, so as not to fill up Redis with lots of expired tokens. +// Endpoint for user login (simulated) +app.post('/login', (req, res) => { + const { username, email } = req.body; + const sessionId = `${username}-${Date.now()}`; // Generate a unique session ID + const token = jwt.encode({ sessionId }, SECRET_KEY, 'HS256'); + redisClient.set(sessionId, JSON.stringify({ username, email }), 'EX', 3600); // Store session with 1-hour expiry + res.json({ token }); +}); -```js -app.post("/logout", authenticateToken, async (request, response) => { - const { userId, token, tokenExp } = request; +// Endpoint for user logout +app.post('/logout', verifyToken, (req, res) => { + const sessionId = req.user.sessionId; + redisClient.del(sessionId, (err) => { + if (err) { + return res.status(500).send('Failed to logout'); + } + // Notify user about session expiration + redisClient.get(sessionId, (err, sessionData) => { + if (!err && sessionData) { + const userEmail = JSON.parse(sessionData).email; + const mailOptions = { + from: 'your_email@gmail.com', + to: userEmail, + subject: 'Session Expired', + text: 'Your session has been terminated. If you did not request this, please contact support.' + }; + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + console.error('Error sending email:', error); + } else { + console.log('Email sent:', info.response); + } + }); + } + }); + res.send('Logged out successfully'); + }); +}); - const token_key = `bl_${token}`; - await redisClient.set(token_key, token); - redisClient.expireAt(token_key, tokenExp); +// Protected endpoint +app.get('/protected', verifyToken, (req, res) => { + res.send(`Hello ${req.user.sessionId}, you have access to this protected resource`); +}); - return response.status(200).send("Token invalidated"); +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); }); ``` +### Token Revocation Lists + +**How It Works:** -Issue a `POST` request to the `/logout` endpoint: +A Token Revocation List (TRL) operates similarly to a blacklist but is often implemented as a centralized or distributed service that can be queried to determine if a token has been revoked. This method is especially effective in large-scale distributed systems where multiple services and endpoints need to recognize and enforce token revocation. + +**Pros:** + +- **Scalability:** A TRL can be designed to handle high loads in distributed environments, ensuring that all services consistently enforce token revocation. +- **Centralized Management:** By centralizing token revocation, this method simplifies the management and enforcement of revocation policies. + +**Cons:** + +- **Latency:** Querying a TRL can introduce latency, especially if the revocation list is not stored in an in-memory database or is not optimized for fast lookups. +- **Complexity:** Implementing and maintaining a TRL requires careful design to ensure scalability and reliability across distributed services. + +**Implementation Tip:** + +When designing a TRL, consider using in-memory data stores like Redis or distributed caching systems to reduce latency. Additionally, ensure that your system can handle large-scale requests by implementing load balancing and failover mechanisms. + +**Example of Implementing a Simple TRL in Node.js:** + +```ts +let trustedRevocationList = []; + +function updateRevocationList(newList) { + trustedRevocationList = newList; +} + +function isTokenRevoked(jti) { + return trustedRevocationList.includes(jti); +} -```bash -curl --location --request GET 'http://localhost:3000/' \ ---header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkRlcmVrIiwiaWF0IjoxNjQxMzA4NTQwLCJleHAiOjE2NDEzMTIxNDB9.nlJJe7XtK3PJVgvjevrHabeImqJyRoUDiXejDhVK5yM' +// Middleware to check TRL +function checkRevocationList(req, res, next) { + const token = req.headers['authorization']; + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + if (isTokenRevoked(decoded.jti)) { + return res.status(401).send('Token has been revoked'); + } + next(); +} + +// Example of updating TRL (this would typically be done periodically) +setInterval(() => { + // Fetch the latest TRL from a trusted source + const latestTRL = fetchTrustedRevocationList(); + updateRevocationList(latestTRL); +}, 60000); // Update every 60 seconds ``` -Now the user’s JWT is technically still valid (until it expires), but now it’s in the blacklist/deny list and will therefore be intercepted on subsequent requests. Repeat the `POST` request to the home route above and you’ll get an HTTP 403 Forbidden error, with a message `"JWT Rejected"`. +### Refresh Tokens for Long-Lived Sessions + +**How It Works:** + +In scenarios where long-lived sessions are necessary, using refresh tokens in conjunction with JWTs provides a secure way to manage token expiry and revocation. The refresh token is stored securely on the server and is used to generate new JWT access tokens when the previous one expires. If a user's account is compromised, the refresh token can be revoked, preventing the issuance of new JWT access tokens. + +**Pros:** + +- **Increased Security:** This method enhances security by limiting the lifespan of JWT access tokens while providing a mechanism for immediate revocation through the refresh token. +- **Flexible Control:** Allows for the revocation of individual sessions without affecting others, providing a granular approach to session management. + +**Cons:** + +- **Complexity:** Managing refresh tokens adds complexity to the backend and client-side logic, as well as potential storage challenges if multiple refresh tokens are issued. +- **No Immediate Revocation for Access Tokens:** Revoking the refresh token does not immediately invalidate the existing JWT access token, leaving a short window of vulnerability. + +**Example in Node.js:** + +```ts +const jwt = require('jsonwebtoken'); +const uuid = require('uuid'); + +// Simulated in-memory refresh token store +let refreshTokenStore = {}; -However, this won’t help if you want to deny access to a user who hasn’t logged out and whose token is still valid. The problem is that you won’t know what the user’s access token is at that time, and therefore you won’t know what to put in the cache. +function generateToken(user) { + const payload = { userId: user.id }; + const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' }); + const refreshToken = uuid.v4(); -To solve this issue, you should add the ability to blacklist a user’s `userID` via redis and check that blacklist as well during JWT verification. Unlike the JWT blacklist, this entry won't have an expiry time associated with it. + // Store refresh token server-side + refreshTokenStore[refreshToken] = user.id; + + return { accessToken, refreshToken }; +} + +function refreshAccessToken(refreshToken) { + const userId = refreshTokenStore[refreshToken]; + if (!userId) { + throw new Error('Invalid refresh token'); + } + + const newAccessToken = jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '15m' }); + return newAccessToken; +} + +function revokeRefreshToken(refreshToken) { + delete refreshTokenStore[refreshToken]; +} + +// Middleware example for refreshing access tokens +function refreshTokenMiddleware(req, res, next) { + const { refreshToken } = req.body; + + try { + const newAccessToken = refreshAccessToken(refreshToken); + res.json({ accessToken: newAccessToken }); + } catch (err) { + res.status(401).send('Invalid or expired refresh token'); + } +} +``` -> You can find a working version of this example on [Github](https://github.com/marklewin/jwt-denylist). -## Is this a good idea? -So now you see that this concept of a JWT blacklist/deny list is relatively easy to implement. But is it the best way of handling this situation? +## How to Make Access Token Management Easier -In our opinion, it’s not. +Effective access token management is crucial for maintaining the security of your application. Here are some best practices to simplify the process: -The biggest advantage of JWTs is that they make session verification fast. If you maintain a blacklist/deny list and have to query it on every API call, then you’ve lost that advantage. +- **Use Refresh Tokens:** As mentioned, refresh tokens can provide an effective way to manage and revoke JWTs. Ensure they are securely stored, either on the client-side or in Redis, and frequently rotated to prevent misuse. +- **Minimize Token Lifespan:** Short-lived tokens reduce the impact of a compromised token. Pair them with refresh tokens to balance security and usability, configuring the `exp` parameter appropriately. +- **Ensure Secure Storage:** Store tokens securely, whether in cookies with the `HttpOnly` and `Secure` flags or in secure client-side storage mechanisms like local storage. +- **Monitor for Suspicious Activity:** Implement monitoring and notifications for unusual token usage patterns, such as tokens being used from multiple locations. +- **Automate Token Management:** Use tools like SuperTokens to handle JWT creation, validation, and revocation. This can streamline your authentication process and reduce the risk of errors. -Instead, what we advocate is a solution for session management that combines the respective strengths of both JWTs and opaque tokens. +For more on creating JWTs and securing user sessions, see our [docs on JWT Creation](https://supertokens.com/docs/microservice_auth/jwt-creation) and our guide on [Using JWTs for User Sessions](https://supertokens.com/blog/are-you-using-jwts-for-user-sessions-in-the-correct-way). You can also explore how to configure secure tokens in Node.js with backend tools like Redis for in-memory storage. -In this workflow, when the user logs in, the server issues a both short-lived JWT (the access token), and a long-lived opaque token (the refresh token). When a user is granted access, both of these tokens are sent to the client. +## Risks of Improper Access Token Management -When the user attempts to access a resource, they send the JWT access token along with every request. When the JWT expires, the client then uses the opaque refresh token to request a new JWT and a new opaque refresh token. This process is known as refresh token rotation. +Failing to properly manage and revoke JWT tokens can lead to serious security risks: -The client then uses this new JWT to make subsequent requests and the process continues. +- **Unauthorized Access:** Compromised tokens can grant unauthorized access to your application, leading to data breaches and other security incidents. +- **Token Theft:** If tokens are not securely stored on the client-side, they can be stolen and used maliciously. +- **Replay Attacks:** Attackers can reuse a stolen token to gain unauthorized access if tokens are not properly managed. +- **Poor User Experience:** Improper token management can lead to unnecessary logouts or difficulties accessing services. +- **Legal Implications:** In regulated industries like fintech, failing to manage tokens properly can result in compliance violations, leading to legal penalties. -## The advantages and disadvantages of issuing two tokens and of implementing refresh token rotation +For a comprehensive guide on implementing secure authentication practices, check out our articles on [OAuth 2.0 vs. JWT](https://supertokens.com/blog/oauth-vs-jwt) and [Passwordless Login](https://supertokens.com/blog/a-guide-to-implementing-passwordless-login). You can also find code examples on GitHub for integrating these practices in your Node.js applications. -The benefits of this approach is that if you want to revoke access, then all you need to do is invalidate the opaque token on the server side. Then, when the refresh endpoint is called, the server looks up the opaque token, sees that it has expired, and logs the user out. +## Conclusion -Note that this doesn’t solve the problem of still-valid JWTs existing on the client. But, because you can make those JWTs much more short-lived (even just a few mins), you should find that it’s not too big a problem. +Revoking JWT tokens is a crucial aspect of securing modern web applications. While the stateless nature of JWTs presents challenges, the methods discussed above offer effective strategies for maintaining secure access management. Whether through token blacklisting, rotating secrets, or using refresh tokens, implementing a robust revocation strategy is essential to protecting your application and users' data. -In this way, you keep one of the key benefits of using JWTs and that’s the fact that you don’t have to keep accessing the database on each API call to verify the JWT - you only need to do a database lookup when refreshing the session - which happens relatively rarely. +There are a ton of intricacies while building a robust authentication solution with JWTs. Keeping up with best practices and the most secure standards, while keeping up-to-date with the latest threat vectors, is hard to say the least. -If this sounds ideal for your use case, check out SuperTokens’ implementation of this session flow. SuperTokens uses this method to harness all the benefits of using JWTs while mitigating many of their disadvantages by combining them with opaque tokens. +Unless you're building a personal project, learning, or have an extremely niche use case that is not supported by auth providers, it is better to offload the headache to a service instead. -Furthermore, changing the refresh token on each use adds additional security benefits like being able to [detect session hijacking](https://supertokens.com/blog/the-best-way-to-securely-manage-user-sessions). +Easily manage user access with [SuperTokens](https://supertokens.com/product) and take the hassle out of JWT management. Start securing your application today. \ No newline at end of file