Showing posts with label user. Show all posts
Showing posts with label user. Show all posts

Impersonation in Windows Service with ASP.NET Core

In a project one of the requirements was to be able to impersonate the current logged on Windows user.

The ASP.NET Core 2.1 project was hosted in a Windows Service. More info about this here:
Host ASP.NET Core in a Windows Service

The problem was that when doing WindowsIdentity.GetCurrent().Name; what was being returned was the account name used as Log On as for the service. That makes sense since it is that account that is executing the service.

This particular project needed to get the current logged on Windows user and check their permissions system wide, that is, check which claims the current user making use of the service has. Claims are the key component by which the outcome of all Web-based authentication and authorization requests are determined. More info on that here: The Role of Claims.

This way we can assure a user belongs to a specific User Group, the user has read permission on some folder, etc.

After trying some options, nothing worked. Then I decided to search for “Windows Authentication in ASP.NET Core” using Google and it lead me to this page: Configure Windows Authentication in ASP.NET Core.
Since we’re not hosting the app in IIS, that was a no go. However there’s another option below that page and it’s called HTTP.sys. HTTP.sys server supports self-hosted scenarios on Windows and that was the way to go.


First thing we need to do is make use of it when creating the WebHost object:

public static IWebHostBuilder CreateWebHostBuilder(string[] args,
    IConfigurationRoot config) =>
        WebHost.CreateDefaultBuilder(args)
               .UseConfiguration(config)
               .UseStartup<Startup>()
               // Using HTTP.sys for Windows Authentication support
               .UseHttpSys(options =>
               {
                   options.Authentication.Schemes =
                   AuthenticationSchemes.NTLM | AuthenticationSchemes.Negotiate;
                   options.Authentication.AllowAnonymous = false;
               });

Then in Startup.cs inside the method ConfigureServices we do:

// This method gets called by the runtime.
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddLogging(builder =>
    {
        builder.AddFilter("Microsoft", LogLevel.Warning)
               .AddFilter("System", LogLevel.Warning)
               .AddConsole();
    });
    // HTTP.sys server
    // HttpSysDefaults requires the following import:
    // using Microsoft.AspNetCore.Server.HttpSys;
    services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);
services.AddMvc();
}


That's it.

Now to impersonate the user we do the following:


[HttpGet]
public async Task<IActionResult> MethodWithImpersonatedWindowsUser(
[FromQueryAttribute] string filePath)
{
    // The user used as Log On as for the Windows Service
    var serviceUser = WindowsIdentity.GetCurrent().Name;
   
    // The user to be impersonated
    var userToImpersonate = (WindowsIdentity)HttpContext.User.Identity;
    // Impersonating the current Windows user [HttpContext.User.Identity]...
    var result = await WindowsIdentity.RunImpersonated(
                                     userToImpersonate.AccessToken, async () =>
    {
        // This time WindowsIdentity.GetCurrent() will retrieve the impersonated
        // user with its claims...
        var impersonatedUser = WindowsIdentity.GetCurrent().Name;
        // Your business logic code here...
    });
    return result;
}

I saw lots of people asking the same question I made prior to knowing how to handle this requirement: why are we seeing the service user returned instead of the current logged on user when doing WindowsIdentity.GetCurrent()? The answers were to the point: in Windows you can have many user sessions (Remote Desktop, etc) where all are logged on at the same time making use of the Windows Service. So, how to get the current logged on user while still making use of the service? That must come from the HttpContext, that is, from the request arriving from the browser. WindowsIdentity.RunImpersonated makes it all possible. It runs the specified lambda action as the impersonated Windows identity.

User Password Expired filter attribute in ASP.NET MVC

A recent set of requirements I’ve been playing with deals with passwords. This one specifically handles password expiration.

Given that I’m working with ASP.NET MVC I know I can rest assured that there’s some great (read awesome) way of implementing a given requirement. This is exactly what happened and I want to show you how to have a clean and beautiful solution to this problem.

So my client’s requirement is the following:

Passwords should expire in 45 days.

I’m currently using the default ASP.NET membership provider. It gives you a database schema ready to manage users and roles. You just have to use ASP.NET Configuration Tool to create Roles and Users, decorate your Controllers/Actions with the Authorize attribute and you’re good to go most of the time. The default membership provider allows a fast project start – no doubt – but as always there’s something that must be done according to the infinitude of possible requirements that change project by project. One of these not contemplated things is a setting in the default provider for handling user’s password expiration. We have to roll our own code to manage this.

My friend Google told me that some folks have already done some work related to this and as always I borrow some of their code and adapt it to my specific case/technology.

First I created a PasswordExpiredAttribute that derives from/extends the AuthorizeAttribute. Here’s its code:

public class PasswordExpiredAttribute : AuthorizeAttribute
{
    private static readonly int PasswordExpiresInDays =
int.Parse(ConfigurationManager.AppSettings["PasswordExpiresInDays"]);

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        IPrincipal user = filterContext.HttpContext.User;
           
        if(user != null && user.Identity.IsAuthenticated)
        {
            MembershipUser membershipUser = Membership.GetUser();

            TimeSpan ts = DateTime.Today - membershipUser.LastPasswordChangedDate;

            // If true, that means the user's password expired
            // Let's force him to change his password before using the application
if (ts.TotalDays > PasswordExpiresInDays) { filterContext.HttpContext.Response.Redirect( string.Format("~/{0}/{1}?{2}", MVC.Account.Name, MVC.Account.ActionNames.ChangePassword, "reason=expired")); } } base.OnAuthorization(filterContext); } }

As you see, the code goes inside the OnAuthorization overridable method. I get the PasswordExpiresInDays setting from the Web.config <appSettings> section. This gives an easy way to change the requirement in the future without the need of recompiling the whole app.

<appSettings>
    <add key="PasswordExpiresInDays" value="45" />
</appSettings>

The code explains itself but let’s go through it:

1 - If the User is authenticated, let’s get his membership data;
2 - A TimeSpan is useful for getting the difference in days between Today and the last time the user changed his password ( LastPasswordChangedDate )
3 - Check if the TimeSpan.TotalDays is greater than the PasswordExpiresInDays setting we got from the Web.config file. If true the user must change his password and we redirect him to the ChangePassword view.

Note 1: I’m using T4MVC to retrieve the Controller and Action names in the code above. You should take a look at it! Really…

Note 2: See that "reason=expired" in the response redirect URL? I’m using this querystring as a route parameter inside the ChangePassword action method to display a message to the user informing him that he’s being asked to change the password because it has expired.

/// <summary>
/// This allows the logged on user to change his password.
/// </summary>
public virtual ActionResult ChangePassword(string reason)
{
    var viewModel = new ChangePasswordViewModel();

    if (reason != null)
    {
ShowMessage(Infrastructure.Notification.MessageType.Warning, Localization.PasswordExpired, true); } return View(viewModel); }

By the way: I use MvcNotification infrastructure by Martijn Boland to display beautiful messages to the user.

OK, getting back to the main point… now it’s just a matter of applying the PasswordExpiredAttribute filter to every controller of the app but the AccountController. With ASP.NET MVC 3 it’s easy to apply a filter to every controller and action using GlobalFilters. Instead of going controller by controller to add this attribute we can just register it as a global filter in the Global.asax file:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
filters.Add(new AuthorizeAttribute());
filters.Add(new PasswordExpiredAttribute()); }

Doing so the PasswordExpiredAttribute will be executed for every controller and action but there’s a problem with the above approach. Since it’s a global filter, it’ll be executed even for the AccountController. Remember: we don’t want it to be executed for the AccontController… How can we exclude a global filter from a single controller or action? To achieve this, there’s an awesome thing we can do: create a ExcludeFilterAttribute and a ExcludeFilterProvider. WOW, ASP.NET MVC has a Filter Provider that gives us even more power when working with filters. Look here for the complete story: Exclude a Filter by Ori Calvo. I’ve uploaded the source code files here: ExcludeFilterAttribute.cs and ExcludeFilterProvider.cs

Now it’s just a matter of decorating the AccountController with the ExcludeFilter attribute like this:

[ExcludeFilter(typeof(PasswordExpiredAttribute))]
public partial class AccountController : BaseController
{
...
}

The ExcludeFilter attribute explicitly tells the ASP.NET MVC runtime to ignore the PasswordExpiredAttribute for the AccountController.

With this in place, once the logged in user tries to access any part of the site and his password is expired, he'll be redirected to the ChangePassword view and won't be allowed access to anywhere else in the site until he changes the password. This is great and the requirement is implemented.

Of course in software there are multiple ways of doing the same thing. If you know of any better option, please share you knowledge in the comments.

Hope it helps.

Bonus
While working on this requirement I posted a question at StackOverflow regarding the use of Web.config settings as magic strings. I’ve found a nice way to let the code a little bit cleaner. So, if you want a nice way to access your Web.config app settings as properties with compile time checking and nice error handling, you can do as described here: T4MVC for Web.config <appSettings>

This is a much better/cleaner code IMO (see the AppSettings class that was automatically generated with the T4 template):

public class PasswordExpiredAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        IPrincipal user = filterContext.HttpContext.User;

        if (user != null && user.Identity.IsAuthenticated)
        {
            MembershipUser membershipUser = Membership.GetUser();

            TimeSpan ts = DateTime.Today - membershipUser.LastPasswordChangedDate;

            // If true, that means the user's password expired
            // Let's force him to change his password before using the system
            if (ts.TotalDays > int.Parse(AppSettings.PasswordExpiresInDays))
            {
                filterContext.HttpContext.Response.Redirect(
                    string.Format("~/{0}/{1}?{2}", MVC.SGAccount.Name, MVC.SGAccount.ActionNames.ChangePassword,
                    "reason=expired"));

            }
        }

        base.OnAuthorization(filterContext);
    }
}

References:
ASP.NET MVC Authentication - Global Authentication and Allow Anonymous by Jon Galloway

ASP.NET MVC Authentication - Customizing Authentication and Authorization The Right Way by Jon Galloway

Exclude a Filter by Ori Calvo

Introducing System.Web.Providers - ASP.NET Universal Providers for Session, Membership, Roles and User Profile on SQL Compact and SQL Azure by Scott Hanselman

Conditional Filters in ASP.NET MVC 3 by Phil Haack

T4MVC by David Ebbo