ASp.net core 2 Identity app sometimes looses claim when kept logged in - asp.net-core

I have an authorization handler, which is meant to restrict users to accessing info only for the organization they belong to, or ANY if they are of a certain account type (known as SuperAdmin here). Aforementioned account type may or may not have an associated Organization, which happens to be a navigation property. It should be mentioned I am working with "user" classes extended from ApplicationUser in the Identity framework.
Here's the handler:
public class OrganizationHandler : AuthorizationHandler<SameOrgRequirment,
Organization>
{
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
SameOrgRequirment requirement, Organization resource)
{
var value = context.User.FindFirst( "OrganizationID").Value;
long claimValue = long.Parse(value);
if(claimValue==resource.Id)
{
context.Succeed(requirement);
}
else
{
string orgClaimValue = context.User.FindFirst(c => c.Type ==
"AccountTypes").Value;
AccountTypes claimAsType =
(AccountTypes)Enum.Parse(typeof(AccountTypes), orgClaimValue);
if(claimAsType == AccountTypes.SuperAdmin)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
public class SameOrgRequirment:IAuthorizationRequirement{}
The problem is I am SOMETIMES getting a NullReferenceException on the line
var value = context.User.FindFirst( "OrganizationID").Value;
which makes no sense as all users in my dev setup have an organizationId navigation property. Inspecting the ClaimsPrincipal I see the AccountTypes claim (see below) exists but the other, OrganizationId does not when this NullReferenceException is thrown.
I have overridden the AppClaimsPrincipalFactory to create these claims:
public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, ApplicationRole>
{
public AppClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager
, RoleManager<ApplicationRole> roleManager
, IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{ }
public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
if (!string.IsNullOrWhiteSpace(user.Type.ToString()))
{
((ClaimsIdentity)principal.Identity).AddClaims(new[]
{
new Claim("AccountTypes", user.Type.ToString())
});
}
try
{
if (!string.IsNullOrWhiteSpace(user.Organization.Id.ToString()))
{
//Used later to restrict data
((ClaimsIdentity)principal.Identity).AddClaims(new[]
{
new Claim("OrganizationId", user.Organization.Id.ToString()),
});
}
}
catch(NullReferenceException ex)
{
((ClaimsIdentity)principal.Identity).AddClaims(new[]
{
new Claim("OrganizationId", "-1"),
});
}
return principal;
}
}
The AccountTypes claim doesnt seem to get lost.
The only thing I have narrowed down is that this only seems to happen for an account where Remember Me? is checked and the app is closed and reopened via visual studio. If I log out and log back with the same account, it works fine! All claims are there and the handler works correctly. Being able to stay logged is a customer requirement so I can't really change that.

Related

Blazor - B2C authentication - What is the proper way to persist user data on login?

I'm building a Blazor app to see how I can persist user data after a B2C AD login.
I want to persist claim data to sql database (ef 6 core) when the user logs in to the app.
I'm trying to capture a Tenant for the user for use in filtering on the app.
I is custom middleware a good way to go with this?
This is a Blazor Server Side app
I have something like this for testing.
public class PersistUserChangesMiddleware
{
private readonly RequestDelegate _next;
public PersistUserChangesMiddleware(RequestDelegate next)
{
_next = next;
}
[Authorize]
public Task Invoke(HttpContext httpContext, MyContext context)
{
try
{
var user = httpContext.User;
var claims = user.Claims;
var tenant = claims?.FirstOrDefault(c => c.Type.Equals("extension_CompanyId", StringComparison.OrdinalIgnoreCase));
if(tenant != null)
{
context.Tenants.Add(new Models.Tenant()
{
TenantName = tenant.Value
});
context.SaveChanges();
}
}
catch (Exception)
{
throw;
}
return _next(httpContext);
}
}
}
I'm not getting the user back from this call in the middleware. Do I need to do it a different way for Blazor? I set [Authorize] but still no user.
I can't see which AuthenticationStateProvider is configured, but it's likely to be ServerAuthenticationStateProvider.
Create a custom AuthenticationStateProvider which is essentially a pass through provider that just grabs the ClaimsPrincipal user and does whatever you want with it. (Let me know if you're using a different provider).
public class MyAuthenticationStateProvider : ServerAuthenticationStateProvider
{
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var authstate = await base.GetAuthenticationStateAsync();
if (authstate.User is not null)
{
ClaimsPrincipal user = authstate.User;
// do stuff with the ClaimsPrincipal
}
return authstate;
}
}
And then register it in Program:
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// sequence is crucial - Must be after AddServerSideBlazor
builder.Services.AddScoped<AuthenticationStateProvider, MyAuthenticationStateProvider>();
builder.Services.AddSingleton<WeatherForecastService>();
Test it with a break point on the first line of GetAuthenticationStateAsync.

Why do I need to set a DefaultForbidScheme

In a WebAPI .net core project I have created a Middleware class that validates an api key. From validating it, it retrieves the permissions that the key has (user or admin) within the invoke method.
I pass it through a switch to set the principle like so
GenericIdentity identity = new GenericIdentity("API");
GenericPrincipal principle = null;
//we have a valid api key, so set the role permissions of the key
switch (keyValidatorRes.Role)
{
case Roles.User:
principle = new GenericPrincipal(identity, new[] { "User" });
context.User = principle;
break;
case Roles.Admin:
principle = new GenericPrincipal(identity, new[] { "Admin" });
context.User = principle;
break;
default:
principle = new GenericPrincipal(identity, new[] { "Other" });
context.User = principle;
break;
}
On controllers methods I have
[Authorize(Roles = "Admin")]
to validate the roles of an authenticated api key
If the user has the admin principle it goes through as expected. However, if it has a user or other principle then I get an error about
not having a DefaultForbidScheme
I googled around and added Authentication to my startup.cs with a customer scheme
services.AddAuthentication(options=> {
options.DefaultForbidScheme = "forbidScheme";
options.AddScheme<AuthSchemeHandle>("forbidScheme", "Handle Forbidden");
});
and created the AuthSchemeHandle
public class AuthSchemeHandle : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.FromResult(AuthenticateResult.Fail("Failed Auth"));
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
Now if the principle does not have Admin it fails without the error but the response that is returned on the API is 200 with no content. I was expecting a 4xx response with the message "Failed Auth"
I am just trying to work out why it is not as expected as although it seems "fixed" I do not understand how it has fixed it.
Is there a better way that I should be doing this?
why it is not as expected as although it seems "fixed" I do not understand how it has fixed it.
There's no dark magic when the authentication handler calls IAuthenticationHandler.ForbidAsync() method. We have to do relevant things ourself. In short, setting the StatusCode=403 as your need.
public async Task ForbidAsync(AuthenticationProperties properties)
{
properties = properties ?? new AuthenticationProperties();
_context.Response.StatusCode = 403;
// ...
return Task.CompletedTask;
}
As a side note, you don't need return a Task.FromResult() as it doesn't care about the result.
Is there a better way that I should be doing this?
The ASP.NET Core Team provides us an abstract class AuthenticationHandler to handle authentication. This abstract class has a built-in implementation for ForbidAsync(AuthenticationProperties properties) (and also for other public methods). So it's much easy to extends this abstract class as below:
public class MyAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public MyAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
return AuthenticateResult.NoResult();
}
}
Finally, add a configuration for authentication service:
services
.AddAuthentication(options=>{
options.DefaultAuthenticateScheme = "forbidScheme";
options.DefaultForbidScheme = "forbidScheme";
options.AddScheme<MyAuthenticationHandler>("forbidScheme", "Handle Forbidden");
});
It should work as expected.

ASP.NET Core Identity 2: User.IsInRole always returns false

The question: I call RoleManager.CreateAsync() and RoleManager.AddClaimAsync() to create roles and associated role claims. Then I call UserManager.AddToRoleAsync() to add users to those roles. But when the user logs in, neither the roles nor the associated claims show up in the ClaimsPrincipal (i.e. the Controller's User object). The upshot of this is that User.IsInRole() always returns false, and the collection of Claims returned by User.Claims doesn't contain the role claims, and the [Authorize(policy: xxx)] annotations don't work.
I should also add that one solution is to revert from using the new services.AddDefaultIdentity() (which is provided by the templated code) back to calling services.AddIdentity().AddSomething().AddSomethingElse(). I don't want to go there, because I've seen too many conflicting stories online about what I need to do to configure AddIdentity for various use cases. AddDefaultIdentity seems to do most things correctly without a lot of added fluent configuration.
BTW, I'm asking this question with the intention of answering it... unless someone else gives me a better answer than the one I'm prepared to post. I'm also asking this question because after several weeks of searching I have yet to find a good end-to-end example of creating and using Roles and Claims in ASP.NET Core Identity 2. Hopefully, the code example in this question might help someone else who stumbles upon it...
The setup:
I created a new ASP.NET Core Web Application, select Web Application (Model-View-Controller), and change the Authentication to Individual User Accounts. In the resultant project, I do the following:
In Package Manager Console, update the database to match the scaffolded migration:
update-database
Add an ApplicationUser class that extends IdentityUser. This involves adding the class, adding a line of code to the ApplicationDbContext and replacing every instance of <IdentityUser> with <ApplicationUser> everywhere in the project.
The new ApplicationUser class:
public class ApplicationUser : IdentityUser
{
public string FullName { get; set; }
}
The updated ApplicationDbContext class:
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{ }
// Add this line of code
public DbSet<ApplicationUser> ApplicationUsers { get; set; }
}
In Package Manager Console, create a new migration and update the database to incorporate the ApplicationUsers entity.
add-migration m_001
update-database
Add the following line of code in Startup.cs to enable RoleManager
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>() // <-- Add this line
.AddEntityFrameworkStores<ApplicationDbContext>();
Add some code to seed roles, claims, and users. The basic concept for this sample code is that I have two claims: can_report allows the holder to create reports, and can_test allows the holder to run tests. I have two Roles, Admin and Tester. The Tester role can run tests, but can't create reports. The Admin role can do both. So, I add the claims to the roles, and create one Admin test user and one Tester test user.
First, I add a class whose sole purpose in life is to contain constants used elsewhere in this example:
// Contains constant strings used throughout this example
public class MyApp
{
// Claims
public const string CanTestClaim = "can_test";
public const string CanReportClaim = "can_report";
// Role names
public const string AdminRole = "admin";
public const string TesterRole = "tester";
// Authorization policy names
public const string CanTestPolicy = "can_test";
public const string CanReportPolicy = "can_report";
}
Next, I seed my roles, claims, and users. I put this code in the main landing page controller just for expedience; it really belongs in the "startup" Configure method, but that's an extra half-dozen lines of code...
public class HomeController : Controller
{
const string Password = "QwertyA1?";
const string AdminEmail = "admin#example.com";
const string TesterEmail = "tester#example.com";
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<ApplicationUser> _userManager;
// Constructor (DI claptrap)
public HomeController(RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
public async Task<IActionResult> Index()
{
// Initialize roles
if (!await _roleManager.RoleExistsAsync(MyApp.AdminRole)) {
var role = new IdentityRole(MyApp.AdminRole);
await _roleManager.CreateAsync(role);
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanReportClaim, ""));
}
if (!await _roleManager.RoleExistsAsync(MyApp.TesterRole)) {
var role = new IdentityRole(MyApp.TesterRole);
await _roleManager.CreateAsync(role);
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
}
// Initialize users
var qry = _userManager.Users;
IdentityResult result;
if (await qry.Where(x => x.UserName == AdminEmail).FirstOrDefaultAsync() == null) {
var user = new ApplicationUser {
UserName = AdminEmail,
Email = AdminEmail,
FullName = "Administrator"
};
result = await _userManager.CreateAsync(user, Password);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
result = await _userManager.AddToRoleAsync(user, MyApp.AdminRole);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
}
if (await qry.Where(x => x.UserName == TesterEmail).FirstOrDefaultAsync() == null) {
var user = new ApplicationUser {
UserName = TesterEmail,
Email = TesterEmail,
FullName = "Tester"
};
result = await _userManager.CreateAsync(user, Password);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
result = await _userManager.AddToRoleAsync(user, MyApp.TesterRole);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
}
// Roles and Claims are in a cookie. Don't expect to see them in
// the same request that creates them (i.e., the request that
// executes the above code to create them). You need to refresh
// the page to create a round-trip that includes the cookie.
var admin = User.IsInRole(MyApp.AdminRole);
var claims = User.Claims.ToList();
return View();
}
[Authorize(policy: MyApp.CanTestPolicy)]
public IActionResult Test()
{
return View();
}
[Authorize(policy: MyApp.CanReportPolicy)]
public IActionResult Report()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
and I register my authentication policies in the "Startup" ConfigureServices routine, just after the call to services.AddMvc
// Register authorization policies
services.AddAuthorization(options => {
options.AddPolicy(MyApp.CanTestPolicy, policy => policy.RequireClaim(MyApp.CanTestClaim));
options.AddPolicy(MyApp.CanReportPolicy, policy => policy.RequireClaim(MyApp.CanReportClaim));
});
Whew. Now, (assuming I've noted all of the applicable code I've added to the project, above), when I run the app, I notice that neither of my "built-in" test users can access either the /home/Test or /home/Report page. Moreover, if I set a breakpoint in the Index method, I see that my roles and claims do not exist in the User object. But I can look at the database and see all of the roles and claims are there.
So, to recap, the question asks why the code provided by the ASP.NET Core Web Application template doesn't load roles or role claims into the cookie when a user logs in.
After much Googling and experimenting, there appear to be two modifications that must be made to the templated code in order to get Roles and Role Claims to work:
First, you must add the following line of code in Startup.cs to enable RoleManager. (This bit of magic was mentioned in the OP.)
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>() // <-- Add this line
.AddEntityFrameworkStores<ApplicationDbContext>();
But wait, there's more! According to this discussion on GitHub, getting the roles and claims to show up in the cookie involves either reverting to the service.AddIdentity initialization code, or sticking with service.AddDefaultIdentity and adding this line of code to ConfigureServices:
// Add Role claims to the User object
// See: https://github.com/aspnet/Identity/issues/1813#issuecomment-420066501
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();
If you read the discussion referenced above, you'll see that Roles and Role Claims are apparently kind-of-deprecated, or at least not eagerly supported. Personally, I find it really useful to assign claims to roles, assign roles to users, and then make authorization decisions based on the claims (which are granted to the users based on their roles). This gives me an easy, declarative way to allow, for example, one function to be accessed by multiple roles (i.e. all of the roles that contain the claim used to enable that function).
But you DO want to pay attention to the amount of role and claim data being carried in the auth cookie. More data means more bytes sent to the server with each request, and I have no clue what happens when you bump up against some sort of limit to the cookie size.
Ahh, there are some changes from ASP.NET Core version 2.0 to 2.1. AddDefaultIdentity is the one.
I don't know where to start from your code, so, I will provide an example to create and get user role(s).
Let's create UserRoles first:
public enum UserRoles
{
[Display(Name = "Quản trị viên")]
Administrator = 0,
[Display(Name = "Kiểm soát viên")]
Moderator = 1,
[Display(Name = "Thành viên")]
Member = 2
}
Note: You can remove the attribute Display.
Then, we create RolesExtensions class:
public static class RolesExtensions
{
public static async Task InitializeAsync(RoleManager<IdentityRole> roleManager)
{
foreach (string roleName in Enum.GetNames(typeof(UserRoles)))
{
if (!await roleManager.RoleExistsAsync(roleName))
{
await roleManager.CreateAsync(new IdentityRole(roleName));
}
}
}
}
Next, in the Startup.cs class, we run it:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
RoleManager<IdentityRole> roleManager)
{
// other settings...
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
var task = RolesExtensions.InitializeAsync(roleManager);
task.Wait();
}
Note: Configure requires a returned type void, so we need to create a task to initialize the user roles and we call Wait method.
Do not change the returned type like this:
public async void Configure(...)
{
await RolesExtensions.InitializeAsync(roleManager);
}
Source: Async/Await - Best Practices in Asynchronous Programming
In the ConfigureServices method, these configurations would NOT work (we cannot use User.IsInRole correctly):
services.AddDefaultIdentity<ApplicationUser>()
//.AddRoles<IdentityRole>()
//.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
I don't know why but AddRoles and AddRoleManager don't support to check role for a user (User.IsInRole).
In this case, we need to register service like this:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
By using this way, we create 3 user roles in the databse:
When register new user, we just need to call:
await _userManager.AddToRoleAsync(user, nameof(UserRoles.Administrator));
Finally, we can use [Authorize(Roles = "Administrator")] and:
if (User.IsInRole("Administrator"))
{
// authorized
}
// or
if (User.IsInRole(nameof(UserRoles.Administrator)))
{
// authorized
}
// but
if (User.IsInRole("ADMINISTRATOR"))
{
// authorized
}
P/S: There are a lot things which need to be implement to achieve this goal. So maybe I missed something in this example.
Also you can try to fix Authentication like this
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
If I use “Roles” instead of ClaimTypes.Role in .net6 blazor wasm , #attribute [Authorize(Roles = "admin")] not work and get this error in browser console :
RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (admin)”
By using of ClaimTypes.Role the problem resolved :
private async Task<List<Claim>> GetClaimsAsync(User user)
{
var claims = new List<Claim>()
{
new Claim("UserName", user.Email),
new Claim("FullName", user.FirstName+" "+user.LastName),
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
claims.Add(new Claim(ClaimTypes.Role, role)); // this line
return claims;
}
https://github.com/mammadkoma/Attendance/blob/master/Attendance/Server/Controllers/AccountsController.cs

How to extend IdentityUser as a claim in ASP.NET Core / MVC 6 / EF7?

I am building a site that has Users that belong to an Account. The account is identified by an AccountId which is a foreign key for most data in the DB such as Charges (associated to an Account) or Receipts (associated to an Account).
Rather than hitting the DB every time the repository needs to be polled for data to get the user's AccountId, I wanted to add the AccountId as a claim. The goal being to do something like:
_repository.GetAllChargesByAccountId(User.Identity.GetAccountId());
I'm finding only tidbits and partial solutions for this and I haven't been able to resolve some differences between those examples and my specific environment (ASP.NET Core RC1, MVC 6, EF7).
I have derived a class from IdentityUser for adding attributes about the user:
public class UserIdentity : IdentityUser {
public static object Identity { get; internal set; }
public int AccountId { get; set; }
}
I have created a UserIdentityContext that derives from IdentityDbContext that I'm using for my EF user store.
And I have the following AuthController:
public class AuthController : Controller {
private SignInManager<UserIdentity> _signInManager;
public AuthController(SignInManager<UserIdentity> signInManager) {
_signInManager = signInManager;
}
public IActionResult Login() {
if (User.Identity.IsAuthenticated)
return RedirectToAction("Dashboard", "App");
return View();
}
[HttpPost]
public async Task<ActionResult> Login(LoginViewModel vm, string returnUrl) {
if (ModelState.IsValid) {
var signInResult = await _signInManager.PasswordSignInAsync(vm.Username, vm.Password, true, false);
if (signInResult.Succeeded) {
if (String.IsNullOrWhiteSpace(returnUrl))
return RedirectToAction("Dashboard", "App");
else return RedirectToAction(returnUrl);
} else {
ModelState.AddModelError("", "Username or password is incorrect.");
}
}
return View();
}
public async Task<IActionResult> Logout() {
if (User.Identity.IsAuthenticated) {
await _signInManager.SignOutAsync();
}
return RedirectToAction("Index", "App");
}
}
Looking at other posts, it sounds like I need to add an IdentityExtension in order to access the claim as User.Identity.GetAccountId() and generate a custom user identity as in this answer: How to extend available properties of User.Identity but obviously this is done in an older version and many of the method calls are not applicable anymore.
Thanks in advance for any answers or guidance.
if you have added a claim for AccountId you can then easily write an extension method to get it:
public static string GetAccountId(this ClaimsPrincipal principal)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
var claim = principal.FindFirst("AccountId");
return claim != null ? claim.Value : null;
}
if you need help on how to add a custom claim see this question

How to use Windows Active Directory Authentication and Identity Based Claims?

Problem
We want to use Windows Active Directory to authenticate a user into the application. However, we do not want to use Active Directory groups to manage authorization of controllers/views.
As far as I know, there is not an easy way to marry AD and identity based claims.
Goals
Authenticate users with local Active Directory
Use Identity framework to manage claims
Attempts (Fails)
Windows.Owin.Security.ActiveDirectory - Doh. This is for Azure AD. No LDAP support. Could they have called it AzureActiveDirectory instead?
Windows Authentication - This is okay with NTLM or Keberos authentication. The problems start with: i) tokens and claims are all managed by AD and I can't figure out how to use identity claims with it.
LDAP - But these seems to be forcing me to manually do forms authentication in order to use identity claims? Surely there must be an easier way?
Any help would be more than appreciated. I have been stuck on this problem quite a long time and would appreciate outside input on the matter.
Just hit AD with the username and password instead of authenticating against your DB
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByNameAsync(model.UserName);
if (user != null && AuthenticateAD(model.UserName, model.Password))
{
await SignInAsync(user, model.RememberMe);
return RedirectToLocal(returnUrl);
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
return View(model);
}
public bool AuthenticateAD(string username, string password)
{
using(var context = new PrincipalContext(ContextType.Domain, "MYDOMAIN"))
{
return context.ValidateCredentials(username, password);
}
}
On ASPNET5 (beta6), the idea is to use CookieAuthentication and Identity : you'll need to add in your Startup class :
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization();
services.AddIdentity<MyUser, MyRole>()
.AddUserStore<MyUserStore<MyUser>>()
.AddRoleStore<MyRoleStore<MyRole>>()
.AddUserManager<MyUserManager>()
.AddDefaultTokenProviders();
}
In the configure section, add:
private void ConfigureAuth(IApplicationBuilder app)
{
// Use Microsoft.AspNet.Identity & Cookie authentication
app.UseIdentity();
app.UseCookieAuthentication(options =>
{
options.AutomaticAuthentication = true;
options.LoginPath = new PathString("/App/Login");
});
}
Then, you will need to implement:
Microsoft.AspNet.Identity.IUserStore
Microsoft.AspNet.Identity.IRoleStore
Microsoft.AspNet.Identity.IUserClaimsPrincipalFactory
and extend/override:
Microsoft.AspNet.Identity.UserManager
Microsoft.AspNet.Identity.SignInManager
I actually have setup a sample project to show how this can be done.
GitHub Link.
I tested on the beta8 and and with some small adaptatons (like Context => HttpContext) it worked too.
Shoe your solution above pushed me toward a direction that worked for me on MVC6-Beta3 Identityframework7-Beta3 EntityFramework7-Beta3:
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return View(model);
}
//
// Check for user existance in Identity Framework
//
ApplicationUser applicationUser = await _userManager.FindByNameAsync(model.eID);
if (applicationUser == null)
{
ModelState.AddModelError("", "Invalid username");
return View(model);
}
//
// Authenticate user credentials against Active Directory
//
bool isAuthenticated = await Authentication.ValidateCredentialsAsync(
_applicationSettings.Options.DomainController,
_applicationSettings.Options.DomainControllerSslPort,
model.eID, model.Password);
if (isAuthenticated == false)
{
ModelState.AddModelError("", "Invalid username or password.");
return View(model);
}
//
// Signing the user step 1.
//
IdentityResult identityResult
= await _userManager.CreateAsync(
applicationUser,
cancellationToken: Context.RequestAborted);
if(identityResult != IdentityResult.Success)
{
foreach (IdentityError error in identityResult.Errors)
{
ModelState.AddModelError("", error.Description);
}
return View(model);
}
//
// Signing the user step 2.
//
await _signInManager.SignInAsync(applicationUser,
isPersistent: false,
authenticationMethod:null,
cancellationToken: Context.RequestAborted);
return RedirectToLocal(returnUrl);
}
You could use ClaimTransformation, I just got it working this afternoon using the article and code below. I am accessing an application with Window Authentication and then adding claims based on permissions stored in a SQL Database. This is a good article that should help you.
https://github.com/aspnet/Security/issues/863
In summary ...
services.AddScoped<IClaimsTransformer, ClaimsTransformer>();
app.UseClaimsTransformation(async (context) =>
{
IClaimsTransformer transformer = context.Context.RequestServices.GetRequiredService<IClaimsTransformer>();
return await transformer.TransformAsync(context);
});
public class ClaimsTransformer : IClaimsTransformer
{
private readonly DbContext _context;
public ClaimsTransformer(DbContext dbContext)
{
_context = dbContext;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
System.Security.Principal.WindowsIdentity windowsIdentity = null;
foreach (var i in context.Principal.Identities)
{
//windows token
if (i.GetType() == typeof(System.Security.Principal.WindowsIdentity))
{
windowsIdentity = (System.Security.Principal.WindowsIdentity)i;
}
}
if (windowsIdentity != null)
{
//find user in database by username
var username = windowsIdentity.Name.Remove(0, 6);
var appUser = _context.User.FirstOrDefault(m => m.Username == username);
if (appUser != null)
{
((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim("Id", Convert.ToString(appUser.Id)));
/*//add all claims from security profile
foreach (var p in appUser.Id)
{
((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim(p.Permission, "true"));
}*/
}
}
return await System.Threading.Tasks.Task.FromResult(context.Principal);
}
}
Do you know how to implement a custom System.Web.Security.MembershipProvider? You should be able to use this (override ValidateUser) in conjunction with System.DirectoryServices.AccountManagement.PrincipalContext.ValidateCredentials() to authenticate against active directory.
try:
var pc = new PrincipalContext(ContextType.Domain, "example.com", "DC=example,DC=com");
pc.ValidateCredentials(username, password);
I had to design a solution to this problem this way:
1. Any AD authenticated user will be able to access the application.
2. The roles and claims of the users are stored in the Identity database of the application.
3. An admin user will be able to assign roles to users (I have added this functionality to the app as well).
Read on if you want to see my complete solution. The link to the full source code is towards the end of this answer.
Basic design
1. User enters Active Directory credentials (Windows login credentials in this case).
2. The app checks if it's a valid login against AD.
2.1. If it's not valid, app returns the page with 'Invalid login attempt' error message.
2.2. If it's valid, go to next step.
3. Check if the user exists in the Identity database.
3.1. If Not, create this user in our Identity database.
3.2 If Yes, go to next step.
4. SignIn the user (using AD credentials). This is where we override UserManager.
Note: The user created in step 3.1 has no roles assigned.
An admin user (with valid AD username) is created during Db initialization. Adjust the Admin2UserName with your AD username if you want to be the admin user who will assign roles to newly added users. Don't even worry about the password, it can be anything because the actual authentication will happen through AD not through what's in Identity database.
Solution
Step 1:
Ensure that you've got Identity setup in your application. As an example, I'm taking a Blazor Server app here. If you don't have Identity setup, follow this guide from Microsoft learn.
Use my project to follow along the guide.
Step 2:
Add ADHelper static class to help with Active Directory login. In my example, it's at Areas/Identity/ADHelper.cs and has contents that look like this:
using System.DirectoryServices.AccountManagement;
namespace HMT.Web.Server.Areas.Identity
{
public static class ADHelper
{
public static bool ADLogin(string userName, string password)
{
using PrincipalContext principalContext = new(ContextType.Domain);
bool isValidLogin = principalContext.ValidateCredentials(userName.ToUpper(), password);
return isValidLogin;
}
}
}
Step 3:
Override CheckPasswordAsync method in UserManager so you can authenticate users against Active Directory. I have done this in Areas/Identity/ADUserManager.cs, the contents of which look like this:
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace HMT.Web.Server.Areas.Identity
{
public class ADUserManager<TUser> : UserManager<TUser> where TUser : IdentityUser
{
public ADUserManager(IUserStore<TUser> store, IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<TUser> passwordHasher, IEnumerable<IUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<TUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer,
errors, services, logger)
{
}
public override Task<bool> CheckPasswordAsync(TUser user, string password)
{
var adLoginResult = ADHelper.ADLogin(user.UserName, password);
return Task.FromResult(adLoginResult);
}
}
}
Step 4:
Register it in your Program.cs
builder.Services
.AddDefaultIdentity<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddRoles<ApplicationRole>()
.AddUserManager<CustomUserManager<ApplicationUser>>() <----- THIS GUY
.AddEntityFrameworkStores<ApplicationDbContext>();
ApplicationUser, ApplicationRole and ApplicationDbContext look like this:
public class ApplicationUser : IdentityUser
{
}
public class ApplicationRole : IdentityRole
{
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
}
}
Step 5:
Update OnPostAsync method in Areas/Identity/Pages/Account/Login.cshtml.cs to implement the authentication flow. The method looks like this:
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
if (ModelState.IsValid)
{
// Step 1: Authenticate an user against AD
// If YES: Go to next step
// If NO: Terminate the process
var adLoginResult = ADHelper.ADLogin(Input.UserName, Input.Password);
if (!adLoginResult)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
// Step 2: Check if the user exists in our Identity Db
// If YES: Proceed to SignIn the user
// If NO: Either terminate the process OR create this user in our Identity Db and THEN proceed to SignIn the user
// I'm going with OR scenario this time
var user = await _userManager.FindByNameAsync(Input.UserName);
if (user == null)
{
var identityResult = await _userManager.CreateAsync(new ApplicationUser
{
UserName = Input.UserName,
}, Input.Password);
if (identityResult != IdentityResult.Success)
{
ModelState.AddModelError(string.Empty, "The user was authenticated against AD successfully, but failed to be inserted into Application's Identity database.");
foreach (IdentityError error in identityResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return Page();
}
}
// Step 3: SignIn the user using AD credentials
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
Basically, we're done here. 🙌
Step 6:
Now if an admin user wants to assign roles to newly added users, simply go to Manage Users page and assign appropriate roles.
Pretty easy, right? 😃
Step 7:
If you want to manage roles (add, edit, delete), simply go to manage/roles page.
Conclusion
This setup ensures that users are authenticated using Active Directory and are authorized using roles in the Identity database.
Complete source code
https://github.com/affableashish/blazor-server-auth/tree/feature/AddADAuthentication