setup with Asp.net & .Net Core 3.1, I have upgraded a previous Identity system using UserName/Password with Roles to use Windows Authentication.
I have created a ClaimsTransformation which gets the windows Identity and creates a new ClaimsPrincipal with the users associated roles. This part is working
My startup.cs looks like this (some parts removed)
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IClaimsTransformation, KiwaClaimsTransformation>();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthorization();
...
services.AddControllers();
services.AddControllersWithViews()
.AddSessionStateTempDataProvider();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{
...
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
// catch all for not found
endpoints.MapControllerRoute("NotFound", "{*url}",
new {controller = "Error", action = "ResourceNotFound"});
});
...
}
The ClaimsTransformation looks like this
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = (ClaimsIdentity)principal.Identity;
if (identity == null) return principal;
var userName = _config["LoginUserName"];
if (userName == null)
{
userName = identity.Name;
if (userName == null) return principal;
}
// need to go and build the Roles claims for the user based on the User Name as a lookup on User table
var claims = new List<Claim>
{
new Claim(#"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", userName, "Name")
};
claims.AddRange(_userLookup.GetUserRolesByNetworkId(userName)
.Select(role => new Claim(ClaimTypes.Role, role)));
//The claim identity uses a claim with the claim type below to determine the name property.
// Get User Roles from database and add to list of claims.
var newClaimsIdentity = new ClaimsIdentity(claims, "Kerberos", "", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
return new ClaimsPrincipal(new ClaimsPrincipal(newClaimsIdentity));
}
I have a basic HomeController which looks like this
public class HomeController : Controller
{
private readonly LoggedOnUser _loggedOnUser;
public HomeController(LoggedOnUser loggedOnUser)
{
_loggedOnUser = loggedOnUser;
}
[Authorize]
[HttpGet]
public IActionResult Index()
{
// check and make sure the user is allowed in
if (!_loggedOnUser.IsValidKiwaUser)
{
return RedirectToActionPermanent("NotAuthorised");
}
return View();
}
[Authorize]
public IActionResult OperationResults()
{
ViewBag.Title = (string)TempData["Title"];
string jsonString = (string)TempData["OperationResults"];
if (string.IsNullOrWhiteSpace(jsonString))
{
return RedirectToPage("/Error/NoResults");
}
return View(JsonConvert.DeserializeObject<List<OperationResult>>(jsonString));
}
public IActionResult NotAuthorised()
{
return View();
}
All of the Controllers have [Authorize(Role="...")], and the Authorisation is happening correctly and the Roles are added as claims via the ClaimsTransformation.
The issue i am having is that if i hit the root of the Website (debugging this is https://localhost:44391), then the routing sends me to the NotAuthorised page on the controller??? It should be default go to https://localhost:44391/Home/index as defined in the default Endpoint.
If I type in https://localhost:44391/Home/index it works and shows the correct main landing page, but if i do NOT include the https://localhost:44391/Home/index in its entirety then it comes back as unauthorized.
Am i missing something here? Also can i turn
I eventually found the issue. During the transition to change over to Windows Authentication, i had left the cookie support i the product. But what this had done was store the starting page as being the NotAuthorised page. Clearing the cookie (and subsequently removing the cookie support from the app), fixed the issue and the Roles were evaluated all the time. Hence why I used a lookup (memory Cache) for accessing the user and their claims - as it gets called for all User requests
Oh by the way. The check for _loggedOnUser.IsValidKiwaUser in the HomeController/Index is actually no longer required if you use this as an example
Related
I want to be able to create a simple authorization scheme where I can protect a given route using [Authorize]. I want to control what happens here. I very simply would like to check the headers for a field like Authorization and grab the bearer token and then manually review it and inject an identity for a single HTTP call. After this the identity is lost and any new calls require another authorization check.
I've been reading the .NET Core 2 documentation but haven't had much luck yet with the role, claim, or policy based authorization schemes outlined in the documentation.
In Java, this is pretty easily achieved via Spring MVC but how can I accomplish the same thing here?
Thanks!
With some time spent in research I was able to find that the best practices are to use an existing scheme. I wasn't able to find a good example of a custom setup but most known schemes are well supported so I chose to use JWT authentication. It is very similar to what I wanted to do originally by generating a randomized token and using that for access to privileged request going forward.
This code works for me. It is a very simple example but it clearly shows how to login and then authenticate to a secured method.
You will want to change the private key and have a way of authenticating your users instead of the static hard-coded user check that I did below.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("THIS_IS_YOUR_SECRET_KEY_CREATE_SOMETHING_RANDOMIZED_AND_COMPLICATED"))
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
//app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
HomeController.cs
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using coreapi.Models;
using Microsoft.AspNetCore.Authorization;
namespace coreapi.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return Json(new ErrorViewModel());
}
[Authorize]
public IActionResult Secured()
{
ErrorViewModel model = new ErrorViewModel();
model.RequestId = "secured works";
return Json(model);
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return Json(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
}
}
}
AuthController.cs
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using coreapi.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace coreapi.Controllers
{
public class AuthController
{
[HttpPost]
public IActionResult Login([FromBody]LoginModel user)
{
if (user == null)
{
return new BadRequestObjectResult("Invalid client request");
}
if (user.EmailAddress == "bob#bobsburgers.com" && user.Password == "test123")
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("THIS_IS_YOUR_SECRET_KEY_CREATE_SOMETHING_RANDOMIZED_AND_COMPLICATED"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
// issuer and audience aren't currently being used
var tokeOptions = new JwtSecurityToken(
issuer: "https://localhost:8080",
audience: "https://localhost:8080",
claims: new List<Claim>(),
expires: DateTime.Now.AddMinutes(5),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return new OkObjectResult(new { Token = tokenString });
}
else
{
return new UnauthorizedResult();
}
}
}
}
LoginModel.cs
namespace coreapi.Models
{
public class LoginModel
{
public string EmailAddress { get; set; }
public string Password { get; set; }
}
}
Sorry in advance for the essay.
I have an MVC application which uses windows authentication.
A number of roles and 'system actions' have been stored in a database.
I've created a custom principal which has a base of ClaimsPrincipal.
I've also created an AuthenticationFilter which implements IAuthenticationFilter.
In the filter, i create a new instance of my custom principal, add the roles and 'system actions' as claims, and then assign it to the filter.Principal.
From there i have a custom AuthorizationAttribute which will make use of these roles and system actions. (i.e. each controller/action will have a [CustomAuthorizationAttribute(Roles = "blah", SystemActions = "blah")])
Additionally - i have a disclaimer page - which when the user agrees, needs to store a claim in my CustomPrincipal. my custom authorization attribute then checks to see if that claim exists.
Now that the background is out of the way;
The issue is that i need to cache this principal somehow - so that i don't have to hit the database on every request.
Am i best to store it in session? Or a cookie? Or is there some other way to do it?
What are the pros and cons of each?
I was leaning towards using a cookie - although this decision is not made with much knowledge of the pros/cons of each (hence the above question).
How would i go about implementing the cookie?
From the disclaimer page, i would then need to add my 'DisclaimerAccepted' claim, and update the cache.
AuthFilter code for reference:
public class AuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
var principal = new CustomPrincipal(filterContext.Principal);
var roles = GetRolesForUser(principal.Identity.Name);
var systemActions = new List<SystemAction>();
foreach (var role in roles)
{
principal.AddRole(role.Name);
systemActions.AddRange(GetSystemActionsForRole(role.Id));
}
principal.AddSystemActions(systemActions.Select(a => a.Name));
filterContext.Principal = principal;
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
filterContext.Result = new HttpUnauthorizedResult();
}
}
The issue is that i need to cache this principal somehow - so that i
don't have to hit the database on every request. Am i best to store it
in session?
You can use OWIN Cookie middleware in which we store claims inside cookie so that we only query database once.
On subsequent request, OWIN Cookie Middleware retrieves the claims from the cookie, and add those to Principle object.
Additionally - i have a disclaimer page - which when the user agrees,
needs to store a claim in my CustomPrincipal.
If you want to add new claims, you will need to call authenticationManager.SignIn(identity); again.
Startup.cs
Configure OWIN Cookie Middleware at startup.
[assembly: OwinStartup(typeof(YourApplication.Startup))]
namespace YourApplication
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ApplicationCookie",
LoginPath = new PathString("/Account/Login")
});
}
}
}
OwinAuthenticationService
public class OwinAuthenticationService : IAuthenticationService
{
private readonly HttpContextBase _context;
private const string AuthenticationType = "ApplicationCookie";
public OwinAuthenticationService(HttpContextBase context)
{
_context = context;
}
public void SignIn(User user)
{
IList<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName),
};
ClaimsIdentity identity = new ClaimsIdentity(claims, AuthenticationType);
IOwinContext context = _context.Request.GetOwinContext();
IAuthenticationManager authenticationManager = context.Authentication;
authenticationManager.SignIn(identity);
}
public void SignOut()
{
IOwinContext context = _context.Request.GetOwinContext();
IAuthenticationManager authenticationManager = context.Authentication;
authenticationManager.SignOut(AuthenticationType);
}
}
You can look at my working sample project at GitHub.
I have created an enum with security access levels, an example:
public enum AccessLevel
{
Total,
DeletionPrivileges,
MaintainUsers,
MaintainInventory,
QueriesOnly,
None
}
I can manage the site so certain features eg delete, are not presented to someone without deletion privileges. But I am also wanting to use some kind of authorisation within the code.
Within the default framework, there is the facility to prevent access to certain areas of a project using [Authorize], how can I create differing levels of authority to tag each method?
You could use claim based authentication feature of Identity to aim this purpose easily. First you need add proper claim per user in log in action method to do this change your log in action method like this:
[HttpPost]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var userManager=HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
var user = userManager.Find(model.UserName, model.Password);
if (user != null)
{
var ident = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
// imaging you have a custom class which return user access levels
var userAccessLevels=_accessLevelManager.GetAccessLevels(user.Id);
// now we are going to add our custom claims
ident.AddClaims(new[]
{
// add each access level as a separate claim
new Claim("AccessLevel",userAccessLevels[0].ToString()),
new Claim("AccessLevel",userAccessLevels[1].ToString()),
// and so on
});
HttpContext.GetOwinContext().Authentication.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);
// authentication succeed do what you want
return Redirect(login.ReturnUrl ?? Url.Action("Index", "Home"));
}
}
ModelState.AddModelError("", "Invalid username or password");
return View(login);
}
Now we have successfully injected our claims to Identity. But you need a custom authorize attribute to check your claims like this:
public class ClaimsAccessAttribute : AuthorizeAttribute
{
public string ClaimType { get; set; }
public string Value { get; set; }
protected override bool AuthorizeCore(HttpContextBase context)
{
return context.User.Identity.IsAuthenticated
&& context.User.Identity is ClaimsIdentity
&& ((ClaimsIdentity)context.User.Identity).HasClaim(x =>
x.Type == ClaimType && x.Value == Value);
}
}
Now you could easily use your attribute in your action methods:
[ClaimsAccess(CliamType="AccessLevel",Value="DeletionPrivileges")]
public ActionResult MyAction()
{
// also you have access the authenticated user's claims
// simply by casting User.Identity to ClaimsIdentity
// ((ClaimsIdentity)User.Identity).Claims
}
I am very confused with Authentication and Authorization in ASP.NET MVC 5.
I am working on an existing website and I need to add security in it. By security I mean Authentication (Logins) and Authorization (Roles). I have access to a Webservice, but not directly to the database though I can access the Entities (Users, Roles etc.).
Membership Provider seems to be a bit old, so I took a look at Identity but it looks complicated to implement to an existing project, especially when I don't have direct access to the database.
What would be a good solution ? What are the best practices ?
Could you suggest me any good resource so I can suits my needs ?
Thank you.
In case someone feel as lost as I was, here is a potential solution using Claims. Ath the end, you will know how to handle Authentication, Authorization and Roles.
Hope this can help.
Startup config
In the root folder off my project I have created a file, startup.cs. She contains a partial class that we will use to configure the application to use a cookie that store the signed user.
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
Then, in the App_Start I have a file, Startup.Auth.cs
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
}
}
Controller
First, I have created an AcountController.cs with attribute of type IAuthenticationManager. This attribute gets the authentication middleware functionality available on the current request.
public class CompteController : Controller
{
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
}
Then, I have a classic view called Login with GET and POST. In the post I check in my Webservice if the user can Log In. If he can, I call a the magic function to authenticate. In this code, the class User is the custom User I get in the Webservice. He don't implement IUser.
private void AuthentifyUser(User user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
CustomIdentity identity = new CustomIdentity(user);
CustomPrincipal principal = new CustomPrincipal(identity);
Thread.CurrentPrincipal = principal;
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
Last important method in my Controller allow users to Log Out.
public ActionResult Deconnexion()
{
AuthenticationManager.SignOut();
return RedirectToAction("Login", "Account");
}
Claims
CustomIdentity and CustomPrincipal are two custom class that I use for the Claims system. They indirectly implement IIdentity and IPrincipal. I put them in a separate new folder.
-Remember, A principal object represents the security context of the user on whose behalf the code is running, including that user's identity (IIdentity) and any roles to which they belong.
-An identity object represents the user on whose behalf the code is running.
public class HosteamIdentity : ClaimsIdentity
{
public HosteamIdentity(User user)
: base(DefaultAuthenticationTypes.ApplicationCookie)
{
AddClaim(new Claim("IdUser", user.Id.ToString()));
AddClaim(new Claim(ClaimTypes.Name, user.Name));
AddClaim(new Claim(ClaimTypes.Role, user.Role));
}
public int IdUser
{
get
{
return Convert.ToInt32(FindFirst("IdUser").Value);
}
}
//Other Getters to facilitate acces to the Claims.
}
The Principal gives us access to the Identity.
public class HosteamPrincipal : ClaimsPrincipal
{
private readonly HosteamIdentity _identity;
public new HosteamIdentity Identity
{
get { return _identity; }
}
public HosteamPrincipal(HosteamIdentity identity)
{
_identity = identity;
}
public override bool IsInRole(string role)
{
return _identity.Role == role;
}
}
Access the CustomPrincipal
Now, I will lgo to the gGlobal.asax, here we will override the Application_PostAuthenticateRequest event. This event is fired when a security module has established the identity of the user.
We will use Thread.CurrentPrincipal, this static object Gets or sets the thread's current principal (for role-based security), so it is perfectly adapted to our case !
You may have to adapt the code here. I personally have to request my Webservice, this may not be your case.
Just talking briefly about our constructors. The fist is empty, we will use it when we don't care about Roles
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
Thread.CurrentPrincipal = new HosteamPrincipal(
new HosteamIdentity(
WebService.GetUser(
HttpContext.Current.User.Identity.Name)));
}
}
In most case, retrieving the user by is Name is not a good practice. Please, adapt the above code to your solution.
Authorize Attribute Filter
Now, it will be great if we could easily tell which Controller or Action can be accessed by an authenticated user. To do so, we will use Filters.
Filters are custom classes that provide both a declarative and programmatic means to add pre-action and post-action behavior to controller action methods. We use them as annotation, for example [Authorize] is a Filter.
As there is to many things to explain, I will let you read the comments, they are very explicit.
Just talking briefly about our Constructors.
-The first one is empty, we will use it when we don't care about Roles. We access it by writing the annotation [CustomAuthorize] abose a Controller or an Action.
-The second one, takes an array of Roles, we will use it by writing the annotation [CustomAuthorize("Role1", "Role2", etc.)] abose a Controller or an Action. He will define which Roles access the Controller or action
public class CustomAuthorize : AuthorizeAttribute
{
private new string[] Roles { get; set; }
public CustomAuthorize() { }
public CustomAuthorize(params string[] roles)
{
this.Roles = roles[0].Split(',');
}
/// <summary>
/// Check for Authorizations (Authenticated, Roles etc.)
/// </summary>
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext.Request.IsAuthenticated)
if (Roles != null)
{
foreach (string role in Roles)
if (((HosteamPrincipal)Thread.CurrentPrincipal).IsInRole(role))
return true;
return false;
}
else
return true;
return false;
}
/// <summary>
/// Defines actions to do when Authorizations are given or declined
/// </summary>
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (!AuthorizeCore(filterContext.HttpContext))
HandleUnauthorizedRequest(filterContext);
}
/// <summary>
/// Manage when an Authorization is declined
/// </summary>
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.Forbidden);
else
base.HandleUnauthorizedRequest(filterContext);
}
}
I'm looking to secure different areas of my MVC application to prevent standard user's from accessing admin type views. Currently, if any user is logged in and they attempt to view the About page (out of the box template in visual studio), it will simply redirect them to the login page. I'd prefer the user is informed that they do not have permission to view the page.
[Authorize(Roles="Admin")]
public ActionResult About()
{
return View();
}
It seems redundant to send an already authenticated user to the login page when they don't have permission.
Here is an attribute that I've created that can be used to direct to an unauthorized security action. it also allows you to specify a Reason which will be passed to the Unauthorized action on the Security controller, which you can then use for the view.
You can create any number of properties to customize this to fit your particular application, just make sure to add it to the RouteValueDictionary.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public sealed class ApplySecurityAttribute : ActionFilterAttribute
{
private readonly Permission _permission;
public ApplySecurityAttribute(Permission permission)
: this(permission, string.Empty) {}
public ApplySecurityAttribute(Permission permission, string reason)
{
_permission = permission
Reason = reason;
}
public string Reason { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!PermissionsManager.HasPermission(_permission)) // Put security check here
{
var routeValueDictionary = new RouteValueDictionary
{
{ "controller", "Security" }, // Security Controller
{ "action", "Unauthorized" }, // Unauthorized Action
{ "reason", Reason } // Put the reason here
};
filterContext.Result = new RedirectToRouteResult(routeValueDictionary);
}
base.OnActionExecuting(filterContext);
}
}
Here is the security controller
public class SecurityController : Controller
{
public ViewResult Unauthorized(string reason)
{
var vm = new UnauthorizedViewModel { Reason = reason };
return View(vm);
}
}
Here is the attribute declaration on a controller you wish to secure
[ApplySecurity(Permission.CanNuke, Reason = "You are not authorized to nuke!")]
Here is how PermissionsManager does the check to see if the user has the permissions
public static class PermissionsManager
{
public static bool HasPermission(EZTracPermission permission)
{
return HttpContext.Current.GetCurrentUser().Can(permission);
}
}