Overriding ASP.NET Core Windows Authentication Identity - asp.net-core

I was looking to use Windows Authentication for a Blazor Server app, but hit a small issue with the default Identity Name.
i.e. when you used
<AuthorizeView>
Hi #context.User.Identity.Name
</AuthorizeView>
I got back 'Hi DOMAIN\A123456', which might be the AD object name, but its not what users would say their name was. I also noticed during debugging that the Identity had pulled back all of my AD groups, but not things like Given Name.
How can I override/amend/alter the processing to 'fix' this, ideally put a proper name in the Name claim and move the id into the NameIdentifier claim.

This is what I came up with using IClaimsTransformation, but not sure if its the right approach at all, esp given that with just the out-of-the-box Blazor project this thing is called 7 times! If I added any db type logic to get the roles or name then this is going tank performance...
public class RoleClaimsTransformer : IClaimsTransformation
{
private readonly ILogger<RoleClaimsTransformer> _logger;
public RoleClaimsTransformer(ILogger<RoleClaimsTransformer> logger)
{
_logger = logger;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
_logger.LogDebug($"Role Transform for {principal.Identity.Name} Auth: {principal.Identity.IsAuthenticated}");
//get the original name claim
var ci = (ClaimsIdentity)principal.Identity;
Claim nameClaim = principal.FindFirst(ci.NameClaimType);
//create a new principal
ClaimsPrincipal newCP = new ClaimsPrincipal();
//and a new identity, using the original authtype (just in case it matters down the line)
var newId = new GenericIdentity("Joe Bloggs", principal.Identity.AuthenticationType);
//add the original name as a NameId
newId.AddClaim(new Claim(ClaimTypes.NameIdentifier, nameClaim.Value));
//add roles etc
newId.AddClaim(new Claim(ClaimTypes.Role, "admin"));
newCP.AddIdentity(newId);
return Task.FromResult(newCP);
}
}
Hopefully its reasonably clear what I've done, but basically ignore the principal from the built in Windows Auth and create your own. Also note that the GenericIdentity does want a ClaimTypes.Role for roles (for use in the AuthorizeView components), and not whatever type the WindowsIdentity needed.
I've subsequently realised that WindowsAuthentication isn't not going to work for my app, and I'll go back to custom auth that just uses AD to check their passwords via a standard pair of login boxes.

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.

How to save 'department id' and 'user id' of 'current logged in user' while saving a record in asp.net core 3.0+?

Previously in asp.net webforms I was used to save current user id, their department id, branch id (in case of multiple branches of organisation), branch name etc in cookies and was using this information to avoid extra server calls while saving, updating, retrieving or deleting record and also for tracking that who deleted, updated or created a specific record. But now in asp.net core 2.0, 3.0+ I wander how to handle this thing. I am thinking to handle this by creating claim and saving this information in claims and then using it for good. Am I doing it wisely or Is there any other effective/efficient way of doing this whole practice ? Can I use JWT for this purpose or not ?
You can create a class (e.g.,) MyUserClaimsFactory that inherits from UserClaimsPrincipalFactory<User, Role> where User and Role are you custom classes that derive from IdentityUser and IdentityRole.
In that class, override the CreateAsync method
public async override Task<ClaimsPrincipal> CreateAsync(User user)
{
var principal = await base.CreateAsync(user);
// Add all the claims you need here.
((ClaimsIdentity)principal.Identity).AddClaims(new[] { new Claim(ClaimTypes.GivenName, user.Name) });
return principal;
}
Instead of ClaimTypes you can also use a string as key.
Add the class via dependency injection:
services.AddScoped<IUserClaimsPrincipalFactory<User>, MyUserClaimsFactory>();
You can retrieve the claims in a Controller via this.User.Identity
var givenName = (this.User.Identity as ClaimsIdentity).FirstOrNull(ClaimTypes.GivenName);

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

How to get Name of Authenticated user in ASP.NET Core application using Azure Active Directory

In a ASP.NET Core application with Azure AD Connected and Configured. I am able to get the NameIdentifier using this code:
var user = User.FindFirst(ClaimTypes.NameIdentifier).Value; ✔️
When trying to get just a name with the following line of code:
var user = User.FindFirst(ClaimTypes.Name).Value; ❌
I receive the following error:
Object reference not set to an instance of an object.
Looked up in Azure Active Directory the user does have a full first and last name. Tried many examples online, it looks like the name should be showing.
UPDATE:
Finally figured it out, at first I managed to get all the human readable text like this:
foreach (Claim item in User.Claims)
{
if (item.Type.Contains("name"))
{
var username = item.Value;
}
}
this is much better
var userName = User.FindFirst("name").Value;
According to document : https://learn.microsoft.com/en-us/azure/architecture/multitenant-identity/claims :
In ASP.NET Core, the OpenID Connect middleware converts some of the claim types when it populates the Claims collection for the user principal:
oid > http://schemas.microsoft.com/identity/claims/objectidentifier
tid > http://schemas.microsoft.com/identity/claims/tenantid
unique_name >
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
upn > http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn
So that if the unique_name exists in ID token , the middleware will populate the value of ClaimTypes.Name . But according to document : Microsoft identity platform ID tokens:
unique_name : Provides a human readable value that identifies the subject of the token. This value isn't guaranteed to be unique within a tenant and should be used only for display purposes. Only issued in v1.0 id_tokens.
So that the claim only issued in Azure AD V1.0 id tokens ,and it isn't guaranteed to be unique within a tenant .
You can get the user's name and email by (The profile scope is required to receive the claims & test with Azure AD V2.0 app):
var userName = User.FindFirst("name").Value;
var Email = User.FindFirst("preferred_username").Value;
If you want to know the first name and last name of current user , you may acquire access token for Microsoft Graph , call user endpoint with token and check the givenName/surname values from response :
https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http
I struggled with the same issue and found an example and it works very well:
Basically:
public WeatherForecastController(ILogger<WeatherForecastController> logger, ITokenAcquisition tokenAcquisition, GraphServiceClient graphServiceClient, IOptions<MicrosoftGraphOptions> graphOptions)
{
_logger = logger;
_tokenAcquisition = tokenAcquisition;
_graphServiceClient = graphServiceClient;
_graphOptions = graphOptions;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
User user = _graphServiceClient.Me.Request().GetAsync().GetAwaiter().GetResult();
_logger.LogInformation("User Id: " + user.Id.ToString());
This link will give the details on how to use it with Azure AD, .net web api, and angular
active-directory-dotnet-native-aspnetcore-v2

How do I create a ClaimsIdentity object for Asp.NET MVC 5?

I am working in a .NET MVC 5 application. I do not want to use Entity Framework. I want to authenticate to a RavenDB database. It looks to me that I want to replace the UserManager that comes with the Account Controller. I think I can rewrite all the UserManager functions to work with my database, except I don't understand the ClaimsIdentity object.
In the SignInAsync method, there is a call to UserManager.CreateIdentityAsync(...). I know it returns a ClaimsIdentity object. What I don't know is how to create a ClaimsIdentity object on my own.
I see that it has 4 properties Actor, BootstrapContext, Claims and Label. I don't know what these properties are used for, and I don't know how to properly generate them. I assume generating them correctly is important since it is how the authentication cookie is made.
I looked at the explanation of the ClaimsIdentity object here, but that didn't really help me understand.
If I could see the code for CreateIdentityAsync(), that would probably help.
If I'm going about this all wrong, please let me know. Otherwise, if someone could point me toward how to generate the ClaimsIdentity object, that would be helpful.
ClaimsIdentity identity = new ClaimsIdentity
{
Actor = ????,
BootstrapContext = ?????,
Claims = ?????,
Label = ?????
}
Perhaps the following link can help:
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, "Brock"));
claims.Add(new Claim(ClaimTypes.Email, "brockallen#gmail.com"));
var id = new ClaimsIdentity(claims,DefaultAuthenticationTypes.ApplicationCookie);
var ctx = Request.GetOwinContext();
var authenticationManager = ctx.Authentication;
authenticationManager.SignIn(id);
I think you are "going about this all wrong". The ASP.NET Identity Framework is designed with pluggable persistence in mind. The correct way to substitute RavenDB for EF is NOT to replace the UserManager. Rather, it is to implement a replacement UserStore. So the line in the AccountController that creates the UserManager would change from:
public AccountController()
: this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext())))
To:
public AccountController()
: this(new UserManager<ApplicationUser>(new RavenDBUserStore<ApplicationUser>/* connection info here*/)))
Where the RavenDBUserStore would be a class you wrote that implemented IUserStore, IUserPasswordStore and whatever other *Store interfaces you needed for your particular application.
This approach avoids you needing to understand and re-implement everything in UserManager and ensures that you will be able to take advantage of future improvements to the UserManager from MS.
For more information on how to do this, see the Overview of Custom Storage Providers for ASP.NET Identity and the example of Implementing a Custom MySQL ASP.NET Identity Storage Provider. You should also check out the code of the RavenDB.AspNet.Identity Nuget Package created by David Boike mentioned in his answer. The source is on github at https://github.com/ILMServices/RavenDB.AspNet.Identity/tree/master/RavenDB.AspNet.Identity
Here is what I came up with. I would love to know if this is the correct way to accomplish this task.
Working in a default MVC5 website, I went to the Account Controller, and found the SignInAsync() function. I adjusted it as follows:
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
//var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); --> this is where I want to get rid of UserManager
List<Claim> claims = new List<Claim>{
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", user.Name), //user.Name from my database
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", user.Id), //user.Id from my database
new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "MyApplication"),
new Claim("FirstName", user.FirstName) //user.FirstName from my database
};
ClaimsIdentity identity = new System.Security.Claims.ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.Name, ClaimTypes.Role);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
Keep in mind that this also requires changing the [HttpPost] Login function to get the user from my database instead of using the UserManager.FindAsync() function.
The LogIn/LogOff parts of the default site seem to work fine after these adjustments. I'll leave this answer here for a while before I accept it in case someone can tell me why I shouldn't do it this way.
I created a RavenDB implementation for the new ASP.NET MVC 5 Identity and released it as a NuGet package. This might work for you.
http://www.nuget.org/packages/RavenDB.AspNet.Identity/
It's somewhat prerelease but I am using it in a real project.
Here's the GitHub project and a short thread on the RavenDB discussion group about it.