How to prevent a user from logging into multiple devices and how do I end the session from the previous device?

Viewed 630

How do I prevent a user Id from only being able to log in to one computer at a time? An example, user "Sam" logs in to his Desktop. User "Sam" also logs in from their laptop. Now I need the session on the desktop killed or what I want to happen is to make the old token invalid on the desktop.

I'm using dotnet 5.0

Would you need a column called currentlyloggedinUser in the "users" table in the database to see if they have logged in using Boolean?

Would you also need a security stamp?

Below is the login method which is placed in the AuthController

[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login(UserForLoginDto userForLoginDto)
{

    try 
    {
        // does the user exist in the database
        var user = await _userManager.FindByNameAsync(userForLoginDto.Username);
        // does the password match
        var result = await _signInManager.CheckPasswordSignInAsync(user, userForLoginDto.Password, true);

        //if the users are logged in, store True Value in the database 
            
            user.nonconcurrent = true; 
            var noLoggedin = await _userManager.UpdateAsync(user);

        if(!result.IsLockedOut)
        {
            if(user.IsEnabled)
            {
                if (result.Succeeded)
                {   
                    var userToReturn = _mapper.Map<UserForReturnDto>(user); 

                    return Ok(new
                    {
                        token = GenerateJwtToken(user).Result,
                        user = userToReturn,                 
                    });
                }
                return Unauthorized("Login Failed"); //if username and password are incorrect return unauthorised
            }
            return Unauthorized("This account is disabled. Please get an administrator to unlock this account.");                               
        }                
        return Unauthorized("This account is locked out. Please try again in 10 minutes or get an " + 
            "administrator to unlock your account.");              
    }
    catch (ArgumentNullException) 
    {
        return Unauthorized("Login Failed");
    }
}

Below is part of the StartUp.cs

 namespace Schedular.API
{
    public class Startup
    {
      public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // services.AddDbContext<DataContext>(x => x.UseMySql(Configuration
            //     .GetConnectionString("DefaultConnection")));

            services.AddDbContext<DataContext>(x => x.UseMySql(Configuration
                .GetConnectionString("DefaultConnection"),
                        new MySqlServerVersion(new Version(8, 0, 21)), 
                        mySqlOptions => mySqlOptions
                            .CharSetBehavior(CharSetBehavior.NeverAppend)));
 
            var lockoutOptions = new LockoutOptions()
            {
                AllowedForNewUsers = true,
                DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10),
                MaxFailedAccessAttempts = 5
            };
                
            // for role and identity authentication
            // initial creation of user in the user table in database 
            IdentityBuilder builder = services.AddIdentityCore<User>(opt =>
            {
                // password requirements
                opt.Lockout = lockoutOptions;
                opt.Password.RequireDigit = true;
                opt.Password.RequiredLength = 8;
                opt.Password.RequireNonAlphanumeric = false;
                opt.Password.RequireUppercase = true;
            });

            builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
            builder.AddEntityFrameworkStores<DataContext>();
            builder.AddRoleValidator<RoleValidator<Role>>();
            builder.AddRoleManager<RoleManager<Role>>();
            builder.AddSignInManager<SignInManager<User>>();

            
            // allows api to use authentication 
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options => 
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
                            .GetBytes(Configuration.GetSection("AppSettings:Token").Value)),
                        ValidateIssuer = false,
                        ValidateAudience = false
                    };
                });  


            // authorisation policy
            services.AddAuthorization(options =>
            {
                options.AddPolicy("AdminAccess", policy => policy.RequireRole("Admin"));
                options.AddPolicy("everyone", policy => policy.RequireRole("Admin", "standard"));
            });

            services.AddControllers(options => 
            {
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
                
                options.Filters.Add(new AuthorizeFilter(policy));
            })
             .AddNewtonsoftJson(opt =>
            {
                opt.SerializerSettings.ReferenceLoopHandling = 
                Newtonsoft.Json.ReferenceLoopHandling.Ignore;
            }); 
            
            services.AddScoped<ITaskScheduleRepository, TaskScheduleRepository>();
            services.AddScoped<IUserRepository, UserRepository>();
            services.AddScoped<INotesRepository, NotesRepository>();
            services.AddScoped<IAttachmentFileRepository, AttachmentFileRepository>();
            services.AddScoped<ICustomerRepository, CustomerRepository>();
            services.AddScoped<IReportRepository, ReportRepository>();
            services.AddControllers();
            services.AddCors(); 
        


            //allows use of tokens
  

            // Auto Mapper Configurations
            var mappingConfig = new MapperConfiguration(mc =>
            {
                mc.AddProfile(new AutoMapperProfiles());
            });
            IMapper mapper = mappingConfig.CreateMapper();
            services.AddSingleton(mapper);            
        }

This is the user.cs model file

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;

namespace Schedular.API.Models
{
    public class User: IdentityUser<int>
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        // to disable account and have a time limiter on when it can be enabled again

        public bool nonconcurrent { get; set; }

        // connect User table to the userRole join table. the below configure the relationship
        public virtual ICollection<UserRole> UserRoles {get; set;}
    }
}

Below is the userForLoginDto

namespace Schedular.API.Dtos
{
    public class UserForLoginDto
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
}
2 Answers

There are several ways you could go about accomplishing this. For a simple starting point I'll provide two possablities.

First could be to include the user's IP address in your data (database, cache, token, etc). Then you could verify that the IP address of the first login is being used, and block all other login attempts or subsequent requests from any other IPs for the given user.

A simple way of doing this could look something like below:

public class UserForLoginDto
{
    public string Username { get; set; }
    public string Password { get; set; }
    public string CurrentIpAddress { get; set; }
}

Then you could pull the IP out of the request or http context and compare for validating on login like so:

if(HttpContext.Connection.RemoteIpAddress == userForLoginDto.CurrentIpAddress)
{
    // do things here after validation.
}
else
{
    // kick-out invalid login attempt.
}

Lastly you'll want to clear out that IP value when the user ends their session, gets kicked from inactivity, etc. So the next login can change the IP address if on a different device. Not a prefect solution, but a good way to start simple.

This would be good for a proof-of-concept, but not necessarily ideal for production as you can run into issues where IPs are not always unique.


Secondly instead of the IP address, which isn't always unique, you could instead generate a GUID, UUID, or some other kind of unique data that gets passed through tokens and persisted data ie in the database.

public class UserForLoginDto
{
    public string Username { get; set; }
    public string Password { get; set; }
    public Guid CurrentLogin { get; set; }
}

Then proceed to make the same kind of check as before on login:

if(Request.Headers.GetValues("your-token-here").FirstOrDefault() == userForLoginDto.CurrentLogin)
{
    // do things here after validation.
}
else
{
    // kick-out invalid login attempt.
}

So this is rather similar to the first example but now you have a most likely unique data point to check against on requests for safer assumptions. Just clear out the token data on session end and in where you're persisting it server side so you can reset it on next successful login.

You'll also want to make a new unique data point on each successful login, and if one already exists you could either end the previous session or invalidate the login attempt to help prevent multiple devices from being used at a time.


Try looking into JWT (Json Web Tokens) for token based approaches. There's lots of great examples out there and tutorials like JWT Authentication with C# or creating and validating jwt tokens in asp net core.

Again there's lots of ways to go about implementing complex things like this. So try out a few different approaches, see what you like, and what works best for you and your project or work. Personally I'd suggest looking into token based authentication and authorization so you can check every request more easily, but go with what works best in your scenario as you dig into it more.

Before creating and returning the session token, get the current active session token for the user, invalidate it and then return the new one. With this, we can make sure that there is only one valid session at a given point in time.

Related