Medical App Security: Fixing JWT Token Lifecycle Vulnerabilities

Summary

A critical architectural vulnerability was identified during the design phase of a medical reporting application. The initial implementation relied on a stateless JWT pattern without a mechanism for token revocation or session management. In a high-stakes environment like pharmacovigilance (handling adverse event reports), failing to manage the lifecycle of a token means that if a medical professional’s device is compromised, there is no way to invalidate the session without changing the global signing key, which would log out every user in the system.

Root Cause

The primary failure was a misunderstanding of the stateless nature of JWTs versus the stateful requirements of security compliance.

  • Lack of Revocation Capability: Purely stateless JWTs cannot be “killed.” Once issued, they are valid until they expire.
  • Absence of Refresh Token Logic: The original implementation provided a single token without a strategy for rotation, leading to either dangerously long-lived access tokens or frequent user friction.
  • Single Point of Failure: The design did not account for multi-device concurrency, making it impossible to distinguish between a legitimate user session and a hijacked one.

Why This Happens in Real Systems

In production environments, engineers often prioritize scalability and simplicity over security and auditability.

  • The Scalability Trap: Engineers assume that “stateless is better” because it avoids database lookups on every request. While true for performance, it is false for security control.
  • Complexity Avoidance: Implementing a Refresh Token Rotation strategy requires managing state in a database (like MySQL), which adds overhead to the authentication flow.
  • Implicit Trust: Developers often assume that because the token is signed, the identity is “safe,” forgetting that a stolen signed token is a golden ticket to the system.

Real-World Impact

In a medical or pharmacovigilance context, the impact is catastrophic:

  • Unauthorized Data Access: An attacker could intercept a JWT and gain access to sensitive Adverse Event Reports, violating HIPAA or GDPR regulations.
  • Identity Impersonation: A compromised “Doctor” or “Specialist” role allows an attacker to inject fraudulent medical data into the reporting pipeline.
  • Audit Failure: If tokens cannot be revoked, the system cannot provide a clean audit trail showing exactly when a compromised session was terminated.

Example or Code (if necessary and relevant)

public class RefreshToken
{
    public int Id { get; set; }
    public string Token { get; set; } = string.Empty;
    public DateTime Expires { get; set; }
    public bool IsRevoked { get; set; }
    public DateTime Created { get; set; }
    public DateTime? Revoked { get; set; }
    public string UserId { get; set; } = string.Empty;
    public string DeviceInfo { get; set; } = string.Empty;
}

public async Task RefreshTokenAsync(string expiredToken, string refreshToken)
{
    var principal = GetPrincipalFromExpiredToken(expiredToken);
    var user = await _userManager.FindByNameAsync(principal.Identity.Name);

    var storedToken = await _context.RefreshTokens
        .FirstOrDefaultAsync(t => t.Token == refreshToken && t.UserId == user.Id);

    if (storedToken == null || storedToken.IsRevoked || storedToken.Expires < DateTime.UtcNow)
    {
        throw new SecurityException("Invalid refresh token.");
    }

    var newAccessToken = _jwtGenerator.GenerateToken(user);
    var newRefreshToken = _jwtGenerator.GenerateRefreshToken();

    storedToken.IsRevoked = true;
    storedToken.Revoked = DateTime.UtcNow;

    _context.RefreshTokens.Add(new RefreshToken 
    { 
        Token = newRefreshToken, 
        UserId = user.Id, 
        Expires = DateTime.UtcNow.AddDays(7) 
    });

    await _context.SaveChangesAsync();

    return new TokenResponseDTO { AccessToken = newAccessToken, RefreshToken = newRefreshToken };
}

How Senior Engineers Fix It

Senior engineers implement Defense in Depth by combining stateless access with stateful control.

  • Short-Lived Access Tokens: Keep JWT lifetimes extremely short (e.g., 5–15 minutes) to minimize the window of exploitation.
  • Stateful Refresh Tokens: Store refresh tokens in the database (MySQL) linked to a UserId and a DeviceId. This allows for granular revocation (logging out one device without affecting others).
  • Refresh Token Rotation: Every time a refresh token is used, issue a new refresh token and invalidate the old one. This detects token reuse by attackers.
  • Database Security: Use Encryption at Rest for sensitive user identifiers in MySQL and ensure the JWT signing key is managed via a Key Vault, not hardcoded in appsettings.json.

Why Juniors Miss It

  • Focus on the “Happy Path”: Juniors focus on making the login work, whereas seniors focus on making the login fail safely.
  • Over-reliance on Library Defaults: They assume that calling AddJwtBearer in .NET handles all security concerns, ignoring the business logic required for session management.
  • Missing the Lifecycle Perspective: Juniors view authentication as a single event (Login), while seniors view it as a continuous lifecycle (Issue $\rightarrow$ Use $\rightarrow$ Refresh $\rightarrow$ Revoke $\rightarrow$ Expire).

Leave a Comment