Tuesday, April 3, 2018

[C#] An fast way to implement Active Directory login to ASP.NET MVC

Although OWIN have a package "Microsoft.Owin.Security.ActiveDirectory" seems can support AD login, but it looks like made for ADFS, and leak document on MSDN so I dunno how to use it on basic AD login, so I decide find other way to implement AD login to MVC.

After long long time search, this post "OWIN with LDAP Authentication" help me a lot, my solution basic on it and add some other feature.



App_Start\IdentityConfig.cs, add in ApplicationSignInManager

//user name without @domain
public async Task CheckActiveDirectoryPassword(string user, string password)
{
    var domain = ConfigurationManager.AppSettings["LdapDomain"];     //ex: died.tw
    var dn = ConfigurationManager.AppSettings["LdapDn"];             //ex: DC=died,DC=tw
    var suffix = ConfigurationManager.AppSettings["LdapPrincipal"];  //ex: @died.tw
    PrincipalContext dc = new PrincipalContext(ContextType.Domain, domain, dn, user+suffix, password);
            
    return await Task.Run(() =>
    {
        bool authenticated = dc.ValidateCredentials(user, password);
        if (!authenticated) return new InnerResult {Success = false};
        //get user info in AD if auth success
        var info = UserPrincipal.FindByIdentity(dc, IdentityType.SamAccountName, $"{domain}\\{user}");  
        return new InnerResult {Success = true, Data = info};
    });
}

Controllers\AccountController.cs, rewrite login.

// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }            

    var result = await SignInManager.CheckActiveDirectoryPassword(model.Name, model.Password);
    if (result.Success)
    {
        // add user info
        UserPrincipal info = result.Data;
        var claims = new List
        {
            new Claim(ClaimTypes.Sid, info.Sid.ToString()),
            new Claim(ClaimTypes.X500DistinguishedName, info.DistinguishedName),
            new Claim(ClaimTypes.GivenName, info.GivenName),
            new Claim(ClaimTypes.Surname, info.Surname)
        };
        
        // if need trust relationshop, check this https://support.microsoft.com/en-us/help/2771040/the-trust-relationship-between-this-workstation-and-the-primary-domain
        /*
        foreach (var group in Request.LogonUserIdentity.Groups)
        {
            string role = new SecurityIdentifier(group.Value).Translate(typeof(NTAccount)).Value;
            string clean = role.Substring(role.IndexOf("\\", StringComparison.Ordinal) + 1, role.Length - (role.IndexOf("\\", StringComparison.Ordinal) + 1));
            claims.Add(new Claim(ClaimTypes.Role, clean));
        }*/
        
        // if want to implement with membership, add info in ApplicationUser then login, and add custom user claims in IdentityModels.cs
        /*
        ApplicationUser user = new ApplicationUser {UserName = model.Name,Email = info.UserPrincipalName };
        SignInManager.SignIn(user, false, true);*/

        // didn't use membership
        ClaimsIdentity ci = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
        AuthenticationManager.SignIn(new AuthenticationProperties
        {
            AllowRefresh = true,
            IsPersistent = false,
            ExpiresUtc = DateTime.UtcNow.AddDays(7)
        }, ci);

        return RedirectToLocal(returnUrl);
    }
    ModelState.AddModelError("", "Invalid login credentials.");
    return View(model);
}


We add some information into user identity, now we need write some extension to easy get those info.


public static class UserExtension
{
    public static string GetSid(this IPrincipal user)
    {
        var claim = ((ClaimsIdentity)user.Identity).FindFirst(ClaimTypes.Sid);
        return claim?.Value;
    }
    public static string GetX500DistinguishedName(this IPrincipal user)
    {
        var claim = ((ClaimsIdentity)user.Identity).FindFirst(ClaimTypes.X500DistinguishedName);
        return claim?.Value;
    }
    public static string GetGivenName(this IPrincipal user)
    {
        var claim = ((ClaimsIdentity)user.Identity).FindFirst(ClaimTypes.GivenName);
        return claim?.Value;
    }
    public static string GetSurname(this IPrincipal user)
    {
            var claim = ((ClaimsIdentity)user.Identity).FindFirst(ClaimTypes.Surname);
        return claim?.Value;
    }
}
//custom class for return value
public class InnerResult
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public int Code { get; set; }
    public dynamic Data { get; set; }
}

That's all, here has a little more info, if you meet Anti-forgery token error after those AD login modify , try add this line to global.cs

AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;

You can check this post "Anti-forgery token issue (MVC 5)" to get more detail.

No comments:

Post a Comment