How to extend and validate session in ASP.NET Core Identity? - asp.net-core

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 :(

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

Related

How to handle array claim values in ASP.net Core using OIDC

Im running Skorubas implementation of IdentityServer4
https://github.com/skoruba/IdentityServer4.Admin
For some reason I end up receiving a single role-claim with the claim-type "role" and a value of an array with all roles for the current user:
["SkorubaIdentityAdminAdministrator","MyRole"]
Now if I would to protect a "page" using the Authorize-attribute:
[Authorize(Role="MyRole")]
This would always end up with an access denied, since ASP.net Core expects multiple claims with the same claim-type, so in this case the claims would be
type | value
role:"SkorubaIdentityAdminAdministrator"
role:"MyRole"
is there any "best practice" to either parse the claims received and reformat them before they are processed by ASP.net core, or to tell the OpenIdConnect extension to handle the array-format as multiple claims?
Generally claims received in JWTs can be arrays or objects as well as simple types. The way to deal with this when using .NET attributes for authorization is via policies.
They are pretty simple and this Curity tutorial has some examples. This code snippet shows that the entire ClaimsPrincipal is available to policies so you could work with array claims easily in your use case:
options.AddPolicy("lowRisk", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(claim =>
claim.Type == "risk" && Int32.Parse(claim.Value) < 50
)
)
);
[HttpGet("lowrisk")]
[Authorize( Policy = "lowRisk")]
public IActionResult LowRisk()
{
return Ok();
}
Turns out that you can create your own ClaimActions, in the example above I had to do the following:
Firts of all.. create a new class:
public class RoleClaimAction : ClaimAction
{
private const string RoleClaimType = "role";
public RoleClaimAction() : base(RoleClaimType, ClaimValueTypes.String)
{
}
public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer)
{
//Map array of roles to separate role claims
var roles = userData.TryGetStringArray(RoleClaimType)?.ToList();
if (roles!.Any())
{
foreach (var role in roles!)
{
AddRoleClaim(identity, role, issuer);
}
return;
}
//If we only have one role (not an array), add it as a single role claim
var singleRole = userData.TryGetString(RoleClaimType);
if(!string.IsNullOrEmpty(singleRole))
AddRoleClaim(identity, singleRole, issuer);
}
private void AddRoleClaim(ClaimsIdentity identity, string role, string issuer)
{
identity.AddClaim(new Claim(JwtClaimTypes.Role, role, ClaimValueTypes.String, issuer));
}
}
This will simply validate that the user has a claim called roles, and the re-map the array-values to separate role-claims, which then "hooks" into the auth-framework.
To add your ClaimAction, simply add it as the following to your OpenIdConnectOptions:
options.ClaimActions.Add(new RoleClaimAction())
Now Authorize-attributes with roles, and the User.IsInRole(string) should work properly.

Perpetual expiry of claims in SignInWithClaimsAsync

I am using ASP.NET Core 3.1 with Identity and storing some basic user information like their full name in a claim using the code below (I am aware of checking password and stuff, ignoring it for brevity):
var user = await _userManager.FindByNameAsync(Input.Username);
var claims = new List<Claim>
{
new Claim("UserFullname", user.Fullname, ClaimValueTypes.String)
}
await _signInManager.SignInWithClaimsAsync(user, Input.RememberMe, claims);
I am accessing it in the _Layout.cshtml using the line below:
var userFullname = User.Claims.Single(c => c.Type == "UserFullname").Value;
The problem is, this seems to expire in some time even though the user is still logged in. I want this to be perpetual until the user logs out.
I am sure there has to be some way in startup.cs to control this and as far as possible, I would like to avoid overriding anything.
--EDIT--
As mentioned in the comments for answer by #yinqiu, I tried the cookie authentication scheme using the line below:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
But it did not help either.
I think you can try to override the SignInWithClaimsAsync method.
public override async Task SignInWithClaimsAsync(ApplicationUser user, AuthenticationProperties authenticationProperties, System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> additionalClaims)
{
if (authenticationProperties != null && authenticationProperties.IsPersistent)
{
authenticationProperties.ExpiresUtc = DateTimeOffset.UtcNow.AddYears(1);
}
await base.SignInWithClaimsAsync(user, authenticationProperties, additionalClaims);
}
This is the appropriate solution of your case:
If you are inheriting Identity Classes (IdentityRole,IdentityUser) into your custom classes then you need to use your inherited classes otherwise you use the default Identity Classes. You need a custom ClaimIdentity Class let assume 'ApplicationClaimsIdentityFactory' and this class should be inherited by UserClaimsPrincipalFactory<AspNetUser, AspNetRole>
Step1 Register your dependencies in Startup.cs
services.AddIdentity<AspNetUser, AspNetRole>().AddEntityFrameworkStores<ICRCOMDMSEntities>().AddDefaultTokenProviders();
services.AddScoped<IUserClaimsPrincipalFactory<AspNetUser>, ApplicationClaimsIdentityFactory>();
Step2:
Override the method CreateAsync in your custom claimsIdentityFactory Calss and here you need to create your custom claims and return like
public async override Task<ClaimsPrincipal> CreateAsync(AspNetUser user)
{
var principal = await base.CreateAsync(user);
((ClaimsIdentity)principal.Identity).AddClaims(new[] {
new Claim("UserLastLogin", user.LastLoginDate.ToString("dd/MM/yyyy hh:mm:ss tt"))
});
return principal;
}
Now your claims persists until user is logged in.

First login to web site after Azure AD authentication

I have an Asp.Net core 3.1 MVC web application. I use EF and Identity
I'm in the process of migrating the authentication process to Azure AD (organizational directory)
I need to find a way to know when the user is login to my site for the first time.
I want to add user-information such as prefered-color, name etc. and to store it in my local DB
My goal is to know when the user logs-in for the first time and redirect him to the profile page for the extra details
Basically, I do it with OnTicketReceived event.
In this event I get the current user after the login process completed, I get the user details from the token and check if the user already in my local DB.
Now I need to redirect it to his profile page if the user record was not found.
I cannot change the redirect URL in OnTicketReceived event, I decided to create a middleware as follows:
In OnTicketReceived I check if the user in my local DB and add "NewUser" key to the session
Added NewUserMiddleware that runs and checks the session key. If "NewUser" key exits, I generate a new record in my local DB and redirect to the profile page, then I remove the key
It's working but I'm sure that there is an easier way to do it...
I don't want to create a register button - the site is internal to the organization and I don't need to create an anonymous register page
Some code stuff:
in OnTicketReceived:
var db = context.HttpContext.RequestServices.GetService<MyDbContext>();
var user = db.Users.SingleOrDefault(u => u.Email == email);
if (user == null)
{
context.HttpContext.Session.SetInt32("NewUser", 1);
}
NewUserMiddleware:
public async Task InvokeAsync(HttpContext context)
{
int? isNewUser = context.Session.GetInt32("NewUser");
if(isNewUser.HasValue && isNewUser.Value == 1)
{
MyDbContext _dbContext = context.RequestServices.GetService(typeof(MyDbContext)) as MyDbContext;
var identity = context.User.Identity as ClaimsIdentity;
string email = identity.FindFirst(c => c.Type == "preferred_username")?.Value;
string name = identity.FindFirst(c => c.Type == "name")?.Value;
User user = new User()
{
Email = email,
UserName = name,
Color = GenerateColor()
};
_dbContext.Users.Add(user);
_dbContext.SaveChanges();
context.Session.Remove("NewUser");
await context.Session.CommitAsync();
context.Response.Redirect("account/profile", true);
}
await _next(context);
}

How to add and Persist a new ClaimsIdentity with custom claims to a ClaimsPrincipal after authentication by an OpenId Connect provider

I'm using OpenId Connect with .NetCore to send users to an external OpenId Connect authentication provider (OP) which returns relevant access tokens and an authenticated ClaimsPrinciple which has claims from the OP in such as Name, Email Address, Customer Id, etc. What I want to be able to do is once the User has been authenticated and the ClaimsPrinciple is returned from the OP, I would like to add a ClaimsIdentities with custom claims for each licence the user holds on there account to the ClaimsPrinciple. So when a user switches between their account licences I can access the correct identity for that licence and provide access to features based on the custom claims. Currently, I can add the custom claims to a ClaimsIdentity then add the ClaimsIdentity to the ClaimsPrinciple but the new identities are not persisted and added into the Cookie.
So A User can have multiple licences.
For each licence I want to add a ClaimsIdentity.
Then I want to persist the changes to the ClaimsPrinciple by using a cookie.
Here is a code snippet which will hopefully add some context. This is for the Login method in my application which is hit once the OP has authenticated the user.
public async Task<IActionResult> Login()
{
string token = HttpContext.Authentication.GetTokenAsync("access_token").Result;
string refreshToken = HttpContext.Authentication.GetTokenAsync("refresh_token").Result;
// ControllerBase User class this is the User I want to add the identites to, I think.
string userGuid = User.Claims.Where(c => c.Type == "Guid").FirstOrDefault().Value;
UserTokens userTokens = new UserTokens
{
LastUpdatedDate = DateTime.Now,
UserGuid = userGuid,
UserAccessToken = token,
RefreshToken = refreshToken
};
await _busClient.PublishAsync<UpdateUserTokens>(new UpdateUserTokens(userTokens));
// Domain Model User, a different User to the controllerBase User
// this is how a user is represented in my application but this has
// no control over authentication and claims
User user = await _requestClient.RequestAsync<UserGuidRequest, User>(new UserGuidRequest(userGuid));
var userLicences = await _requestClient.RequestAsync<UserLicenceRequest, List<UserLicence>>(new UserLicenceRequest(userGuid));
var identityServerUserClaims = User.Claims.ToList();
foreach(UserLicence userLicence in userLicences)
{
List<Claim> userLicenceIdentityClaims = new List<Claim>();
foreach (Claim claim in identityServerUserClaims)
{
userLicenceIdentityClaims.Add(claim);
}
userLicenceIdentityClaims.Add(new Claim(userLicence.RoleType.ToString(), ""));
var userLicenceIdentity = new ClaimsIdentity(userLicenceIdentityClaims, User.Identity.AuthenticationType);
userLicenceIdentity.Label = userLicence.Id.ToString();
User.AddIdentity(userLicenceIdentity);
}
//TODO: either need to sign out then back in or save new user claims to cookies somehow?
return View("Index", user);
}
If anyone could help it would be greatly appreciated. If you need any more information please ask and I'll try to provide whatever I can, hopefully, this is a good start.

userManager.FindByName does not return roles

I am using OpenIddict for token authentication. Yesterday when I call userManager.FindByNameAsync(request.Username) I get User with Roles.
Today I get user with Roles property count = 0.
I tried to load roles with await userManager.GetRolesAsync(user); and I get array with count 3. That means user has roles.
I do not know what changed, but how can I load user with roles with FindByNameAsync function?
Complete code:
[HttpPost("token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (request.IsPasswordGrantType())
{
var user = await userManager.FindByNameAsync(request.Username); //roles count is 0
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The email/password couple is invalid."
});
}
var roles = await userManager.GetRolesAsync(user); //roles count is 3
but how can I load user with roles with FindByNameAsync function?
You can't (at least not without implementing your own IUserStore).
Under the hood, the default ASP.NET Core Identity store implementation (based on EF) doesn't eagerly load the navigation properties associated with the user entity for performance reasons, which is why the Roles property is not populated.
To load the roles associated with a user, use UserManager.GetRolesAsync(user);.