1

I wanted to implement forms authentication with membership in my asp.net MVC Core application. We had forms authentication setup in our previous application as below and wanted to use the same in .net core.

  [HttpPost]
public ActionResult Login(LoginModel model, string returnUrl)
{
  if (!this.ModelState.IsValid)
  {
      return this.View(model);
   }

   //Authenticate
   if (!Membership.ValidateUser(model.UserName, model.Password))
   {
       this.ModelState.AddModelError(string.Empty, "The user name or 
   password provided is incorrect.");
   return this.View(model);
   }
   else
   {
       FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
        return this.RedirectToAction("Index", "Home");
   }
 return this.View(model);
   }

In my config:

  <membership defaultProvider="ADMembership">
       <providers>
         <add name="ADMembership" 
           type="System.Web.Security.ActiveDirectoryMembershipProvider" 
           connectionStringName="ADConnectionString" 
           attributeMapUsername="sAMAccountName" />
       </providers>
   </membership>

So we are using active directory here in membership.

Is this still applicable in .net core.

If not what else is available in .net core for forms authentication and AD.

Would appreciate inputs.

aman
  • 4,198
  • 4
  • 24
  • 36

3 Answers3

2

Yes you can do that in Core MVC application. You enable form authentication and use LDAP as user store at the back-end.

Here is how I set things up, to give you start:

Startup.cs

public class Startup
{
    ...
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        // Read LDAP settings from appsettings
        services.Configure<LdapConfig>(this.Configuration.GetSection("ldap"));

        // Define an interface for authentication service,
        // We used Novell.Directory.Ldap as implementation.
        services.AddScoped<IAuthenticationService, LdapAuthenticationService>();

        // Global filter is enabled to protect the whole site
        services.AddMvc(config =>
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            config.Filters.Add(new AuthorizeFilter(policy));
            ...
        });

        // Form authentication and cookies settings
        var cookiesConfig = this.Configuration.GetSection("cookies").Get<CookiesConfig>();
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options =>
        {
            options.Cookie.Name = cookiesConfig.CookieName;
            options.LoginPath = cookiesConfig.LoginPath;
            options.LogoutPath = cookiesConfig.LogoutPath;
            options.AccessDeniedPath = cookiesConfig.AccessDeniedPath;
            options.ReturnUrlParameter = cookiesConfig.ReturnUrlParameter;
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // Redirects all HTTP requests to HTTPS
        if (env.IsProduction())
        {
            app.UseRewriter(new RewriteOptions()
                .AddRedirectToHttpsPermanent());
        }

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
        }

        app.UseStaticFiles();

        app.UseStatusCodePagesWithReExecute("/error", "?code={0}");

        app.UseAuthentication();

        app.UseMvc(routes =>
        {
            ...
        });
    }
}

appsettings.json

{
  "connectionStrings": {
    "appDbConnection": xxx
  },
  "ldap": {
    "url": "xxx.loc",
    "bindDn": "CN=Users,DC=xxx,DC=loc",
    "username": "xxx",
    "password": "xxx",
    "searchBase": "DC=xxx,DC=loc",
    "searchFilter": "(&(objectClass=user)(objectClass=person)(sAMAccountName={0}))"
  },
  "cookies": {
    "cookieName": "xxx",
    "loginPath": "/account/login",
    "logoutPath": "/account/logout",
    "accessDeniedPath": "/account/accessDenied",
    "returnUrlParameter": "returnUrl"
  }
}

IAuthenticationService.cs

namespace DL.SO.Services.Core
{
    public interface IAuthenticationService
    {
        IAppUser Login(string username, string password);
    }
}

LdapAuthenticationService.cs

Ldap implementation of authentication service, using Novell.Directory.Ldap library to talk to active directory. You can Nuget that library.

using Microsoft.Extensions.Options;
using Novell.Directory.Ldap;
...
using DL.SO.Services.Core;

namespace DL.SO.Services.Security.Ldap
{
    public class LdapAuthenticationService : IAuthenticationService
    {
        private const string MemberOfAttribute = "memberOf";
        private const string DisplayNameAttribute = "displayName";
        private const string SAMAccountNameAttribute = "sAMAccountName";
        private const string MailAttribute = "mail";

        private readonly LdapConfig _config;
        private readonly LdapConnection _connection;

        public LdapAuthenticationService(IOptions<LdapConfig> configAccessor)
        {
            // Config from appsettings, injected through the pipeline
            _config = configAccessor.Value;
            _connection = new LdapConnection();
        }

        public IAppUser Login(string username, string password)
        {
            _connection.Connect(_config.Url, LdapConnection.DEFAULT_PORT);
            _connection.Bind(_config.Username, _config.Password);

            var searchFilter = String.Format(_config.SearchFilter, username);
            var result = _connection.Search(_config.SearchBase, LdapConnection.SCOPE_SUB, searchFilter,
            new[] { MemberOfAttribute, DisplayNameAttribute, SAMAccountNameAttribute, MailAttribute }, false);

            try
            {
                var user = result.next();
                if (user != null)
                {
                    _connection.Bind(user.DN, password);
                    if (_connection.Bound)
                    {
                        var accountNameAttr = user.getAttribute(SAMAccountNameAttribute);
                        if (accountNameAttr == null)
                        {
                            throw new Exception("Your account is missing the account name.");
                        }

                        var displayNameAttr = user.getAttribute(DisplayNameAttribute);
                        if (displayNameAttr == null)
                        {
                            throw new Exception("Your account is missing the display name.");
                        }

                        var emailAttr = user.getAttribute(MailAttribute);
                        if (emailAttr == null)
                        {
                            throw new Exception("Your account is missing an email.");
                        }

                        var memberAttr = user.getAttribute(MemberOfAttribute);
                        if (memberAttr == null)
                        {
                            throw new Exception("Your account is missing roles.");
                        }

                        return new AppUser
                        {
                            DisplayName = displayNameAttr.StringValue,
                            Username = accountNameAttr.StringValue,
                            Email = emailAttr.StringValue,
                            Roles = memberAttr.StringValueArray
                                .Select(x => GetGroup(x))
                                .Where(x => x != null)
                                .Distinct()
                                .ToArray()
                        };
                    }
                }
            }
            finally
            {
                _connection.Disconnect();
            }

            return null;
        }
    }
}

AccountController.cs

Then finally after the user is verified, you need to construct the principal from the user claims for sign in process, which would generate the cookie behind the scene.

public class AccountController : Controller
{
    private readonly IAuthenticationService _authService;

    public AccountController(IAuthenticationService authService)
    {
        _authService = authService;
    }

    ...
    [HttpPost]
    [AllowAnonymous]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (ModelState.Valid)
        {
            try
            {
                var user = _authService.Login(model.Username, model.Password);
                if (user != null)
                {
                    var claims = new List<Claim>
                    {
                        new Claim(ClaimTypes.Name, user.Username),
                        new Claim(CustomClaimTypes.DisplayName, user.DisplayName),
                        new Claim(ClaimTypes.Email, user.Email)
                    }

                    // Roles
                    foreach (var role in user.Roles)
                    {
                        claims.Add(new Claim(ClaimTypes.Role, role));
                    }

                    // Construct Principal
                    var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, _authService.GetType().Name));

                    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, 
                        principal,
                        new AuthenticationProperties
                        {
                            IsPersistent = model.RememberMe
                        }
                    );

                    return Redirect(Url.IsLocalUrl(model.ReturnUrl)
                        ? model.ReturnUrl
                        : "/");              
                }

                ModelState.AddModelError("", @"Your username or password is incorrect.");
            }
            catch(Exception ex)
            {
                ModelState.AddModelError("", ex.Message);
            }
        }
        return View(model);
    }
}
David Liang
  • 20,385
  • 6
  • 44
  • 70
  • Thanks David. I would try to integrate Novel AD and implement this solution. Will let you know how it goes. – aman Oct 20 '17 at 18:09
  • I started to use your code above and got the novel from nuget. When trying to create a ldap connection like below: I get an error conn.Bind(username, password); Error: 80090308: LdapErr: DSID-0C09042F, comment: AcceptSecurityContext error, data 52e, v2580 I have the host as: xx.com:389 and to get it working I have to pass the domain name as : conn.Bind(ldapVersion, "domainname\\" + username, password); Why do I have to hardcode domain name – aman Oct 24 '17 at 16:50
  • Interesting. That I don't know. I didn't have to pass the domain name with the username. Please refer to their documentation [https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard] for more configurations. And to test the AD setup, I downloaded the Active Directory Explorer too [https://learn.microsoft.com/en-us/sysinternals/downloads/adexplorer] so that you can verify your username and password setup. – David Liang Oct 24 '17 at 17:17
  • OK. One more question. Though I can use this function conn.Bind(ldapVersion, "domainname\\" + username, password); this seems to be pretty slow. Takes about 10 -12 seconds to connect. Is this the expected behaviour – aman Oct 24 '17 at 18:45
  • No, that's weird. Mine runs fast. Now I remember we do run into similar issue before with an old ASP.NET MVC project. We created `ActiveDirectoryRoleProvider` which inherits from `RoleProvider` and the AD was in Server 2003. That method took 7 secs to finish. We couldn't figure out the reason. Later we moved the AD to Server 2008 and used new method to talk to AD. Since then it only takes couple ms to complete. It might have sth to do with your AD server, or even the machine you run your code on. Maybe that if your machine is already joined to the domain the code will run faster? I don't know. – David Liang Oct 24 '17 at 19:30
  • Thanks I will look into it. Now wheneevr I pass wrong credentials it fails and throws "Invalid Credentials' But when I pass correct ones it connects fine but while disconnection it says PlatformNotSupportedException: Thread abort is not supported on this platform. – aman Oct 24 '17 at 19:38
  • hmmmm never seen that exception before. Need to look into Novell documentation for that. In the future there might be more AD libraries portable with Core 2.0. You might want to try those implementations too. – David Liang Oct 24 '17 at 20:45
0

Would this post help you integrate with AD for Authentication and Authorization? MVC Core How to force / set global authorization for all actions?

The idea is add authentication within ConfigureServices method in Startup.cs file:

services.AddMvc(config =>
{
    var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .RequireRole([Your AD security group name in here without domain name]) // This line adds authorization to users in the AD group only
                     .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});
Xiao Han
  • 1,004
  • 7
  • 16
  • 33
-1

In Asp.Net Core the Authentication is controlled through project properties.

Open the solution. Right click on the Project and Click Properties. Click the Debug tab. Check the Enable Windows Authentication checkbox. Ensure Anonymous Authentication is disabled.

Here is Microsoft's document, https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth

Cheers!

Jared
  • 394
  • 4
  • 15
  • I am not looking for windows authentication but forms authentication where I would authenticate via AD – aman Oct 20 '17 at 18:09