MVC 5, OWIN and Active Directory Authentication

Update: I’ve replaced the Task.Run calls with Task.FromResult to improve performance.

I’ve seen several questions on Stack Overflow asking how to do “forms authentication” using the new MVC OWIN stack. This is actually really easy, and requires only a small amount of customization of the starter template you get when you make a New Project using Individual Accounts. Keep in mind there will be a bit of work required if you want to get around some of the other components of the default project if you don’t want to require users sign up for a new account with a fake password. That is a project for another time.

First things first, this solution uses System.DirectoryServices.AccountManagement, so you will need to add that to your project.

Since we will need a PrincipalContext for the all of this, we can use a request-wide singleton produced by the pipeline for us by editing App_Start\Startup.Auth.cs.

using System.DirectoryServices.AccountManagement;
public void ConfigureAuth(IAppBuilder app) {
// This is the important part, this is how you will configure the PrincipalContext used throughout your app
app.CreatePerOwinContext(() => new PrincipalContext(ContextType.Domain));
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
}

That was easy, right? Next we need to tackle App_Start\IdentityConfig.cs, since that contains the ApplicationUserManager with a lot of methods you can override (thanks abstract!). Seriously, if you ever get a chance, check out the source or MSDN docs for it.

The key function we want to play with is CheckPasswordAsync. We will also need to modify the constructor so we have access to our PrincipalContext. Also, there is the “Create” method we need to tweak to accept our new constructor.

 public class ApplicationUserManager : UserManager<ApplicationUser>
    {
    private readonly PrincipalContext _context ;
    public ApplicationUserManager(IUserStore<ApplicationUser> store, PrincipalContext context)
      : base(store)
      {
        _context = context;
      }

    public override async Task<bool> CheckPasswordAsync(ApplicationUser user, string password)
    {
        return await Task.FromResult(_context.ValidateCredentials(user.UserName, password, ContextOptions.Negotiate));
    }

    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
    {
        var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()), context.Get<PrincipalContext>());
        //...SNIP...    
    }
}

That’s it! We simply override the password checking algorithm and checkthe password against AD. The reason for using ContextOptions.Negotiate is because it is required to get IIS Express to work correctly.

If you wanted to get extra fancy, you could add a property to the ApplicationUser to say whether or not that user should authenticate against AD. Then you could have a password checking method that falls back to the built-in authentication method, or even just fall back if AD authentication fails.

public override async Task<bool> CheckPasswordAsync(ApplicationUser user, string password)
{
    if (user.IsADUser)
    {
        return await Task.FromResult(_context.ValidateCredentials(user.UserName, password, ContextOptions.Negotiate));
    }
    return await base.CheckPasswordAsync(user, password);
}

Happy Coding!

Advertisements

34 thoughts on “MVC 5, OWIN and Active Directory Authentication

  1. Using Task.Run will hurt performance on ASP.NET. Use Task.FromResult to return a completed Task instead.

  2. Thanks. This appears to be exactly what I am looking for; the ability to sign in with AD or fall back to local sign on. However, plugging your code into the default template, a breakpoint in CheckPasswordAsync is never hit. Is there some other config change that needs to be made for the solution to use application user manager?

      • Thanks. I did actually get it to function as expected in the end. Still not sure it is exactly what I need though; as my client is asking for SSO when logged into the domain, and local accounts when not, plus the ability to “switch” user even with the SSO automated login. I dont even think what they are tasking me with is fully possible. 🙂

      • You are absolutely right. However upon asking “do we have ADFS”…the reply was a simple “no. we don’t have that”, so I need to find another way.

        I have currently implemented a method using System.DirectoryServices.AccountManagement whereby a local user is created when a user logs in with a domain account. I have pretty much replicated the way external providers work without an external provider. The user CAN “change user” and log in as a local user, and when they log out it logs back in as the domain account. It appears to work, but needs testing on the deployed environment. If it works I will blog about it, when I get time.

        Thanks.

      • i’m not sure, i can’t get it to work.
        on an existing project i get a login redirection loop.
        on a new project, i get a little bit further but it seems to have a problem with usernames:
        when i type an email, it hits CheckPasswordAsync Method, but since this method checks against ad, it will fail; but when i type a valid ad account it fails in VerifyCode action from AccountController without even hitting CheckPasswordAsync.
        I’m really confused, and my boss is literally behind me.
        Any help is appreciated, thanks

          • sorry for late reply,
            since my last post, i digged around in identity source code, and i i found out that i had to override a couple methods
            FindByNameAsync and maybe another one
            Now PasswordSignInAsync returns success
            but user.Identity is still
            what should i do?

            • i prefer not to create a mirror in the database, is there a way to create the identity object without creating users outside the AD?

                • choices made by my superior and left up to me to deal with. usual stuff you know. 🙂
                  anyway, i stumbled upon a snippet with led me think that overriding GenerateUserIdentityAsync should do the job for me.
                  right now i moved on to something else, i will update as soon as i get back onto it.

  3. I need your advise on the requirement I had

    An intranet web app required user to login using active directory via a form authentication. They can come from different domain e.g. Domain1\user1, Domain2\user2

    I created the sample app using individual account, and implement the same as your suggestion. One thing that I am concern is how do you configure the ldap configuration so that user can point to different domain.

    Thanks
    Thomas

    • Do you have a full trust relationship between the two domains? If so, then it shouldn’t be a problem, since you *should* be able to authenticate with either domain. (Don’t take my word for it though)

      Otherwise, there’s a lot more work to do because the line in ConfigureAuth.cs is “app.CreatePerOwinContext(() => new PrincipalContext(ContextType.Domain));” and that will only bind to a single domain, before any of the auth information has arrived.

      You would have a lot more work to create some kind of wrapper class for PrincipalContext that would contain both contexts you’d need to try to authenticate against, and then have CheckPasswordAsync use that helper class.

  4. I have the same problem as Jeff. Plugging your code into the default template, a breakpoint in CheckPasswordAsync is never hit. In my login controller I’m using the SignInManager.PasswordSignInAsync method. Should I use another method?

    • Find the problem. I was trying to login with a user that do not exists in the system.

      The SignInManager.PasswordSignInAsync never invoke the ApplicationUserManager. CheckPasswordAsync if the user not exists in the user store repository.

  5. Find the problem. I was trying to login with a user that do not exists in the system.

    The SignInManager.PasswordSignInAsync never invoke the ApplicationUserManager. CheckPasswordAsync if the user not exists in the user store repository.

  6. Hi, this is actually a nice share but I got a problem, I keep getting a failure in SignInManager.PasswordSignIn(model.Username, model.Password, false, false);

    Is this code should be called in a PC that connect to the domain?

      • Thanks for the reply, so can you define what is “create my PrincipalContext properly” mean?

        I follow your post here, or am i missing something?

        • I finally found the problem, looks like it was because i entered Domain\LoginName for the username.

          looks like only LoginName is needed for the username (without the domain).

          Thanks Michael Dunlap

  7. Hey. I have a problem with the modifications. I did everything you described in this article but still get error in Login.aspx method LogIn in line :var result = signinManager.PasswordSignIn(Email.Text, Password.Text, RememberMe.Checked, shouldLockout: false);

    It gives me error regarding SQL server connection.

    Is there any other modification that must be made except the ones you described above?

    Thank you!

      • A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 50 – Local Database Runtime error occurred. The specified LocalDB instance does not exist.

    • You shouldn’t have to make any changes in your web.config for the most basic setup. I have since changed jobs and I don’t know when exactly I’d be able to put up a “working” example on Github, especially since making it work requires some modifications depending on your environment.

  8. Hi Michael,
    your code works properly with system Server that connect to AD server. But when I disable the network in the system Server, It got an error. The server could not be contacted. app.CreatePerOwinContext(() => new PrincipalContext(ContextType.Domain));. How can I avoid this problem?

    • This was designed to authenticate against Active Directory. You’ll need something for the PrincipalContext to talk to for authentication. You might be able to use ContextType.Machine to authenticate against local accounts, but I haven’t personally tried that.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s