Why is restricting users to a single active session critical in enterprise applications?
During a recent project for a highly regulated Healthcare SaaS platform, we encountered a significant security and compliance challenge. The system, built with an ASP.NET Core 8 backend, an Angular web frontend and a cross-platform mobile application, allowed medical staff to access sensitive patient records. However, during an internal security audit, we realized that users were sharing credentials to log in simultaneously across multiple devices, such as a desktop workstation and a mobile tablet.
This concurrent login behavior compromised the system’s strict audit trails. If a user updated a medical record from the web application while the same account was used on a mobile device to modify a prescription, race conditions occurred and attributing actions to the actual physical user became impossible. The business mandate was clear: we needed to enforce a strict single active session per user across all platforms. If a user logged into the web portal and subsequently logged into the mobile app, the web session had to be immediately invalidated with a clear message explaining why.
Solving this required moving beyond standard stateless JWT authentication. This challenge inspired this article, as managing cross-platform session state is a recurring architectural hurdle. By sharing our approach, engineering teams can implement robust session controls and avoid data integrity risks.
How does concurrent login impact system security and architecture?
The core business use case demanded that an account could only have one active footprint globally. In our architecture, the Angular frontend and the mobile application both consumed the same ASP.NET Core 8 RESTful API. When a user authenticated, the backend issued a JSON Web Token (JWT) that the client stored and attached to subsequent HTTP requests.
The issue surfaced when you consider how a user interacts with dual platforms. For example, when you hire app developer to create a mobile app that interfaces with an existing web platform, the authentication layer is often shared. If a user logs into the web application and starts an intensive data-entry task and then logs into the mobile app using the same credentials, both devices hold valid JWTs. Without a centralized tracking mechanism, the backend server blindly accepts both tokens until they expire. The symptom we observed was duplicate data entries and corrupted audit logs because the application could not distinguish between the physical user on the web and the potentially different user holding the mobile device.
What are the common pitfalls of stateless JWT authentication?
The primary architectural oversight in the initial design was relying solely on the stateless nature of JWTs. Because a JWT carries its own expiration and cryptographic signature, the backend does not inherently need to query a database to validate it. This is excellent for performance but terrible for session revocation.
Our application logs revealed that even if a user explicitly clicked log out on one device, the token on the second device remained fully authorized. We were lacking a mechanism to tie a specific token to a unique login event and we lacked a centralized state store to declare which login event was the currently active one. We needed a way to intercept requests, validate the token against a single source of truth and safely terminate older sessions.
What are the ways to enforce single active sessions in ASP.NET Core?
We evaluated several diagnostic steps and tradeoffs before settling on our final architecture. If you plan to hire dotnet developers for enterprise modernization, evaluating these tradeoffs is a critical part of the system design phase.
Can we use a database-based session tracking approach?
Our first thought was to add a SessionId column to our SQL database under the Users table. On every login, we would generate a new unique identifier, save it to the database and embed it in the JWT. On every API request, a middleware would query the database to ensure the token’s SessionId matched the database’s SessionId. While simple, we quickly discarded this approach because querying a relational database on every single HTTP request introduces a massive performance bottleneck.
Is blacklisting JWT tokens a viable solution?
We also considered maintaining a denylist of revoked tokens. When a user logged in on a new device, we would add the old token to a denylist. However, this meant we had to track every issued token globally and query this growing list on every request. It also required complex cleanup jobs to remove tokens from the denylist once their natural expiration time passed.
Why is Redis-backed active session tracking the optimal choice?
We ultimately decided on an active session tracking model using Redis. Redis is an in-memory data store that offers sub-millisecond response times, making it perfect for validating sessions on every request without dragging down API performance. On login, the backend generates a unique SessionId, stores it in Redis against the UserId and embeds it in the JWT. A custom ASP.NET Core middleware intercepts incoming requests, extracts the SessionId from the token and compares it to the value in Redis. If they differ, the request is rejected with a 401 Unauthorized status.
How to implement Redis-based session control in .NET 8 and Angular?
Our final implementation involved changes to both the backend authentication flow and the frontend HTTP request handling. We integrated Redis using the IDistributedCache interface in ASP.NET Core 8.
Updating the Authentication Endpoint
When a user successfully authenticates, we generate a unique session identifier. We save this identifier to Redis, automatically overwriting any previously stored session for that user.
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginDto request)
{
var user = await _userService.ValidateCredentials(request.Username, request.Password);
if (user == null) return Unauthorized();
var sessionId = Guid.NewGuid().ToString();
var cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(8)
};
await _cache.SetStringAsync($"ActiveSession:{user.Id}", sessionId, cacheOptions);
var token = _jwtService.GenerateToken(user, sessionId);
return Ok(new { Token = token });
}
Implementing the Session Validation Middleware
Next, we created a custom middleware to validate the token on every authenticated request. This middleware ensures the token being used matches the active session stored in Redis.
public class SingleSessionMiddleware
{
private readonly RequestDelegate _next;
public SingleSessionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IDistributedCache cache)
{
if (context.User.Identity.IsAuthenticated)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var tokenSessionId = context.User.FindFirst("SessionId")?.Value;
if (!string.IsNullOrEmpty(userId) && !string.IsNullOrEmpty(tokenSessionId))
{
var activeSessionId = await cache.GetStringAsync($"ActiveSession:{userId}");
if (activeSessionId != tokenSessionId)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.Headers.Add("X-Session-Terminated", "true");
await context.Response.WriteAsync("Session terminated due to login from another device.");
return;
}
}
}
await _next(context);
}
}
Handling the Response in Angular
On the Angular side, we implemented an HTTP Interceptor. This interceptor listens for 401 Unauthorized responses. If it detects our custom header, it triggers a user-friendly modal explaining the logout before clearing local storage and redirecting to the login screen. This seamless frontend handling is exactly why companies look to hire angular developers for secure frontends that prioritize user experience.
@Injectable()
export class SessionInterceptor implements HttpInterceptor {
constructor(private authService: AuthService, private router: Router) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
const isSessionTerminated = error.headers.get('X-Session-Terminated');
if (isSessionTerminated === 'true') {
alert('Your session has been terminated because your account was logged in from another device.');
this.authService.clearStorage();
this.router.navigate(['/login']);
}
}
return throwError(() => error);
})
);
}
}
What can engineering teams learn from session concurrency limits?
Implementing this architecture yielded several vital lessons for managing state in distributed environments. When organizations look to hire software developer teams, they should expect engineers to proactively address these underlying architectural challenges.
- Stateful rules require fast data stores: Mixing stateless JWTs with stateful business rules (like single sessions) necessitates high-performance caching like Redis. Relational databases will degrade API performance under this load.
- Custom Headers improve UI/UX: Returning a generic 401 Unauthorized is not enough. Adding custom headers (e.g., X-Session-Terminated) allows the frontend to distinguish between an expired token and a concurrent login termination.
- Security over convenience: Overwriting the active session identifier upon a new login is safer than blocking the new login attempt. If a user abandons a web session without logging out, they must still be able to access the mobile app.
- Middleware ordering matters: The session validation middleware must be registered in the ASP.NET Core pipeline after the Authentication middleware but before Authorization and Endpoint Routing.
- SignalR for Real-Time Feedback (Optional): For applications requiring instantaneous logout without waiting for the next HTTP request, integrating WebSockets (SignalR) alongside Redis allows the server to actively push a termination event to the old client.
How do we summarize the approach to cross-platform session management?
Restricting a user to a single active session across web and mobile platforms requires bridging the gap between stateless token authentication and stateful session requirements. By generating a unique session identifier on login, storing it in Redis and validating it via ASP.NET Core middleware on subsequent requests, we secured the healthcare application against concurrent login risks. The Angular interceptor then gracefully handles the client-side experience by translating backend security rules into clear user communication. If your organization is facing complex architectural challenges and needs dedicated technical expertise, contact us.
Social Hashtags
#DotNet8 #ASPNETCore #Angular #Redis #JWTAuthentication #SessionManagement #Cybersecurity #WebSecurity #SoftwareArchitecture #EnterpriseSecurity #FullStackDevelopment #DotNetDeveloper
Frequently Asked Questions
Because the validation relies on Redis, an in-memory data store, the performance impact is negligible. Redis handles sub-millisecond reads, making it highly efficient even for middleware that runs on every request.
If Redis is unreachable, the middleware will throw an exception, potentially blocking all authenticated requests. It is crucial to implement proper error handling, circuit breakers and Redis clustering to ensure high availability for the session state.
Yes. While the external provider handles the actual credential verification and initial token issuance, your backend API can still act as an API Gateway or middleware layer that tracks and enforces the active session footprint for your specific ecosystem.
This mechanism only enforces single active sessions for online API interactions. If the mobile app functions offline, the user can still interact with cached local data. The session termination will only enforce the logout once the mobile app reconnects and attempts to synchronize data with the ASP.NET Core backend.
Success Stories That Inspire
See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.

California-based SMB Hired Dedicated Developers to Build a Photography SaaS Platform

Swedish Agency Built a Laravel-Based Staffing System by Hiring a Dedicated Remote Team
















