Search code examples
asp.net-coreasp.net-core-identity

How to extend and validate session in ASP.NET Core Identity?


We want to offer the users to manage their login sessions. This worked so far pretty easy with ASP.NET Core and WITHOUT the Identity Extensions.

https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1#react-to-back-end-changes

But how can we invoke this validation with ASP.NET Core Identity?

Problem we have:

  • How do we store login-session-based information like Browser Version, Device Type and User Position? Do we extend any type or what is the idea?
  • How do we dynamically set the cookie expiration based on a specific user?
  • How do we invalidate the Cookie from the backend (like the link above shows)?
  • How do we required additional password-prompts for special functions?

It feels the ASP.NET Core Identity is still not that extensible and flexible :(


Solution

  • Unfortunately, this area of ASP.NET Identity is not very well documented, which I personally see as a risk for such a sensitive area.

    After I've been more involved with the source code, the solution seems to be to use the SignIn process of the SignIn Manager.

    The basic problem is that it's not that easy to get your custom claims into the ClaimsIdentity of the cookie. There is no method for that. The values for this must under no circumstances be stored in the claims of the user in the database, as otherwise every login receives these claims - would be bad.

    So I created my own method, which first searches for the user in the database and then uses the existing methods of the SignInManager.

    After having a ClaimsIdentity created by the SignIn Manager, you can enrich the Identity with your own claims. For this I save the login session with a Guid in the database and carry the id as a claim in the cookie.

        public async Task<SignInResult> SignInUserAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
        {
            DateTimeOffset createdLoginOn = DateTimeOffset.UtcNow;
            DateTimeOffset validTo = createdLoginOn.AddSeconds(_userAuthOptions.ExpireTimeSeconds);
    
            // search for user
            var user = await _userManager.FindByNameAsync(userName);
            if (user is null) { return SignInResult.Failed; }
    
    
            // CheckPasswordSignInAsync checks if user is allowed to sign in and if user is locked
            // also it checks and counts the failed login attempts
            var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure);
            if (attempt.Succeeded)
            {
                // TODO: Check 2FA here
    
                // create a unique login entry in the backend
                string browserAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"];
    
                Guid loginId = await _eventDispatcher.Send(new AddUserLoginCommand(user.Id, user.UserName, createdLoginOn, validTo, browserAgent));
    
                // Write the login id in the login claim, so we identify the login context
                Claim[] customClaims = { new Claim(CustomUserClaims.UserLoginSessionId, loginId.ToString()) };
    
                // Signin User
                await SignInWithClaimsAsync(user, isPersistent, customClaims);
    
                return SignInResult.Success;
            }
    
            return attempt;
        }
    

    With each request I can validate the ClaimsIdentity and search for the login id.

    public class CookieSessionValidationHandler : CookieAuthenticationEvents
    {
        public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
        {
            ClaimsPrincipal userPrincipal = context.Principal;
    
            if (!userPrincipal.TryGetUserSessionInfo(out int userId, out Guid sessionId))
            {
                // session format seems to be invalid
                context.RejectPrincipal();
            }
            else
            {
                IEventDispatcher eventDispatcher = context.HttpContext.RequestServices.GetRequiredService<IEventDispatcher>();
    
                bool succeeded = await eventDispatcher.Send(new UserLoginUpdateLoginSessionCommand(userId, sessionId));
                if (!succeeded)
                {
                    // session expired or was killed
                    context.RejectPrincipal();
                }
            }
        }
    }
    

    See also https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1#react-to-back-end-changes