My Asp.Net Core mvc web application requires Windows Authentication. In developpement, on IIS Express, everything works fine thanks to this setting
launchSettings.json
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false,
"iisExpress": {
"applicationUrl": "http://localhost:61545/",
"sslPort": 0
}
}
When deploying to IIS, I get a blank page. The Request to my site get a 500 error code.
I tried to add this configuration to Startup.cs, as explained here , without success.
services.Configure<IISOptions>(options => {
options.ForwardWindowsAuthentication = true;
});
When I look into the authentication parameters directly in IIS, Windows Authentication is activated.
I found some post talking about a package called Microsoft.AspNetCore.Server.WebListener
, others about implementing a custom Middleware. I can’t imagine this basic feature needs that much effort to work. Am I missing something ?
This article shows how to setup an ASP.NET Core MVC application to support both users who can login in with a local login account, solution specific, or use a windows authentication login. The identity created from the windows authentication could then be allowed to do different tasks, for example administration, or a user from the local authentication could be used for guest accounts, etc. To do this, IdentityServer4 is used to handle the authentication. The ASP.NET Core MVC application uses the OpenID Connect Hybrid Flow.
Code: https://github.com/damienbod/AspNetCoreWindowsAuth
Posts in this series:
- Supporting both Local and Windows Authentication in ASP.NET Core MVC using IdentityServer4
- ASP.NET Core Authorization for Windows, Local accounts
History
2020-08-23 Updated to .NET Core 3.1, IdentityServer4 V4
2019-09-12 Updated to .NET Core 3.0
Setting up the STS using IdentityServer4
The STS is setup using the IdentityServer4 dotnet templates. Once installed, the is4aspid template was used to create the application from the command line.
The windows authentication is activated in the launchSettings.json. To setup the windows authentication for the deployment, refer to the Microsoft Docs.
{ "iisSettings": { "windowsAuthentication": true, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44364/", "sslPort": 44364 } },
The OpenID Connect Hybrid Flow was then configured for the client application.
new Client { ClientId = "hybridclient", ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.HybridAndClientCredentials, ClientSecrets = { new Secret("hybrid_flow_secret".Sha256()) }, RequirePkce = false, RedirectUris = { "https://localhost:44381/signin-oidc" }, FrontChannelLogoutUri = "https://localhost:44381/signout-oidc", PostLogoutRedirectUris = { "https://localhost:44381/signout-callback-oidc" }, AllowOfflineAccess = true, AllowedScopes = { "openid", "profile", "offline_access", "scope_used_for_hybrid_flow" } },
ASP.NET Core MVC Hybrid Client
The ASP.NET Core MVC application is configured to authenticate using the STS server, and to save the tokens in a cookie. The AddOpenIdConnect method configures the OIDC Hybrid client, which must match the settings in the IdentityServer4 application.
The TokenValidationParameters MUST be used, to set the NameClaimType property, otherwise the User.Identity.Name property will be null. This value is returned in the ‘name’ claim, which is not the default.
services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(options => { options.SignInScheme = "Cookies"; options.Authority = stsServer; options.RequireHttpsMetadata = true; options.ClientId = "hybridclient"; options.ClientSecret = "hybrid_flow_secret"; options.ResponseType = "code id_token"; options.GetClaimsFromUserInfoEndpoint = true; options.Scope.Add("scope_used_for_hybrid_flow"); options.Scope.Add("profile"); options.Scope.Add("offline_access"); options.SaveTokens = true; // Set the correct name claim type options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name" }; });
Then all controllers can be secured using the Authorize attribute. The anti forgery cookie should also be used, because the application uses cookies to store the tokens.
[Authorize] public class HomeController : Controller {
Displaying the login type in the ASP.NET Core Client
Then application then displays the authentication type in the home view. To do this, a requireWindowsProviderPolicy policy is defined, which requires that the identityprovider claim has the value Windows. The policy is added using the AddAuthorization method options.
var requireWindowsProviderPolicy = new AuthorizationPolicyBuilder() .RequireClaim("http://schemas.microsoft.com/identity/claims/identityprovider", "Windows") .Build(); services.AddAuthorization(options => { options.AddPolicy( "RequireWindowsProviderPolicy", requireWindowsProviderPolicy ); });
The policy can then be used in the cshtml view.
@using Microsoft.AspNetCore.Authorization @inject IAuthorizationService AuthorizationService @{ ViewData["Title"] = "Home Page"; } <br /> @if ((await AuthorizationService.AuthorizeAsync(User, "RequireWindowsProviderPolicy")).Succeeded) { <p>Hi Admin, you logged in with an internal Windows account</p> } else { <p>Hi local user</p> }
Both applications can then be started. The client application is redirected to the STS server and the user can login with either the Windows authentication, or a local account.
The text in the client application is displayed depending on the Identity returned.
Identity created for the Windows Authentication:
Local Identity:
Next Steps
The application now works for Windows authentication, or a local account authentication. The authorization now needs to be set, so that the different types have different claims. The identities returned from the Windows Authentication will have different claims, to the identities returned form the local logon, which will be used for guest accounts.
Links:
https://docs.microsoft.com/en-us/aspnet/core/security/authorization/views?view=aspnetcore-2.1&tabs=aspnetcore2x
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-2.1
https://mva.microsoft.com/en-US/training-courses/introduction-to-identityserver-for-aspnet-core-17945
https://stackoverflow.com/questions/34951713/aspnet5-windows-authentication-get-group-name-from-claims/34955119
https://github.com/IdentityServer/IdentityServer4.Templates
https://docs.microsoft.com/en-us/iis/configuration/system.webserver/security/authentication/windowsauthentication/
Table of Contents
- Introduction
- Implement ASP.NET Core Identity
- Application Roles
- Role List View
- Add/Edit Application Role
- Delete Application Role
- Application Users
- User List View
- Add User
- Edit User
- Delete User
- Authentication and Authorization
- Role-Based Authorization
- Login
- Logout
- Downloads
- See Also
- Conclusion
Introduction
The ASP.NET Core Identity is a membership system, which allows us to add authentication and authorization functionality to our Application. A user can create his/her own account with it and access the system, which is based on his/her roles or claims. It
can configure with SQL Server database. It also provides the features to authenticate a user, which is based on his/her external login providers such as Facebook Google, Twitter etc.
The authentication means who is the user. It is the process of verifying the identity of a user by the credentials. The back-end Application accesses after user successful login, so this login process is called authentication. The user holds the credential,
which is the combination of the username and password.
The authorization means what user is allowed to do. It is the mechanism in the Application, which determines what level of access for resources by a particular action authenticates the user. Suppose an Application has a functionality to add and edit a user.
The authorization mechanism determines who is allowed to add the user or who can edit a user.
The source code of this article is available at
MSDN Sample
Implement ASP.NET Core Identity
First, create an empty ASP.NET Core project. As this project doesn’t hold default implementation of ASP.NET Core Identity, so we build an Application step by step with ASP.NET Core Identity. We don’t have the default implementation of ASP.NET Core Identity
due to which project.json file doesn’t have any identity NuGet packages. Now, open the project.json file and modify it to add the dependencies and the tools, as per the code snippet given below.
{
"dependencies"
: {
"Microsoft.NETCore.App"
: {
"version"
:
"1.1.0"
,
"type"
:
"platform"
},
"Microsoft.AspNetCore.Razor.Tools"
: {
"version"
:
"1.0.0-preview2-final"
,
"type"
:
"build"
},
"Microsoft.EntityFrameworkCore.Tools"
: {
"version"
:
"1.0.0-preview2-final"
,
"type"
:
"build"
},
"BundlerMinifier.Core"
:
"2.2.306"
,
"Microsoft.AspNetCore.Mvc"
:
"1.1.0"
,
"Microsoft.AspNetCore.Server.IISIntegration"
:
"1.1.0"
,
"Microsoft.AspNetCore.Server.Kestrel"
:
"1.1.0"
,
"Microsoft.AspNetCore.StaticFiles"
:
"1.1.0"
,
"Microsoft.EntityFrameworkCore.SqlServer"
:
"1.1.0"
,
"Microsoft.EntityFrameworkCore.SqlServer.Design"
:
"1.1.0"
,
"Microsoft.Extensions.Configuration.EnvironmentVariables"
:
"1.1.0"
,
"Microsoft.Extensions.Configuration.Json"
:
"1.1.0"
,
"Microsoft.Extensions.Logging"
:
"1.1.0"
,
"Microsoft.Extensions.Logging.Console"
:
"1.1.0"
,
"Microsoft.Extensions.Logging.Debug"
:
"1.1.0"
,
"Microsoft.Extensions.Options.ConfigurationExtensions"
:
"1.1.0"
,
"Microsoft.VisualStudio.Web.BrowserLink.Loader"
:
"14.1.0"
,
"Microsoft.AspNetCore.Authentication.Cookies"
:
"1.1.0"
,
"Microsoft.AspNetCore.Diagnostics"
:
"1.1.0"
,
"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore"
:
"1.1.0"
,
"Microsoft.AspNetCore.Identity.EntityFrameworkCore"
:
"1.1.0"
,
"Microsoft.AspNetCore.Routing"
:
"1.1.0"
},
"tools"
: {
"Microsoft.AspNetCore.Razor.Tools"
:
"1.0.0-preview2-final"
,
"Microsoft.AspNetCore.Server.IISIntegration.Tools"
:
"1.0.0-preview2-final"
,
"Microsoft.EntityFrameworkCore.Tools"
:
"1.0.0-preview2-final"
},
"frameworks"
: {
"netcoreapp1.1"
: {
"imports"
: [
"dotnet5.6"
,
"portable-net45+win8"
]
}
},
"buildOptions"
: {
"emitEntryPoint"
:
true
,
"preserveCompilationContext"
:
true
},
"runtimeOptions"
: {
"configProperties"
: {
"System.GC.Server"
:
true
}
},
"publishOptions"
: {
"include"
: [
"wwwroot"
,
"**/*.cshtml"
,
"appsettings.json"
,
"web.config"
]
},
"scripts"
: {
"prepublish"
: [
"bower install"
,
"dotnet bundle"
],
"postpublish"
: [
"dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%"
]
}
}
This file includes NuGet packages for both Entity Framework Core and ASP.NET Identity.
Now, create custom ApplicationUser class, which inherits IdentityUser class. This class holds the additional field for the identity user. The IdentityUser class holds user basic information such as Email, UserName, Password etc. The ApplicationUser class
extends this class. It is used to store the user information. The code snippet is given below for the same.
using
Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace
IdentitySampleApplication.Data
{
public
class
ApplicationUser:IdentityUser
{
public
string
Name { get
;
set
; }
}
}
Now, create custom ApplicationRole class, which inherits IdenityRole class. This class holds the additional fields for the identity role.The IdentityRole class holds the role information such as RoleName. The ApplicationRole class extends this class. It
is used to store the role information. The code snippet is given below for the same.
using
Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Threading.Tasks;
namespace
IdentitySampleApplication.Data
{
public
class
ApplicationRole:IdentityRole
{
public
string
Description { get
;
set
; }
public
DateTime CreatedDate {
get
;
set
; }
public
string
IPAddress { get
;
set
; }
}
}
To perform the database operations, we create an IdentityDbContext class named ApplicationDbContext, as per the code snippet given below.
using
Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using
Microsoft.EntityFrameworkCore;
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Threading.Tasks;
namespace
IdentitySampleApplication.Data
{
public
class
ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole,
string
>
{
public
ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) :
base
(options)
{
}
}
}
This class needs to know that which type Application user and role are dealing with the Application. We passed ApplicationUser and ApplicationRole as a parameter, while creating the object of ApplicationDbContext class. Here, the third parameter represents
the primary key data type for both IdentityUser and IdentityRole.
The Application needs to configure to implement ASP.NET Core Identity. Now, add the identity in the method Configure of the Startup class. The code snippet is given below to add the identity in the Application.
As the concept of Dependency Injection is central to ASP.NET Core Application, we register context and identity to Dependency Injection during the Application start up. Thus, we register these as a Service in the ConfigureServices method of the StartUp class,
as per the code snippet given below.
public
void
ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString(
"DefaultConnection"
)));
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
Here, the DefaultConnection is the connection string, which is defined in appsettings.json file, as per the code snippet given below.
{
"ConnectionStrings"
: {
"DefaultConnection"
:
"Data Source=DESKTOP-RG33QHE;Initial Catalog=IdentitySampleDb;User ID=sa; Password=****"
},
"Logging"
: {
"IncludeScopes"
:
false
,
"LogLevel"
: {
"Default"
:
"Debug"
,
"System"
:
"Information"
,
"Microsoft"
:
"Information"
}
}
}
Now, we have configured settings to create the database, so we have time to create a database, using migration. For database migration , we need to follow the following steps.
- Tools -> NuGet Package Manager -> Package Manager Console.
- Run PM> Add-Migration MyFirstMigration to scaffold a migration to create the initial set of tables for our model. If we receive an error , which states the term `add-migration’ is not recognized as the name of a cmdlet, then close and reopen Visual Studio.
- Run PM> Update-Database to apply the new migration to the database. Since our database doesn’t exist yet, it will be created for us before the migration is applied.
Application Roles
This section demonstrates that how to create, edit and delete the identity roles. There is a RoleManager, class which exposes the role related APIs. This creates a role in the Application and store in the database.
Now, we proceed to the controller. We create a controller named ApplicationRoleController under the Controllers folder of the Application. It has all ActionResult methods for the end user interface of the operations. We create RoleManager instance. Subsequently,
we inject this in the controller’s constructor to get its object. The following is a partial code snippet for the ApplicationRoleController in which RoleManager is injected, using constructor Dependency Injection.
using
IdentitySampleApplication.Data;
using
IdentitySampleApplication.Models;
using
Microsoft.AspNetCore.Http;
using
Microsoft.AspNetCore.Identity;
using
Microsoft.AspNetCore.Mvc;
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Threading.Tasks;
namespace
IdentitySampleApplication.Controllers
{
public
class
ApplicationRoleController : Controller
{
private
readonly
RoleManager<ApplicationRole> roleManager;
public
ApplicationRoleController(RoleManager<ApplicationRole> roleManager)
{
this
.roleManager = roleManager;
}
}
}
We can notice that Controller takes RoleManager as a constructor parameter. ASP.NET Core Dependency Injection will take care of passing an instance of RoleManager into ApplicationRoleController. The controller is developed to handle the operations requests
for the Application identity role. Now, let’s develop the user interface for the Role Listing, Add Role, Edit Role and Delete Role. Let’s see each one by one.
Role List View
This is the first view when the Application is accessed or the entry point of the Application is executed. It shows the role listing, as shown in Figure 1. The role data is displayed in a tabular format and on this view, it has linked to adding a new role,
edit a role and delete a role.
To pass the data from controller to view, create named ApplicationRoleListViewModel view model, as per the code snippet, mentioned below. This view model is used for role listing.
namespace
IdentitySampleApplication.Models
{
public
class
ApplicationRoleListViewModel
{
public
string
Id { get
;
set
; }
public
string
RoleName { get
;
set
; }
public
string
Description { get
;
set
; }
public
int
NumberOfUsers { get
;
set
; }
}
}
Now, we create action method, which returns an index view with the data. The code snippet of an Index action method in ApplicationRoleController is mentioned below.
[HttpGet]
public
IActionResult Index()
{
List<ApplicationRoleListViewModel> model =
new
List<ApplicationRoleListViewModel>();
model = roleManager.Roles.Select(r =>
new
ApplicationRoleListViewModel
{
RoleName = r.Name,
Id = r.Id,
Description = r.Description,
NumberOfUsers = r.Users.Count
}).ToList();
return
View(model);
}
Now, we create an index view, as per the code snippet, mentioned below under the ApplicationRole folder of views.
@model IEnumerable<
ApplicationRoleListViewModel
>
@using IdentitySampleApplication.Models
@using IdentitySampleApplication.Code
<
div
class
=
"top-buffer"
></
div
>
<
div
class
=
"panel panel-primary"
>
<
div
class
=
"panel-heading panel-head"
>Application Roles</
div
>
<
div
class
=
"panel-body"
>
<
div
class
=
"btn-group"
>
<
a
id
=
"createRoleModal"
data-toggle
=
"modal"
asp-action
=
"AddEditApplicationRole"
data-target
=
"#modal-action-application-role"
class
=
"btn btn-primary"
>
<
i
class
=
"glyphicon glyphicon-plus"
></
i
>
Add Role
</
a
>
</
div
>
<
div
class
=
"top-buffer"
></
div
>
<
table
class
=
"table table-bordered table-striped table-condensed"
>
<
thead
>
<
tr
>
<
th
>Name</
th
>
<
th
>Description</
th
>
<
th
>Users</
th
>
<
th
>Action</
th
>
</
tr
>
</
thead
>
<
tbody
>
@foreach (var item in Model)
{
<
tr
>
<
td
>@item.RoleName</
td
>
<
td
>@item.Description</
td
>
<
td
>@item.NumberOfUsers</
td
>
<
td
>
<
a
id
=
"addEditApplicationRoleModal"
data-toggle
=
"modal"
asp-action
=
"AddEditApplicationRole"
asp-route-id
=
"@item.Id"
data-target
=
"#modal-action-application-role"
class
=
"btn btn-info"
>
<
i
class
=
"glyphicon glyphicon-pencil"
></
i
>
Edit
</
a
>
@if (item.NumberOfUsers == 0)
{
<
a
id
=
"deleteApplicationRoleModal"
data-toggle
=
"modal"
asp-action
=
"DeleteApplicationRole"
asp-route-id
=
"@item.Id"
data-target
=
"#modal-action-application-role"
class
=
"btn btn-danger"
>
<
i
class
=
"glyphicon glyphicon-trash"
></
i
>
Delete
</
a
>
}
</
td
>
</
tr
>
}
</
tbody
>
</
table
>
</
div
>
</
div
>
@Html.Partial("_Modal", new BootstrapModel { ID = "modal-action-application-role", AreaLabeledId = "modal-action-application-role-label", Size = ModalSize.Medium
})
@section scripts
{
<
script
src
=
"~/js/application-role-index.js"
asp-append-version
=
"true"
></
script
>
}
It shows how to create and delete forms in the Bootstrap model popup, so create the Application-role — index.js file, as per the code snippet given below.
(
function
($) {
function
ApplicationRole() {
var
$
this
=
this
;
function
initilizeModel() {
$(
"#modal-action-application-role"
).on(
'loaded.bs.modal'
,
function
(e) {
}).on(
'hidden.bs.modal'
,
function
(e) {
$(
this
).removeData(
'bs.modal'
);
});
}
$
this
.init =
function
() {
initilizeModel();
}
}
$(
function
() {
var
self =
new
ApplicationRole();
self.init();
})
}(jQuery))
When the Application runs and calls the index() action method from ApplicationRoleController with a HttpGet request, it gets all the roles listed in the UI, as shown in Figure 1.
Figure 1: Application Role Listing
Add/Edit Application Role
To pass the data from UI to a controller to add and edit an Application role, use view model named ApplicationRoleViewModel. The code snippet is given below for the same.
using
System.ComponentModel.DataAnnotations;
namespace
IdentitySampleApplication.Models
{
public
class
ApplicationRoleViewModel
{
public
string
Id { get
;
set
; }
[Display(Name =
"Role Name"
)]
public
string
RoleName { get
;
set
; }
public
string
Description { get
;
set
; }
}
}
The ApplicationRoleController has an action method named AddEditApplicationRole, which returns the view to add and edit an Application role. As we add and edit Application, the role is using same UI screen. The code snippet mentioned below is for same action
method for both GET and Post requests.
[HttpGet]
public
async Task<IActionResult> AddEditApplicationRole(
string
id)
{
ApplicationRoleViewModel model =
new
ApplicationRoleViewModel();
if
(!String.IsNullOrEmpty(id))
{
ApplicationRole applicationRole = await roleManager.FindByIdAsync(id);
if
(applicationRole !=
null
)
{
model.Id = applicationRole.Id;
model.RoleName = applicationRole.Name;
model.Description = applicationRole.Description;
}
}
return
PartialView(
"_AddEditApplicationRole"
, model);
}
[HttpPost]
public
async Task<IActionResult> AddEditApplicationRole(
string
id, ApplicationRoleViewModel model)
{
if
(ModelState.IsValid)
{
bool
isExist = !String.IsNullOrEmpty(id);
ApplicationRole applicationRole = isExist ? await roleManager.FindByIdAsync(id) :
new
ApplicationRole
{
CreatedDate = DateTime.UtcNow
};
applicationRole.Name = model.RoleName;
applicationRole.Description = model.Description;
applicationRole.IPAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString();
IdentityResult roleRuslt = isExist? await roleManager.UpdateAsync(applicationRole)
: await roleManager.CreateAsync(applicationRole);
if
(roleRuslt.Succeeded)
{
return
RedirectToAction(
"Index"
);
}
}
return
View(model);
}
There are three asynchronous methods, which are used of the RoleManager class, which performs actions, as shown below.
- FindByIdAsync: This method has role Id as a parameter and returns already existing role, which is based on the input.
- CreateAsync: This method has new ApplicationRole as a parameter and creates new role in the Application.
- UpdateAsync: This method has an existing ApplicationRole as a parameter and updates that role in the Application
The GET request for the AddEditApplicationRole action method returns _AddEditApplicationRole partial view; the code snippet follows under the ApplicationRole folder of views.
@model ApplicationRoleViewModel
@using IdentitySampleApplication.Models
<
form
asp-action
=
"AddEditApplicationRole"
role
=
"form"
>
@await Html.PartialAsync("_ModalHeader", new ModalHeader { Heading =$"{(String.IsNullOrEmpty(Model.Id)? "Add" : "Edit")} Application
Role" })
<
div
class
=
"modal-body form-horizontal"
>
<
div
class
=
"row"
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"RoleName"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
input
asp-for
=
"RoleName"
class
=
"form-control"
/>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"Description"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
input
asp-for
=
"Description"
class
=
"form-control"
/>
</
div
>
</
div
>
</
div
>
</
div
>
@await Html.PartialAsync("_ModalFooter", new ModalFooter { })
</
form
>
When the Application runs and you click on the Add Role button or Edit button in the role listing, it makes a GET request for the AddEditApplicationRole() action; add/edit an Application role screen opens, as shown in Figure 2.
Figure 2: Add Application Role
Delete Application Role
The ApplicationRoleController has an action method named DeleteApplicationRole, which returns the view to delete an Application role named _DeleteApplicationRole. The code snippet mentioned below is for the same action method for both GET and Post requests.
[HttpGet]
public
async Task<IActionResult> DeleteApplicationRole(
string
id)
{
string
name =
string
.Empty;
if
(!String.IsNullOrEmpty(id))
{
ApplicationRole applicationRole = await roleManager.FindByIdAsync(id);
if
(applicationRole !=
null
)
{
name = applicationRole.Name;
}
}
return
PartialView(
"_DeleteApplicationRole"
, name);
}
[HttpPost]
public
async Task<IActionResult> DeleteApplicationRole(
string
id, FormCollection form)
{
if
(!String.IsNullOrEmpty(id))
{
ApplicationRole applicationRole = await roleManager.FindByIdAsync(id);
if
(applicationRole !=
null
)
{
IdentityResult roleRuslt = roleManager.DeleteAsync(applicationRole).Result;
if
(roleRuslt.Succeeded)
{
return
RedirectToAction(
"Index"
);
}
}
}
return
View();
}
Here DeleteAsync method of RoleManager is used, which has an existing Application role as an input parameter. It deletes an existing Application role.
The GET request for the DeleteApplicationRoler action method returns _DeleteUser partial View. The code snippet mentioned below is under the ApplicationRole folder of Views.
@model string
@using IdentitySampleApplication.Models
<
form
asp-action
=
"DeleteApplicationRole"
role
=
"form"
>
@Html.Partial("_ModalHeader", new ModalHeader { Heading = "Delete Application Role" })
<
div
class
=
"modal-body form-horizontal"
>
Are you want to delete @Model?
</
div
>
@Html.Partial("_ModalFooter", new ModalFooter { SubmitButtonText = "Delete" })
</
form
>
When the Application runs and a user clicks on the «Delete» button in the Application role listing, it makes a GET request for the DeleteUser() action, then the delete Application role screen is shown below.
Figure 3: Delete Application Role
Application Users
This section demonstrates that how to create, edit and delete the identity users and how to assign the role to a user. There is a RoleManager class, which exposes the role related APIs and another is UserManager, which exposes the user related APIs. This
creates the user in the Application and is stored in the database.
Now, we proceed to the controller. We create controller named UserController under the Controllers folder of the Application. It has all ActionResult methods for the end user interface of operations. We create both UserManager and RoleManager instances.
Subsequently, we inject these in the controller’s constructor to get its object. The following is a partial code snippet for the UserController in which UserManager and RoleManager are injected, using constructor Dependency Injection.
using
IdentitySampleApplication.Data;
using
IdentitySampleApplication.Models;
using
Microsoft.AspNetCore.Http;
using
Microsoft.AspNetCore.Identity;
using
Microsoft.AspNetCore.Mvc;
using
Microsoft.AspNetCore.Mvc.Rendering;
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Threading.Tasks;
namespace
IdentitySampleApplication.Controllers
{
public
class
UserController : Controller
{
private
readonly
UserManager<ApplicationUser> userManager;
private
readonly
RoleManager<ApplicationRole> roleManager;
public
UserController(UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager)
{
this
.userManager = userManager;
this
.roleManager = roleManager;
}
}
}
We can notice that Controller takes UserManager and RoleManager as constructor parameters. ASP.NET Core Dependency Injection will take care of passing the instances of both UserManager and RoleManager into UserController. The controller is developed to handle
the operations requests for the Application identity user. Now, let’s develop the user interface for the user listing, add user, edit user and delete user. Let’s see each one by one.
User List View
When we click on top User menu of the Application, it shows the role listing, as shown in Figure 4. The user data is displayed in a tabular format and on this view, it has linked to adding a new user, edit a user and delete a user.
To pass the data from the controller to view, create named UserListViewModel view model, as per the code snippet, mentioned below. This view model is used for the user listing.
namespace
IdentitySampleApplication.Models
{
public
class
UserListViewModel
{
public
string
Id { get
;
set
; }
public
string
Name { get
;
set
; }
public
string
Email { get
;
set
; }
public
string
RoleName { get
;
set
; }
}
}
Now, we create an action method, which returns an index view with the data. The code snippet of an Index action method in UserController is mentioned below.
[HttpGet]
public
IActionResult Index()
{
List<UserListViewModel> model =
new
List<UserListViewModel>();
model = userManager.Users.Select(u =>
new
UserListViewModel
{
Id = u.Id,
Name = u.Name,
Email = u.Email
}).ToList();
return
View(model);
}
Now, we create an index view, as per the code snippet, mentioned below under the User folder of the views.
@model IEnumerable<
UserListViewModel
>
@using IdentitySampleApplication.Models
@using IdentitySampleApplication.Code
<
div
class
=
"top-buffer"
></
div
>
<
div
class
=
"panel panel-primary"
>
<
div
class
=
"panel-heading panel-head"
>Users</
div
>
<
div
class
=
"panel-body"
>
<
div
class
=
"btn-group"
>
<
a
id
=
"createEditUserModal"
data-toggle
=
"modal"
asp-action
=
"AddUser"
data-target
=
"#modal-action-user"
class
=
"btn btn-primary"
>
<
i
class
=
"glyphicon glyphicon-plus"
></
i
>
Add User
</
a
>
</
div
>
<
div
class
=
"top-buffer"
></
div
>
<
table
class
=
"table table-bordered table-striped table-condensed"
>
<
thead
>
<
tr
>
<
th
>Name</
th
>
<
th
>Email</
th
>
<
th
>Action</
th
>
</
tr
>
</
thead
>
<
tbody
>
@foreach (var item in Model)
{
<
tr
>
<
td
>@item.Name</
td
>
<
td
>@item.Email</
td
>
<
td
>
<
a
id
=
"editUserModal"
data-toggle
=
"modal"
asp-action
=
"EditUser"
asp-route-id
=
"@item.Id"
data-target
=
"#modal-action-user"
class
=
"btn btn-info"
>
<
i
class
=
"glyphicon glyphicon-pencil"
></
i
>
Edit
</
a
>
<
a
id
=
"deleteUserModal"
data-toggle
=
"modal"
asp-action
=
"DeleteUser"
asp-route-id
=
"@item.Id"
data-target
=
"#modal-action-user"
class
=
"btn btn-danger"
>
<
i
class
=
"glyphicon glyphicon-trash"
></
i
>
Delete
</
a
>
</
td
>
</
tr
>
}
</
tbody
>
</
table
>
</
div
>
</
div
>
@Html.Partial("_Modal", new BootstrapModel { ID = "modal-action-user", AreaLabeledId = "modal-action-user-label", Size = ModalSize.Medium })
@section scripts
{
<
script
src
=
"~/js/user-index.js"
asp-append-version
=
"true"
></
script
>
}
It shows all the operational forms in the Bootstrap model popup, so create the user — index.js file, as per the code snippet given below.
(
function
($) {
function
User() {
var
$
this
=
this
;
function
initilizeModel() {
$(
"#modal-action-user"
).on(
'loaded.bs.modal'
,
function
(e) {
}).on(
'hidden.bs.modal'
,
function
(e) {
$(
this
).removeData(
'bs.modal'
);
});
}
$
this
.init =
function
() {
initilizeModel();
}
}
$(
function
() {
var
self =
new
User();
self.init();
})
}(jQuery))
When the Application runs and calls the index() action method from UserController with a HttpGet request, it gets all the users, which are listed in the UI, as shown in Figure 4.
Figure 4: User Listing
Add User
To pass the data from UI to a controller to add a user, it uses view model named UserViewModel. The code snippet is given below for the same.
using
Microsoft.AspNetCore.Mvc.Rendering;
using
System.Collections.Generic;
using
System.ComponentModel.DataAnnotations;
namespace
IdentitySampleApplication.Models
{
public
class
UserViewModel
{
public
string
Id { get
;
set
; }
public
string
UserName { get
;
set
; }
[DataType(DataType.Password)]
public
string
Password { get
;
set
; }
[Display(Name=
"Confirm Password"
)]
[DataType(DataType.Password)]
public
string
ConfirmPassword { get
;
set
; }
public
string
Name { get
;
set
; }
public
string
Email { get
;
set
; }
public
List<SelectListItem> ApplicationRoles {
get
;
set
; }
[Display(Name =
"Role"
)]
public
string
ApplicationRoleId { get
;
set
; }
}
}
The UserController has an action method, which is named as AddUser, which returns the view to add a user. The code snippet mentioned below is for same action method for both GET and Post requests.
[HttpGet]
public
IActionResult AddUser()
{
UserViewModel model =
new
UserViewModel();
model.ApplicationRoles = roleManager.Roles.Select(r =>
new
SelectListItem
{
Text = r.Name,
Value = r.Id
}).ToList();
return
PartialView(
"_AddUser"
, model);
}
[HttpPost]
public
async Task<IActionResult> AddUser(UserViewModel model)
{
if
(ModelState.IsValid)
{
ApplicationUser user =
new
ApplicationUser
{
Name = model.Name,
UserName = model.UserName,
Email = model.Email
};
IdentityResult result = await userManager.CreateAsync(user, model.Password);
if
(result.Succeeded)
{
ApplicationRole applicationRole = await roleManager.FindByIdAsync(model.ApplicationRoleId);
if
(applicationRole !=
null
)
{
IdentityResult roleResult = await userManager.AddToRoleAsync(user, applicationRole.Name);
if
(roleResult.Succeeded)
{
return
RedirectToAction(
"Index"
);
}
}
}
}
return
View(model);
}
There are two asynchronous methods, which are used of the UserManager class, which performs an action, as shown below.
- CreateAsync: This method has new ApplicationUser as a parameter and creates new user in the Application.
- AddToRoleAsync: This method has two parameters, where one is an existing Application user and another is the role name. It assigns a role to the Application user.
The GET request for the AddUser action method returns _AddUser partial view; the code snippet follows under the User folder of views.
@model UserViewModel
@using IdentitySampleApplication.Models
<
form
asp-action
=
"AddUser"
role
=
"form"
>
@await Html.PartialAsync("_ModalHeader", new ModalHeader { Heading = "Add User" })
<
div
class
=
"modal-body form-horizontal"
>
<
div
class
=
"row"
>
<
div
class
=
"col-lg-6"
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"Name"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
input
asp-for
=
"Name"
class
=
"form-control"
/>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"Email"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
input
asp-for
=
"Email"
class
=
"form-control"
/>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"ApplicationRoleId"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
select
asp-for
=
"ApplicationRoleId"
asp-items
=
"@Model.ApplicationRoles"
class
=
"form-control"
>
<
option
>Please
select</option
>
</
select
>
</
div
>
</
div
>
</
div
>
<
div
class
=
"col-lg-6"
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"UserName"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
input
asp-for
=
"UserName"
class
=
"form-control"
/>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"Password"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
input
asp-for
=
"Password"
class
=
"form-control"
/>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"ConfirmPassword"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
input
asp-for
=
"ConfirmPassword"
class
=
"form-control"
/>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
@await Html.PartialAsync("_ModalFooter", new ModalFooter { })
</
form
>
When the Application runs and we click on the Add User button, it makes a GET request for the AddUser() action; add a user screen, as shown in Figure 5.
Figure 5: Add User
Edit User
To pass the data from UI to the controller to edit a user, use view model named EditUserViewModel. The following code snippet for EditUserViewModel.
using
Microsoft.AspNetCore.Mvc.Rendering;
using
System.Collections.Generic;
using
System.ComponentModel.DataAnnotations;
namespace
IdentitySampleApplication.Models
{
public
class
EditUserViewModel
{
public
string
Id { get
;
set
; }
public
string
Name { get
;
set
; }
public
string
Email { get
;
set
; }
public
List<SelectListItem> ApplicationRoles {
get
;
set
; }
[Display(Name =
"Role"
)]
public
string
ApplicationRoleId { get
;
set
; }
}
}
The UserController has an action method named EditUser, which returns the view to edit a user. The code snippet mentioned below is for the same action method for both GET and Post requests.
[HttpGet]
public
async Task<IActionResult> EditUser(
string
id)
{
EditUserViewModel model =
new
EditUserViewModel();
model.ApplicationRoles = roleManager.Roles.Select(r =>
new
SelectListItem
{
Text = r.Name,
Value = r.Id
}).ToList();
if
(!String.IsNullOrEmpty(id))
{
ApplicationUser user = await userManager.FindByIdAsync(id);
if
(user !=
null
)
{
model.Name = user.Name;
model.Email = user.Email;
model.ApplicationRoleId = roleManager.Roles.Single(r => r.Name == userManager.GetRolesAsync(user).Result.Single()).Id;
}
}
return
PartialView(
"_EditUser"
, model);
}
[HttpPost]
public
async Task<IActionResult> EditUser(
string
id, EditUserViewModel model)
{
if
(ModelState.IsValid)
{
ApplicationUser user = await userManager.FindByIdAsync(id);
if
(user !=
null
)
{
user.Name = model.Name;
user.Email = model.Email;
string
existingRole = userManager.GetRolesAsync(user).Result.Single();
string
existingRoleId = roleManager.Roles.Single(r => r.Name == existingRole).Id;
IdentityResult result = await userManager.UpdateAsync(user);
if
(result.Succeeded)
{
if
(existingRoleId != model.ApplicationRoleId)
{
IdentityResult roleResult = await userManager.RemoveFromRoleAsync(user, existingRole);
if
(roleResult.Succeeded)
{
ApplicationRole applicationRole = await roleManager.FindByIdAsync(model.ApplicationRoleId);
if
(applicationRole !=
null
)
{
IdentityResult newRoleResult = await userManager.AddToRoleAsync(user, applicationRole.Name);
if
(newRoleResult.Succeeded)
{
return
RedirectToAction(
"Index"
);
}
}
}
}
}
}
}
return
PartialView(
"_EditUser"
, model);
}
There are four asynchronous methods, which are used of the UserManager class, which performs an action, as shown below.
- FindByIdAsync: This method has an Application user Id as a parameter and returns already existing user, which is based on the input.
- GetRolesAsync: This method takes an existing Application user as a parameter and returns the existing roles, which assigned to that particular user.
- UpdateAsync: This method has an existing ApplicationUser as a parameter and updates that user in the Application.
- RemoveFromRoleAsync: This method has two parameters, where one is an existing Application user and another is assigned the role name. It removes the assigned role from that Application user.
The GET request for the EditUser action method returns _EditUser partial view; the code snippet follows under the User folder of views.
@model EditUserViewModel
@using IdentitySampleApplication.Models
<
form
asp-action
=
"EditUser"
role
=
"form"
>
@await Html.PartialAsync("_ModalHeader", new ModalHeader { Heading = "Edit User" })
<
div
class
=
"modal-body form-horizontal"
>
<
div
class
=
"row"
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"Name"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
input
asp-for
=
"Name"
class
=
"form-control"
/>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"Email"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
input
asp-for
=
"Email"
class
=
"form-control"
/>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"ApplicationRoleId"
class
=
"col-lg-3 col-sm-3 control-label"
></
label
>
<
div
class
=
"col-lg-6"
>
<
select
asp-for
=
"ApplicationRoleId"
asp-items
=
"@Model.ApplicationRoles"
class
=
"form-control"
>
<
option
>Please select</
option
>
</
select
>
</
div
>
</
div
>
</
div
>
</
div
>
@await Html.PartialAsync("_ModalFooter", new ModalFooter { })
</
form
>
When the Application runs and you click on the Edit button in the User listing, it makes a GET request for the EditUser() action, followed by editing the user screen, which is shown in Figure 6.
Figure 6: Edit User
Delete User
The UserController has an action method named DeleteUser, which returns the view to delete a user. The code snippet mentioned below is for the same action method for both GET and Post requests.
[HttpGet]
public
async Task<IActionResult> DeleteUser(
string
id)
{
string
name =
string
.Empty;
if
(!String.IsNullOrEmpty(id))
{
ApplicationUser applicationUser = await userManager.FindByIdAsync(id);
if
(applicationUser !=
null
)
{
name = applicationUser.Name;
}
}
return
PartialView(
"_DeleteUser"
, name);
}
[HttpPost]
public
async Task<IActionResult> DeleteUser(
string
id, FormCollection form)
{
if
(!String.IsNullOrEmpty(id))
{
ApplicationUser applicationUser = await userManager.FindByIdAsync(id);
if
(applicationUser !=
null
)
{
IdentityResult result = await userManager.DeleteAsync(applicationUser);
if
(result.Succeeded)
{
return
RedirectToAction(
"Index"
);
}
}
}
return
View();
}
Here, DeleteAsync method of UserManager is used, which takes an existing Application user as an input parameter. It deletes an existing Application user.
The GET request for the DeleteUser action method returns _DeleteUser partial View. The code snippet mentioned below is under the User folder of Views.
@model string
@using IdentitySampleApplication.Models
<
form
asp-action
=
"DeleteUser"
role
=
"form"
>
@Html.Partial("_ModalHeader", new ModalHeader { Heading = "Delete User" })
<
div
class
=
"modal-body form-horizontal"
>
Are you want to delete @Model?
</
div
>
@Html.Partial("_ModalFooter", new ModalFooter { SubmitButtonText = "Delete" })
</
form
>
When the Application runs and a user clicks on the «Delete» button in the user listing, it makes a GET request for the DeleteUser() action, then the delete user screen is shown below.
Figure 7: Delete User
Authentication and Authorization
This section demonstrates the login and logout functionality of the Application. As the Application users are already existing in the system with a role, to implement login and logout functionality, create AccountController under the Controllers folder.
This controller holds both login and logout action methods. We create SignInManager instance. Now, we inject it in the controller’s constructor to get its object. The following is a partial code snippet for the AccountController in which SignInManager is injected,
using constructor Dependency Injection.
using
IdentitySampleApplication.Data;
using
IdentitySampleApplication.Models;
using
Microsoft.AspNetCore.Authorization;
using
Microsoft.AspNetCore.Identity;
using
Microsoft.AspNetCore.Mvc;
using
System.Threading.Tasks;
namespace
IdentitySampleApplication.Controllers
{
public
class
AccountController : Controller
{
private
readonly
SignInManager<ApplicationUser> signInManager;
public
AccountController(SignInManager<ApplicationUser> signInManager)
{
this
.signInManager = signInManager;
}
}
}
Role-Based Authorization
The Application has a HomeController, which is accessed after successful authentication. It holds an action method named Index, which returns a view by authenticating the username.
using
IdentitySampleApplication.Data;
using
Microsoft.AspNetCore.Authorization;
using
Microsoft.AspNetCore.Identity;
using
Microsoft.AspNetCore.Mvc;
namespace
IdentitySampleApplication.Controllers
{
[Authorize]
public
class
HomeController : Controller
{
private
readonly
UserManager<ApplicationUser> userManager;
public
HomeController(UserManager<ApplicationUser> userManager)
{
this
.userManager = userManager;
}
[Authorize(Roles =
"User"
)]
public
IActionResult Index()
{
string
userName = userManager.GetUserName(User);
return
View(
"Index"
,userName);
}
}
}
Here, Authorize attribute is used on the controller level, which means that this controller is accessed by only authenticate users. The action method has also used Authorize attribute with roles, which represents that what role can access this action method.
The code snippet given above represents that if an authenticate user has the “User” role then he is authorized to access this action. This is role based authorization.
The GetUserName method of UserManager returns the authenticate user’s username, which is based on the user.
The authorized GET request for the Index action method returns an Index view. The code snippet follows under the Home folder of views.
@model string
<
h1
> Welcome @Model</
h1
>
The Application has a partial view, which is used to show following details on the top header. If the user is not authenticated, it shows Log In button on top. If the user is authenticated, it shows the username and Log Off button.
@using Microsoft.AspNetCore.Identity
@using IdentitySampleApplication.Models
@using IdentitySampleApplication.Data
@inject SignInManager<
ApplicationUser
> SignInManager
@inject UserManager<
ApplicationUser
> UserManager
@if (SignInManager.IsSignedIn(User))
{
<
form
asp-area
=
""
asp-controller
=
"Account"
asp-action
=
"SignOff"
method
=
"post"
id
=
"logoutForm"
class
=
"navbar-right"
>
<
ul
class
=
"nav navbar-nav navbar-right"
>
<
li
>
<
a
asp-area
=
""
asp-controller
=
"Manage"
asp-action
=
"Index"
title
=
"Manage"
>Hello @UserManager.GetUserName(User)!</
a
>
</
li
>
<
li
>
<
button
type
=
"submit"
class
=
"btn btn-link navbar-btn navbar-link"
>Log off</
button
>
</
li
>
</
ul
>
</
form
>
}
else
{
<
ul
class
=
"nav navbar-nav navbar-right"
>
<
li
><
a
asp-area
=
""
asp-controller
=
"Account"
asp-action
=
"Login"
>Log in</
a
></
li
>
</
ul
>
}
As per the code snippet given above, the view has Injected Dependency on its level and created instances of both SignInManager and UserManager. The IsSignedIn method of SignInManager class checks whether a user login or not in the application.
Login
To pass the data from UI to a controller to login an Application user, it uses view model named LoginViewModel. The code snippet is given below for the same.
using
System.ComponentModel.DataAnnotations;
namespace
IdentitySampleApplication.Models
{
public
class
LoginViewModel
{
[Required]
public
string
UserName { get
;
set
; }
[Required]
[DataType(DataType.Password)]
public
string
Password { get
;
set
; }
[Display(Name =
"Remember me?"
)]
public
bool
RememberMe { get
;
set
; }
}
}
The AccountController has two action methods, where one is for GET request named Login, which returns the view to login and another has same name. It also handles POST request to login an Application user. The code snippet mentioned below is for same action
method for both GET and Post requests.
[HttpGet]
public
IActionResult Login(
string
returnUrl =
null
)
{
ViewData[
"ReturnUrl"
] = returnUrl;
return
View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public
async Task<IActionResult> Login(LoginViewModel model,
string
returnUrl = null
)
{
ViewData[
"ReturnUrl"
] = returnUrl;
if
(ModelState.IsValid)
{
var result = await signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure:
false
);
if
(result.Succeeded)
{
return
RedirectToLocal(returnUrl);
}
else
{
ModelState.AddModelError(
string
.Empty,
"Invalid login attempt."
);
return
View(model);
}
}
return
View(model);
}
private
IActionResult RedirectToLocal(
string
returnUrl)
{
if
(Url.IsLocalUrl(returnUrl))
{
return
Redirect(returnUrl);
}
else
{
return
RedirectToAction(nameof(HomeController.Index),
"Home"
);
}
}
Here the Login action method has a parameter named returnUrl, which represents that a user is redirected on a page after login. Suppose an end user is not authenticated and he tries to access the internal page via URL, then this internal page URL is stored
in this parameter and the user is redirected on the login screen. Afterwards, the user authenticates from the login screen, followed by redirecting on that URL page rather than a regular redirection.
The SignInManager class exposes API methods, which is used to manage sign in operations. There is a asynchronous method, which is PasswordSignInAsync. This method takes the username and password of a user as inputs and checks its validity and issues the
Application cookie, if they are correct.
The GET request for the Login action method returns Login view and the code snippet follows under the Account folder of views.
@model LoginViewModel
@using IdentitySampleApplication.Models
<
div
class
=
"row"
>
<
div
class
=
"col-md-3"
></
div
>
<
div
class
=
"col-md-6"
>
<
div
class
=
"top-buffer"
></
div
>
<
div
class
=
"panel panel-primary"
>
<
div
class
=
"panel-heading"
>Login</
div
>
<
div
class
=
"panel-body"
>
<
section
>
<
form
asp-controller
=
"Account"
asp-action
=
"Login"
asp-route-returnurl
=
"@ViewData["
ReturnUrl"]"
method
=
"post"
class
=
"form-horizontal"
>
<
h4
>Use a local account
to log in.</h4
>
<
hr
/>
<
div
asp-validation-summary
=
"All"
class
=
"text-danger"
></
div
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"UserName"
class
=
"col-md-2 control-label"
></
label
>
<
div
class
=
"col-md-10"
>
<
input
asp-for
=
"UserName"
class
=
"form-control"
/>
<
span
asp-validation-for
=
"UserName"
class
=
"text-danger"
></
span
>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
label
asp-for
=
"Password"
class
=
"col-md-2 control-label"
></
label
>
<
div
class
=
"col-md-10"
>
<
input
asp-for
=
"Password"
class
=
"form-control"
/>
<
span
asp-validation-for
=
"Password"
class
=
"text-danger"
></
span
>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
div
class
=
"col-md-offset-2 col-md-10"
>
<
div
class
=
"checkbox"
>
<
label
asp-for
=
"RememberMe"
>
<
input
asp-for
=
"RememberMe"
/>
@Html.DisplayNameFor(m => m.RememberMe)
</
label
>
</
div
>
</
div
>
</
div
>
<
div
class
=
"form-group"
>
<
div
class
=
"col-md-offset-2 col-md-10"
>
<
button
type
=
"submit"
class
=
"btn btn-primary"
>Log in</
button
>
</
div
>
</
div
>
</
form
>
</
section
>
</
div
>
</
div
>
</
div
>
<
div
class
=
"col-md-3"
></
div
>
</
div
>
When the Application runs and you click on the LogIn button, it makes a GET request for the Login() action and show the login screen, as shown in Figure 8.
Figure 8: Login Screen
Now, we login with the valid credentials of a user, which has “User” role as well, followed by redirecting on the Index action method of HomeController. The Index view shows is shown in Figure 9.
Figure 9: Home screen
Now, we login with valid credentials of a user while that user doesn’t have “User” role as well then it redirects on the AccessDenied action method of AccountController.
public
IActionResult AccessDenied()
{
return
View();
}
The AccessDenied action method returns AccessDenied view, which has the code snippet given below.
<
div
class
=
"row"
>
<
div
class
=
"col-md-3"
></
div
>
<
div
class
=
"col-md-6"
>
<
div
class
=
"top-buffer"
></
div
>
<
div
class
=
"panel panel-danger"
>
<
div
class
=
"panel-heading"
>Access Denied</
div
>
<
div
class
=
"panel-body"
>
<
section
>
<
h1
class
=
"text-danger"
>401 ! Access Denied</
h1
>
<
br
/>
<
a
href
=
"javascript:void(0)"
onClick
=
"backAway()"
class
=
"btn btn-success"
>Back</
a
>
</
section
>
</
div
>
</
div
>
</
div
>
<
div
class
=
"col-md-3"
></
div
>
</
div
>
@section scripts
{
<
script
>
function backAway() {
if (history.length === 1) {
} else {
history.back();
}
}
</
script
>
}
Now, run the Application and login with the valid credentials. Its authentication is successful. This authenticates the user, who doesn’t have ‘User’ roles due to which it’s not authorized to access Index method of HomeController and is being redirected
on access denied. The screen given below shows in case of access denied.
Figure 10: Access Denied
Logout
Now, create an action method for Logout in AccountController, as per the code snippet given below.
[HttpPost]
[ValidateAntiForgeryToken]
public
async Task<IActionResult> SignOff()
{
await signInManager.SignOutAsync();
return
RedirectToAction(
"Login"
);
}
This SignOutAsync clears the user claims, which are stored in cookies. This action method calls, when we click on Logout button, which is placed at the top.
Downloads
You can download the complete source code from the MSDN Sample, using the links, mentioned below.
- Rating Star Application in ASP.NET Core
- CRUD Operations in ASP.NET Core and Entity Framework Core
- Repository Pattern In ASP.NET Core
- Generic Repository Pattern in ASP.NET Core
- Onion Architecture In ASP.NET Core MVC
- ASP.NET Core MVC: Authentication and Role Based Authorisation with Identity
- ASP.NET Core MVC: Authentication and Claim Based authorization with Identity
See Also
It’s recommended to read more articles related to ASP.NET Core.
- ASP.NET Core: Overview
- ASP.NET Core With Visual Studio 2017 RC
- ASP.NET Core Entity Framework
Core Code First: CRUD Operations - Repository Pattern In ASP.NET Core
- ASP.NET Core: Generic Repository Pattern
- Onion Architecture In ASP.NET Core MVC
- ASP.NET
Core MVC: Authentication And Claim Based Authorisation With ASP.NET Identity Core - ASP.NET Core : Overview Of Dependency Injection
Conclusion
This article introduced the authentication and authorization in ASP.NET Core with ASP.NET Core Identity, using Entity Framework Core with the «code first» development approach. It explained the role based authorization in the Application. We used Bootstrap,
CSS and JavaScript for the user interface design in this Application.
In this post, I share what I have learned about integrated windows authentication and how to enable it in a web application which consists of an angular front-end and ASP.NET core 3 backend.
What is integrated windows authentication?
Let me explain by giving an example. At work, my computer is joined to a domain controller, which is basically a server that runs active directory. Joining a domain controller means the domain controller manages my credentials, not my computer. When I login using my windows credentials, my computer communicates with the domain controller to validate my credentials and allow access. We have .NET applications running on IIS on a set of servers that are joined to the domain controller. IIS can check against the domain controller to ensure I have authenticated before granting access. Furthermore, it can work with the browser do so seamlessly without requiring me to enter my credentials because it has built in integrated windows authentication. This is possible because both the server on which IIS runs and the browser on my machine are joined to a same domain controller, and the browser supports the Negotiate authentication scheme. From the document, this is an advantage of integrated windows authentication.
Built into IIS. – Does not send the user credentials in the request. – If the client computer belongs to the domain (for example, intranet application), the user does not need to enter credentials
Integrated Windows Authentication
Hopefully, you now have some ideas about integrated windows authentication. Next, let’s look at how it works.
How does integrated windows authentication work?
Per the document, integrated windows authentication
works with any browser that supports the Negotiate authentication scheme, which includes most major browsers.
Integrated Windows Authentication
The Negotiate authentication scheme is Microsoft’s authentication mechanism which uses Kerberos which is a system that validates a user’s identity based on shared secrets and provides access by issuing tickets.
Here is how it works.
To access a protected resource, the client must present a valid ticket to the server. To obtain the ticket, the client sends a request to a Key Distribution Center (KDC). The client encrypts the request using the user’s credentials. Upon receiving the encrypted request, the KDC retrieves the user’s password from active directory given the username, and uses the password to decrypt the request. By way of encrypting and decrypting the request using the user’s password which the KDC can get from the database, the KDC can verify the user’s identity without having the client sending over the password. Once the client receives the ticket, which the KDC encrypts using a key that it shares with the resource server, the client sends over the ticket to the resource server, which in turn validates the ticket against the KDC using the shared key. Once all the validations are done, the server returns the resource to the client.
The above is just a high level summary. If you want to learn more about Kerberos and see examples, I suggest you watch this short video, read this blog and IETF article.
Hopefully, you now have some ideas about how integrated windows authentication works, let’s discuss when should you use it.
When should you use integrated windows authentication
As a summarize, you should consider using integrated windows authentication if:
- Both the server and the client machine use Windows and are joined to the same domain controllers.
- The application is for internal use only. Obviously, if it is accessible by the public, it will not work because the client computers may not use Windows and joined to the domain controllers.
- The browser supports Negotiate mechanism (most major browsers supports it).
- The server supports integrated windows authentication. As mentioned in the document, IIS has built in support for integrated windows authentication.
The document mentions integrated windows authentication is susceptible to cross-site request forgery, so just keep this in mind.
Now that you know about integrated windows authentication and how it works, let’s look at how you can implement it in your ASP.NET core application.
In my case, it turns out to be not difficult to configure my application and IIS to use integrated windows authentication. I just have to make a few changes in the app, and enable Windows authentication in IIS.
Changes in applicationhost.config
Set <WindowsAuthentication
> to true in applicationhost.config, which is under .vs -> {PROJECT_NAME} -> config directory. The .vs directory is hidden by default, so I enabled the option to show the hidden folders.
<windowsAuthentication enabled="true"> <providers> <add value="Negotiate" /> <add value="NTLM" /> </providers> </windowsAuthentication>
See this link for instructions on how to view hidden folder in Windows 10.
Changes in launchSettings.json
In launchSettings.json, which is under Properties folder of the ASP.NET core project, enable WindowsAuthentication under iisSettings:
{ "iisSettings": { "windowsAuthentication": true, "anonymousAuthentication": false, "iisExpress": { "applicationUrl": "http://localhost:61863/", "sslPort": 44378 } } }
Changes in Startup.cs file
- In
Configure(...)
method, add these middlewares:
app.UseAuthentication(); app.UseAuthorization();
Since the app is an ASP.NET core 3 app, per the document, I put the above middlewares between app.UseRouting()
and app.UseEndpoints()
.
If the app uses authentication/authorization features such as
AuthorizePage
or[Authorize]
, place the call toUseAuthentication
andUseAuthorization
: after,UseRouting
andUseCors
, but beforeUseEndpoints
:Migrate from ASP.NET Core 2.2 to 3.0
If you want to learn more, checkout this post on StackOverflow.
In ConfigureServices()
method, I added the following:
services.AddAuthentication(IISDefaults.AuthenticationScheme); services.AddAuthorization();
Changes on IIS site on remote server
- In IIS Manager, under Features View of the site, double-click on Authentication feature.
- Select Windows Authentication and set Status to Enabled.
Changes in angular app
Technically, you don’t need to make any changes in angular for integrated windows authentication to work. Some tutorials online I looked at suggest to add to the header the key and value: withCredentials: true
. However, I realized that this is not necessary, and the authentication still work even after I removed the codes.It appears the browser automatically handles the process by the Negotiate authentication scheme.
Optional: Get windows user’s info in angular
It seems as if there is not a way to get info about the windows user from the client app. Therefore, to get the username and status of the windows user, I make the call to the backend.
- In the asp.net core app, I added the following endpoint to return info about the authenticated user.
[Route("GetAuthenticatedUser")] [HttpGet("[action]")] public IdentityUser GetUser() { return new IdentityUser() { Username = User.Identity?.Name, IsAuthenticated = User.Identity != null ? User.Identity.IsAuthenticated : false, AuthenticationType = User.Identity?.AuthenticationType }; } public class IdentityUser { public string Username { get; set; } public bool IsAuthenticated { get; set; } public string AuthenticationType { get; set; } }
- In angular, I added a guard to load the user’s info and validate the user has access before activating the route.
@Injectable() export class AuthGuardService implements CanActivate { constructor(public router: Router, private apiService: ApiService) { } canActivate(): Observable<boolean> | Promise<boolean> | boolean { return this.apiService.User.pipe( take(1), switchMap(currentUser => { if (!currentUser) { return this.apiService.loadUser().pipe( take(1), switchMap(user => { return of(user && user.isAuthenticated); }), catchError((err: HttpErrorResponse) => { if (err) { console.error("Failed to load user: " + JSON.stringify(err)); if (err.status === 403) { this.apiService.setErrorMessage("You don't have access to use this application."); } else { this.apiService.setErrorMessage("Something went wrong! :("); } } return of(false); })); } return of(currentUser.isAuthenticated); })); } }
References
Integrated Windows Authentication
Enable Windows Authentication In Web API And Angular App
What is the difference between Negotiate and NTLM authentication?
Kerberos – authentication protocol
SPNEGO-based Kerberos and NTLM HTTP Authentication in Microsoft Windows
How to enable Windows authentication for a Web site, Web application, or Web service
Windows Authentication in ASP.NET Core
Last Modified: 2017-03-28
Using Windows Authentication in ASP.NET Core Web Applications
ASP.NET Core Windows Authentication
Note that some of the content does not apply to RC1 or earlier versions and may not apply to later versions either.
General
- https://docs.asp.net/en/latest/security/authentication/index.html
- https://blogs.msdn.microsoft.com/webdev/2016/03/11/first-look-authentication-in-asp-net-core/
- https://docs.asp.net/en/latest/fundamentals/servers.html
- https://docs.asp.net/en/latest/publishing/iis.html
- https://github.com/aspnet/Announcements/issues/204
Enable Windows Authentication
The server running the application must be configured to enable windows authentication and disable anonymous authentication.
If anonymous authentication is enabled, then it will be used by default and no user information is collected or required.
Hosting Options
- IIS + Kestrel: Windows authentication is configured in IIS (or
PropertieslaunchSettings.json
when debugging with Visual Studio and IIS Express). - WebListener: Windows authentication is configured in web host builder programmatically.
At the time of writing, windows authentication only works when the server is hosted on the Windows platform (IIS and WebListener are Windows-only).
Take a look at ASP.NET Core Hosting for setting up either hosting option.
Sources:
- https://docs.asp.net/en/latest/fundamentals/servers.html
WebListener
When using WebListener, you need to set up the authentication scheme in WebListener options in Program.cs
:
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Net.Http.Server;
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseWebListener(options =>
{
options.ListenerSettings.Authentication.Schemes = AuthenticationSchemes.NTLM; // <--
options.ListenerSettings.Authentication.AllowAnonymous = false; // <--
})
.UseStartup<Startup>()
.Build();
host.Run();
}
Note: installing package Microsoft.Net.Http.Server
from NuGet is required for accessing the AuthenticationSchemes class.
Sources:
- https://github.com/aspnet/Announcements/issues/204
- http://stackoverflow.com/questions/37694211/windows-authentication-with-asp-net-core
IIS Integration
When using IIS Integration (Express or not), there are some configuration options that you can tweak.
Add configuration in Startup.cs
in the ConfigureServices
method:
services.Configure<IISOptions>(options => {
//options.AuthenticationDescriptions holds a list of allowed authentication schemes
options.AutomaticAuthentication = true;
options.ForwardClientCertificate = true;
options.ForwardWindowsAuthentication = true;
});
All three options default to true
at least when running on IIS Express through Visual Studio.
Source: https://docs.asp.net/en/latest/fundamentals/servers.html
IIS Express (when Debugging from Visual Studio)
In visual studio, right-click into the project properties and select the Debug tab.
Check “Enable Windows Authentication” and uncheck “Enable Anonymous Authentication”
The values are stored in PropertieslaunchSettings.json
:
{
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false,
...
},
...
}
Making this change also forces forwardWindowsAuthToken
to true
in web.config
(aspNetCore
-element under system.webServer
) each time you start the app in debug mode.
IIS
Enable windows authentication in IIS application host configuration file which can be found in the system32inetsrv
directory.
NOTE: IIS Express application configuration file lives in $(solutionDir).vsconfigapplicationhost.config
source when using Visual Studio 2015 (or %userprofile%documentsiisexpressconfigapplicationhost.config
or somewhere else when using an earlier version).
TODO not verified using IIS Express directly. The configuration does not affect the behaviour of IIS Express when debugging through Visual Studio.
The correct section can be found in configuration -> system.webServer -> security -> authentication -> windowsAuthentication.
The configuration should look as follows.
<windowsAuthentication enabled="true">
<providers>
<add value="Negotiate" />
<add value="NTLM" />
</providers>
</windowsAuthentication>
TODO May have to remove the Negotiate
provider as per http://stackoverflow.com/questions/36946304/using-windows-authentication-in-asp-net?
Windows authentication can also be enabled using the Internet Information Services Manager:
Go to the site’s Authentication settings, enable Windows Authentication and disable Anonymous Authentication.
Make sure that the forwardWindowsAuthToken
is set to true
in web.config
(aspNetCore
-element under system.webServer
).
Sources:
- https://docs.asp.net/en/latest/publishing/iis.html
- http://www.codeproject.com/Tips/1022870/AngularJS-Web-API-Active-Directory-Security
- http://stackoverflow.com/questions/4762538/iis-express-windows-authentication
- http://stackoverflow.com/questions/36946304/using-windows-authentication-in-asp-net
- http://www.danesparza.net/2014/09/using-windows-authentication-with-iisexpress/
Identity Impersonation
TODO For accessing further resources such as an SQL DB or other APIs with windows authentication.
Sources:
- http://stackoverflow.com/questions/35180871/asp-net-core-1-0-impersonation
- https://aleksandarsimic.wordpress.com/2016/07/21/asp-net-core-1-0-iis-impersonation/
Accessing User Information
CSHtml
You can access user identity in .cshtml
files by using, for example:
<pre>@Html.Raw(Json.Serialize(User, new Newtonsoft.Json.JsonSerializerSettings() { ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore }))</pre>
<p>Name: @User.Identity.Name</p>
<p>Authenticated: @User.Identity.IsAuthenticated</p>
If you need to access the HttpContext, you need to add the HttpContextAccessor service in Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
...
}
And in cshtml:
@inject IHttpContextAccessor httpContextaccessor
<pre>@Html.Raw(Json.Serialize(HttpContextAccessor.HttpContext.User.Identity, new Newtonsoft.Json.JsonSerializerSettings() { ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore }))</pre>
Source: http://stackoverflow.com/questions/38945678/access-cookie-in-layout-cshtml-in-asp-net-core
In MCV or WebAPI Controllers
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var userName = User.FindFirstValue(ClaimTypes.Name);
var userName2 = User.Identity.Name;
Requires package Microsoft.AspNetCore.Identity
Sources:
- http://stackoverflow.com/questions/30701006/how-to-get-the-current-logged-in-user-id-asp-net-core
JavaScript
There is no way that I came across to get at the windows user information directly in JavaScript, except by injecting through script tags and cshtml.
Source: http://stackoverflow.com/questions/3013692/getting-windows-username-with-javascript
Calling API Methods from JavaScript
Make sure you include credentials in calls, e.g. with fetch
:
fetch("/api/SampleData/WeatherForecasts", { credentials: 'include' })
.then(response => { ... });
Local groups:
- Local groups are written without the domain part or prefixed with the host name:
<group>
or<hostname><group>
. - Built-in local groups (e.g.
BUILTINAdministrators
) are not recognized by name.
You have to write the corresponding SID instead. - You can find out the SIDs by using the
PsGetSid
tool: https://technet.microsoft.com/en-us/sysinternals/bb897417. - The
BUILTINAdministrators
group is not recognized even when using the correct SID.
Group membership shows as role membership in ASP.NET Core.
You can enforce group membership directly with the Authorize attribute, with an authorization policy, or programmatically in the controller methods.
Authorize Attribute
Add [Authorize(Roles = @"<domain><group>")]
attribute (or [Authorize(Roles = @"<domain><group1>,<domain><group2>")]
for multiple allowed roles) to the controller or method.
Sources:
- https://docs.asp.net/en/latest/security/authorization/roles.html
Authorization Policy
Add a new policy to service configuration in ConfigureServices
method in Startup.cs
:
services.AddAuthorization(options =>
{
options.AddPolicy("RequireWindowsGroupMembership", policy => policy.RequireRole(@"<domain><group>"));
});
To get the required group name from settings, add the group name into appsettings.json
(note the double backslashes):
{
"Logging": {
...
},
"WindowsGroup": "<domain>\<group>"
}
Then read it in when configuring authorization:
services.AddAuthorization(options =>
{
var windowsGroup = Configuration.GetValue<string>("WindowsGroup");
options.AddPolicy("RequireWindowsGroupMembership", policy =>
{
policy.RequireAuthenticatedUser(); // Policy must have at least one requirement
if (windowsGroup != null)
policy.RequireRole(windowsGroup);
});
});
Use a comma-separated string for multiple allowed roles: <domain><group1>,<domain><group2>
.
Finally, add the authorize-attribute on the controller or method: [Authorize(Policy = "RequireWindowsGroupMembership")]
Sources:
- https://docs.asp.net/en/latest/security/authorization/roles.html
The policy syntax allows for more elaborate authorization scenarios with custom requirements, such as activity/permission-based authentication
- https://docs.asp.net/en/latest/security/authorization/policies.html
- https://lostechies.com/derickbailey/2011/05/24/dont-do-role-based-authorization-checks-do-activity-based-checks/
- http://benjamincollins.com/blog/practical-permission-based-authorization-in-asp-net-core/
- http://benfoster.io/blog/asp-net-identity-role-claims
Programmatically
Check for role membership in controller method and return 403 Forbidden status code if not authorized.
[HttpGet("[action]")]
public IActionResult SomeValue()
{
if (!User.IsInRole(@"DomainGroup")) return StatusCode(403);
return Ok("Some Value");
}
Note that the return type of the method must be IActionResult
.
Browser Settings
If you need automatic windows authentication, then you may have to enable it specifically in the client browser
- IE (TODO verify same works in EDGE)
- Advanced -> Enable Integrated Windows Authentication in Internet Options
- Security -> Local intranet -> Custom level -> User Authentication -> Automatic logon / Prompt for user name and password
- Chrome
- Chrome uses settings in Windows’ internet options so the IE options should sufficesource
- Firefox
- about:config -> network.automatic-ntlm-auth.trusted-uris -> add url of application
Sources:
- http://www.codeproject.com/Tips/1022870/AngularJS-Web-API-Active-Directory-Security
- http://stackoverflow.com/questions/36946304/using-windows-authentication-in-asp-net
Different Domain or No Domain Binding
TODO I did not get this to work from a remote site, with or without VPN connection (flashes a new console window and dies instantly, unable to capture error message)
If you are developing on a computer that is not bound to a domain, or is bound to a different domain that the app should authenticate against, you can run the server like so:
runas /netonly /user:<user> "<command> <args...>"
where <user>
is domainusername
or username@domain
.
IIS: you must establish trust between the two domains to be able to run app pools under a user in different domain than the server.
IIS: does this work at all when running as network service??
Sources:
- http://codebetter.com/jameskovacs/2009/10/12/tip-how-to-run-programs-as-a-domain-user-from-a-non-domain-computer/
- http://stackoverflow.com/questions/4762538/iis-express-windows-authentication
- http://stackoverflow.com/questions/5331206/how-to-run-iisexpress-app-pool-under-a-different-identity
- http://stackoverflow.com/questions/22058645/authenticate-against-a-domain-using-a-specific-machine/22060458#22060458
- https://forums.iis.net/t/1213147.aspx?How+I+can+run+IIS+app+pool+by+domain+account+
- https://blogs.msdn.microsoft.com/ssehgal/2009/06/23/running-iis6-app-pools-under-a-domain-account-identity/
- Remove From My Forums
-
Question
-
User-462241089 posted
Hey, sorry, its me again. I asked this question and this
one too, but I just had a code review with my team and it seems like I was doing to all wrong.What I actually need to do is integrate a windows authentication in my .NET Core MVC application. I was referred to this article
for help in implementing this, but it doesn’t quite make any sense.All this needs to do is use windows authentication (the basic one, I believe), but instead of the popup window, it uses the input fields I have on a webpage. The problem is, I have no idea where to even begin, and everything with .net core is really vague.
Does anyone know how to do this? My team says it is possible…
Answers
-
User-474980206 posted
Windows authentication takes place between the browser and IIS. You can use basic, Kerberos or ntlm. This is all configured with iis. You write no code to implement. You don’t have a form, the browser manages the login.
https://docs.microsoft.com/en-us/iis/configuration/system.webserver/security/authentication/windowsauthentication/
once it’s configured, you app want to know the user name. With asp.net core, this is handled by iis integration. you configure asp.net core to create an identity based on the token iis passes to asp.net core.
if you want to use your own login form, with windows accounts, then that’s AD authentication, not windows. In this case you need an AD provider for asp.net identity.
-
Marked as answer by
Thursday, October 7, 2021 12:00 AM
-
Marked as answer by
Most REST services that are being built using asp.net core now are using token based authentication either using asp.net core authentication middleware or third party products such as Identity Server. But, sometimes you only need to build your APIs for intrenal use within your organization who happens to be using Windows Authentication.
In this point, I will explain how to build a web API that utilizes AD for authentication and AD groups for authorization and how to integrate it with authorization policies.
Creating the project
Open Visual Studio 2017, Create new asp.net core Web Application and name it AspnetCoreWindowsAuth, then press Ok. Choose Web API as a project Template and Change the authentication method to Windows then press Ok to create the project.
If you select the project in the solution explorer and press F4, you will find nothing to set the authentication mode to Windows and enable/disable anonmous access just like you used to do in normal MVC web application. This is because it is moved to the launchsettings.json file under the properties folder. If you want to change it, you have to open the file and edit the value of the json property iisSettings which looks like below:
You can also modify the URL and SSL settings.
Now, if you run the project, it will run just fine and you can call the default Values controller and see the output and even windows authentication will be working as well and you can get the name of the logged in user using the User.Identity.Name
property and it will return the Domain\username although we didn’t add any authentication code yet in the pipeline
Add windows authentication middleware
Now, lets add the authentication middleware into the request processing pipeline. Add the line app.UseAuthentication();
in the Configure
method just before the app.UseMvc();
. Remeber that the middlewares run in the same order they were added in the Configure method.
Add the following code in the ConfigureServices
method before the services.AddMvc();
services.Configure(options =>
{
options.AutomaticAuthentication = true;
});
services.AddAuthentication(IISDefaults.AuthenticationScheme);
To make sure this is working fine, you can edit the Authorize attribute on the ValuesController and add the role name which should be an AD group name, ex: Employees
[Authorize(Roles ="Employees")]
Now you have asp.net core working fine with Active Directory and you can can authenticate the users according to the AD groups they belong to.
Using Authorization Policies
If you need more fine grained control over your controllers and you need to add more authorizastion logc, then you can go for authorization policies and it is really easy to configure as you can see below. Just add the following lines in the ConfigureServices
method before the AddMvc statement
services.AddAuthorization(options =>
{
options.AddPolicy("OnlyEmployees", policy =>
{
policy.AddAuthenticationSchemes(IISDefaults.AuthenticationScheme);
policy.RequireRole(""S-1-5-4");
});
});
Here we defined a policy called OnlyEmployees and it requires the users to be windows authenticated and in the Role named Employees which is eventually mapped to AD group named employees. Notice that I didn’t write the name Employees in the RequireRole method. Instead, the value “S-15-4” was used, which is the SID for the AD Group named Employees. I found that this is how the group names are mapped to Roles in asp.net core and even if you tried to retrive the list of claims that the user have, it will translate to all SIDs of the groups that the user belongs to in AD.
To utilize this policy you have to annotate the controller or method with it as below
[Authorize(Policy = "OnlyEmployees")]
[Route("api/[controller]")]
public class ValuesController : Controller
{
}
By now you should have a working solution that depends on windows authentication and AD groups. Notice that this will only work with windows and most probably IIS.
You can find the code on GitHub if you want to use it or add to it.
https://github.com/haitham-shaddad/aspnetcore-windows-auth