LDAP Authentication in ASP.NET Core
Prior to the Windows Compatibility Pack in .NET Core, I have been using the library from Novell for LDAP authentication. Now, we can use the familiar DirectoryEntry class that we have been using in .NET framework and this simplifies our code to do LDAP authentication.
The first thing that we have to do is to add the NuGet package – Microsoft.Windows.Compatibility:
Let’s define our configuration to query our Active Directory:
public class LdapConfig { public string Path { get; set; } public string UserDomainName { get; set; } }
In our appsettings.json, we add an Ldap section, which contains the path and userDomainName values:
{ "Ldap": { "Path": "<<LDAP Path>>", "UserDomainName": "<<Domain Name>>" } }
We will use the Options pattern to retrieve our Ldap configuration. In order for us to bind to our LdapConfig, call the Configure method in the ConfigureServices method of our Startup.cs file:
public void ConfigureServices(IServiceCollection services) { // read LDAP Configuration services.Configure<LdapConfig>(Configuration.GetSection("Ldap")); }
Now, let’s define an interface for our authentication service. This interface is needed for us to use dependency injection later on when we need our authentication service:
public interface IAuthenticationService { User Login(string userName, string password); }
Here, we reference a User data model, which may be defined as follows (depending on what data is available for your User model):
public class User { public string UserName { get; set; } public string DisplayName { get; set; } // other properties }
Now, let’s implement our LdapAuthenticationService:
public class LdapAuthenticationService : IAuthenticationService { private const string DisplayNameAttribute = "DisplayName"; private const string SAMAccountNameAttribute = "SAMAccountName"; private readonly LdapConfig config; public LdapAuthenticationService(IOptions<LdapConfig> config) { this.config = config.Value; } public User Login(string userName, string password) { try { using (DirectoryEntry entry = new DirectoryEntry(config.Path, config.UserDomainName + "\\" + userName, password)) { using (DirectorySearcher searcher = new DirectorySearcher(entry)) { searcher.Filter = String.Format("({0}={1})", SAMAccountNameAttribute, userName); searcher.PropertiesToLoad.Add(DisplayNameAttribute); searcher.PropertiesToLoad.Add(SAMAccountNameAttribute); var result = searcher.FindOne(); if (result != null) { var displayName = result.Properties[DisplayNameAttribute]; var samAccountName = result.Properties[SAMAccountNameAttribute]; return new User { DisplayName = displayName == null || displayName.Count <= 0 ? null : displayName[0].ToString(), UserName = samAccountName == null || samAccountName.Count <= 0 ? null : samAccountName[0].ToString() }; } } } } catch (Exception ex) { // if we get an error, it means we have a login failure. // Log specific exception } return null; } }
Here, we simply use the DirectoryEntry to login to the specified Active Directory. If there is no exception, then the user is authenticated. We use the DirectorySearcher to look into the properties of the user. We specify only the properties that we want to inspect and add them to the PropertiesToLoad property of the DirectorySearcher object.
Let’s register our LdapAuthentication service for dependency injection. Add the following in the ConfigureServices method of your Startup.cs:
services.AddScoped<IAuthenticationService, LdapAuthenticationService>();
This allows us to get the appropriate service when we inject IAuthenticationService in our controller class. For example, in our SecurityController:
[Route("api/[controller]/[action]")] public class SecurityController : Controller { private readonly IAuthenticationService authService; public SecurityController(IAuthenticationService authService) { this.authService = authService; } [HttpPost] public async Task<IActionResult> Login(string userName, string password) { var user = authService.Login(userName, password); if (null != user) { // create your login token here } else { return Unauthorized(); } } }
To use in our Angular client, we add a login method in an appropriate service (SecurityService):
import qs from 'qs'; import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from "rxjs"; import { User } from "../models/user"; @Injectable({ providedIn: 'root' }) export class SecurityService { constructor(private http: HttpClient) { } public login(userName: string, password: string): Observable<User> { let httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' }) }; return this.http.post<User>( this.baseUrl + '/security/login', qs.stringify({ userName: userName, password: password }), httpOptions ); } }
Then, in our login component, we invoke the login method of our service when the user clicks the login button:
import { Component, OnInit } from '@angular/core'; import { NgForm } from '@angular/forms'; import { SecurityService } from '../../services/security.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) export class LoginComponent implements OnInit { userName: string; password: string; constructor(private securityService: SecurityService) { } ngOnInit() { } login(loginForm: NgForm): void { this.securityService.login(this.userName, this.password) .subscribe( _ => { // redirect to home page }, error => { // handle the error here... } ); } }