Revoke Refresh Tokens in JWT-Based Token Authentication

Revoke Refresh Tokens in JWT-Based Token Authentication

In this article, I will discuss Implementing Logout Endpoint to Revoke Refresh Tokens in JWT-Based Token Authentication in our Authentication Server Project. Please read our previous article discussing Refresh Token in ASP.NET Core Web API using JWT Authentication.

What Are Access and Refresh Tokens in JWT Authentication?
Access Tokens:
  • Grant access to protected resources (APIs) for a limited time.
  • Short-lived (e.g., 15 minutes to 1 hour), stateless, and self-contained.
  • Included in API requests (typically in the Authorization header) to authenticate and authorize access.
Refresh Tokens:
  • Obtain new access tokens without requiring the user to re-authenticate.
  • Long-lived (e.g., days, weeks, or months), stored securely on the client side, and often tied to a specific user and client application.
  • Sent to a dedicated endpoint (e.g., /refresh-token) to request a new access token when the current one expires.
Why Use Refresh Tokens?
  • Enhanced Security: Keeping access tokens short-lived minimizes the risk window for compromised tokens.
  • Improved User Experience: Users remain authenticated without frequent logins, as new access tokens can be seamlessly obtained using refresh tokens.
  • Controlled Access: Refresh tokens can be revoked independently of access tokens, providing more control over user sessions.
What Does It Mean to Revoke Refresh Tokens?

Revoking a refresh token means invalidating it so that it can no longer be used to obtain new access tokens. Once revoked, any attempt to use the refresh token will be denied, ensuring that the session associated with that token is effectively terminated.

Logout Endpoint to Revoke Refresh Tokens in JWT-Based Token Authentication

Adding a Logout Endpoint to our ASP.NET Core Web API for invalidating or revoking refresh tokens is a crucial step in enhancing the security of our authentication system. This endpoint allows clients or users to invalidate or revoke their refresh tokens. By doing so, we ensure that no further tokens can be issued using those refresh tokens, effectively logging the user out of their current session.

Implementing Logout Endpoint to Revoke Refresh Tokens

To implement a logout endpoint that revokes refresh tokens, we need to follow the below steps:

  • DTO Creation: Define a LogoutRequestDTO to capture the necessary information from the client.
  • Endpoint Implementation: Create a new Logout endpoint in the AuthController. The Logout Endpoint will be implemented as follows:
      1. Validate the incoming request.
      2. Hash the provided refresh token.
      3. Locate the refresh token in the database.
      4. Revoke the refresh token by updating its status or removing it altogether from the database.
      5. Optionally, revoke all refresh tokens associated with the user for additional security.
  • Response Handling: Provide appropriate responses based on the success or failure of the operation.

Let us proceed and implement this step by step. We need to implement this in our Authentication Server Web API application.

Creating the Logout Request DTO

First, define a Data Transfer Object (DTO) that captures the necessary information from the client during logout. Typically, the refresh token is sufficient, but including the ClientId can add an extra validation layer. So, create a new class file named LogoutRequestDTO.cs in the DTOs folder and add the following code:

using System.ComponentModel.DataAnnotations;
namespace JWTAuthServer.DTOs
{
    public class LogoutRequestDTO
    {
        [Required]
        public string RefreshToken { get; set; }

        [Required]
        public string ClientId { get; set; }

        public bool IsLogoutFromAllDevices { get; set; }
    }
}
Key Points:
  • Validation: The [Required] attribute provides both RefreshToken and ClientId.
  • Security: Including ClientId helps verify that the refresh token belongs to the requesting client. The IsLogoutFromAllDevices property tells whether to logout the user from all devices.
Updating the AuthController with the Logout Endpoint

Now, implement the Logout endpoint within the AuthController. This endpoint will handle the revocation of refresh tokens. So, please add the following method to your existing AuthController:

// Only authenticated users can access the Logout endpoint.
[Authorize]
public async Task<IActionResult> Logout([FromBody] LogoutRequestDTO requestDto)
{
    //  Ensures that the incoming request contains both RefreshToken and ClientId.
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    // The user ID is extracted from the access token's claims to ensure that
    // the refresh token being revoked belongs to the authenticated user.
    var userIdClaim = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);

    if (userIdClaim == null)
    {
        return Unauthorized("Invalid access token.");
    }

    // Ensure that the refresh token being revoked belongs to the authenticated user.
    if (!int.TryParse(userIdClaim.Value, out int userId))
    {
        return Unauthorized("Invalid user ID in access token.");
    }

    // Hash the incoming refresh token to compare with stored hash
    var hashedToken = HashToken(requestDto.RefreshToken);

    // The hashed token, ClientId and User Id are used to locate the corresponding RefreshToken entity in the database.
    // Includes the User and Client entities for potential additional operations.
    var storedRefreshToken = await _context.RefreshTokens
        .Include(rt => rt.User)
        .Include(rt => rt.Client)
        .FirstOrDefaultAsync(rt => rt.Token == hashedToken && rt.Client.ClientId == requestDto.ClientId && rt.UserId == userId);

    // Checks if the refresh token exists.
    if (storedRefreshToken == null)
    {
        return Unauthorized("Invalid refresh token.");
    }

    // Ensures the token hasn't already been revoked to prevent redundant operations.
    if (storedRefreshToken.IsRevoked)
    {
        return BadRequest("Refresh token is already revoked.");
    }

    // Revoke the refresh token
    // Sets IsRevoked to true and updates the RevokedAt timestamp.
    storedRefreshToken.IsRevoked = true;
    storedRefreshToken.RevokedAt = DateTime.UtcNow;

    if (requestDto.IsLogoutFromAllDevices)
    {
        // Revoke all refresh tokens for the user
        // This is useful if you want to logout the user from all other devices.
        var userRefreshTokens = await _context.RefreshTokens
            .Where(rt => rt.UserId == storedRefreshToken.UserId && !rt.IsRevoked)
            .ToListAsync();

        foreach (var token in userRefreshTokens)
        {
            token.IsRevoked = true;
            token.RevokedAt = DateTime.UtcNow;
        }
    }

    foreach (var token in userRefreshTokens)
    {
        token.IsRevoked = true;
        token.RevokedAt = DateTime.UtcNow;
    }

    // Persists the changes to the database.
    await _context.SaveChangesAsync();

    // Returns a success message upon successful revocation.
    return Ok(new
    {
        Message = "Logout successful. Refresh token has been revoked."
    });
}
Testing the Logout Endpoint
Generate the Access Token using the Login Endpoint:

Authenticate using the Login endpoint to obtain both access and refresh tokens.
HTTP Method: POST
Endpoint: /api/Auth/Login
Headers:
Content-Type: application/json
Body (Raw JSON):

{
    "Email": "john.doe@example.com",
    "Password": "Password@123",
    "ClientId": "Client1"
}

Expected Response:
If the provided credentials are valid, you will get a response containing a valid JWT in the token field. You will need this token to authenticate requests.

{
    "Token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQ1NiJ9..."
    "RefreshToken": "E3478J/iVoKyiVn45fldoVuk2E+ui9/Y2yArIkSNFpPA/E4L…"
}
Access Protected Resource (Optional):

Use the access token to access a protected endpoint to ensure it’s valid. The following Get user profile endpoint retrieves the authenticated user’s profile information.

HTTP Method: GET
Endpoint: /api/Users/GetProfile
Headers:
Authorization: Bearer {JWT Token}}

Note: You need to replace {JWT Token} with the actual access token you received when you call the Login endpoint.

Expected Response:
You will get a response containing the user’s profile information, including roles, as shown below. Please ensure that the data matches what was registered.

{
    "Id": 1,
    "Email": "john.doe@example.com",
    "Firstname": "John",
    "Lastname": "Doe",
    "Roles": [
        "User"
    ]
}
Revoking the Refresh Token using the Logout Endpoint:

Call the Logout endpoint with the obtained refresh token and client ID.

HTTP Method: POST
Endpoint: /api/Auth/Logout
Headers:
Content-Type: application/json
Body (Raw JSON):

{
  "RefreshToken": "E3478J/iVoKyiVn45fldoVuk2E+ui9/Y2yArIkSNFpPA/E4LOzvITUdNVip2ZcVBYkmDIhVZ6icyyABz47mXsQ==",
  "ClientId": "Client1",
  "IsLogoutFromAllDevices": true
}

Expected Response:
You will get a response indicating that the Token Revocation was completed successfully as follows:

{
   "Message": "Logout successful. Refresh token has been revoked."
}
Verify Revocation:

Attempt to use the same refresh token with the RefreshToken endpoint to ensure it’s been revoked.
HTTP Method: POST
Endpoint: /api/Auth/RefreshToken
Content-Type: application/json
Body (Raw JSON):

{
    "RefreshToken": " E3478J/iVoKyiVn45fldoVuk2E+ui9/Y2yArIkSNFpPA/E4LOzvITUdNVip2ZcVBYkmDIhVZ6icyyABz47mXsQ==",
    "ClientId": "Client1"
}

Expected Response:
Here also, you will get a response containing a valid JWT Token and a new Refresh Token, as shown below, if the provided Refresh Token and ClientId are valid.
Invalid refresh token.

Why Is Revoking Refresh Tokens Necessary?
  • Token Theft: If a refresh token is stolen, an attacker can continue to obtain new access tokens, potentially accessing sensitive resources indefinitely.
  • Session Hijacking: Revoking refresh tokens ensures that stolen or misused tokens cannot be used to maintain unauthorized sessions.
  • Respond to Security Breaches: In a security incident (e.g., detected breach, compromised client application), revoking refresh tokens can quickly limit the damage by preventing further access.
  • Logout Functionality: When users log out, revoking their refresh tokens ensures their session is fully terminated, requiring re-authentication for future access.
  • Password Changes: After changing a password, revoking existing refresh tokens prevents old tokens from being used with potentially outdated credentials.

Revoking refresh tokens is a critical feature in JWT-based authentication systems, enhancing security and enabling better lifecycle and session management. It is essential for reducing risks associated with token theft, logout processes, and account changes.

In the next article, I will discuss implementing Role Based JWT Authentication in ASP.NET Core Web API. In this article, I explain How to Implement Client Validation using JWT Token-Based Authentication in ASP.NET Core Web API Application with an Example. I hope you enjoy this article on implementing client validation using JWT token-based authentication in the ASP.NET core web API.

Leave a Reply

Your email address will not be published. Required fields are marked *