Reputation: 1838
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.
But how can we invoke this validation with ASP.NET Core Identity?
Problem we have:
It feels the ASP.NET Core Identity is still not that extensible and flexible :(
Upvotes: 2
Views: 5420
Reputation: 1838
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();
}
}
}
}
Upvotes: 1