You may have heard of something called LDAP, or you have been tasked to integrate it into your .NET app. Well, you're in luck, as this article focuses on explaining this technology with practical code examples. What you'll see is how I implemented LDAP authentication in a .NET Web API.
What is LDAP?
Lightweight Directory Access Protocol (LDAP) is a crucial component for managing user accounts, groups, and other directory-related data in your .NET applications.
What does this mean? LDAP is a protocol that allows you to integrate directory structure into your app. Large enterprise companies typically have complex organizational structures. By utilizing LDAP in your application, you can simplify your infrastructure and maintain authentication and authorization data (such as user lists with roles) in one centralized place.
There are many other use cases for LDAP, but this article will focus primarily on authentication. Authorization will be covered in a future article.
Different types of LDAP
You should know there are more "types" of LDAP. For example OpenLDAP or Active Directory (or Azure). This article covers mainly active directory. However it is really simple use example with any other LDAP disctribution. You just need replace cn keywords and the code should work
for example this (Active directory)
"OU=Users,DC=example,DC=com"
may look like
"cn=Users, o=example, c=com"
Basic Authentication Flow with LDAP in .NET
The following example implements JWT authentication with LDAP as the identity provider:
Login Flow
- User enters credentials
- Input is validated (username and password) by calling LDAP
- Refresh token is generated
- JWT token is generated
- Both tokens are returned to the client
- Client can now call any authorized endpoint with the JWT included in the authorization header
This approach allows you to authenticate users against LDAP once and then use stateless JWT tokens for subsequent requests.
Required NuGet Packages
To implement the solution in this article, you'll need the following packages:
Microsoft.AspNetCore.Authentication.JwtBearer
System.DirectoryServices.Protocols
System.IdentityModel.Tokens.Jwt
Implementation in Code
Let's walk through the implementation of LDAP authentication in a .NET 8 Web API application, from user login to JWT token generation.
1. Setting Up the Login Endpoint
First, we need a login endpoint that accepts user credentials:
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly ILdapService _ldapService;
private readonly ITokenService _tokenService;
public AuthController(ILdapService ldapService, ITokenService tokenService)
{
_ldapService = ldapService;
_tokenService = tokenService;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Authentication logic will be implemented here
// ...
}
}
public class LoginRequest
{
[Required]
public string Username { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
}
2. Implementing LDAP Authentication Service
Next, we'll create an LDAP service to verify credentials against the directory:
public interface ILdapService
{
Task<LdapUser?> AuthenticateAsync(string username, string password);
}
public class LdapService : ILdapService
{
private readonly LdapSettings _settings;
private readonly ILogger<LdapService> _logger;
public LdapService(IOptions<LdapSettings> settings, ILogger<LdapService> logger)
{
_settings = settings.Value;
_logger = logger;
}
public async Task<LdapUser?> AuthenticateAsync(string username, string password)
{
// Using System.DirectoryServices.Protocols for modern LDAP access in .NET
using var connection = new LdapConnection(new LdapDirectoryIdentifier(_settings.ServerAddress, _settings.ServerPort));
try
{
// Set connection options
connection.SessionOptions.ProtocolVersion = 3;
connection.SessionOptions.SecureSocketLayer = _settings.UseSsl;
// Determine the user's distinguished name (DN)
string userDn = GetUserDN(username);
// Attempt to bind (authenticate) with the provided credentials
await connection.AuthenticateAsync(
new NetworkCredential(userDn, password));
_logger.LogInformation("User {Username} authenticated successfully via LDAP", username);
// If we get here, authentication was successful
// Retrieve user information, including roles
return await GetUserDetailsAsync(connection, userDn);
}
catch (LdapException ex)
{
_logger.LogWarning(ex, "LDAP authentication failed for user {Username}", username);
return null;
}
}
private string GetUserDN(string username)
{
// Format the user's distinguished name based on your LDAP structure
// Examples:
// - Active Directory: "CN=username,OU=Users,DC=example,DC=com"
// - OpenLDAP: "uid=username,ou=people,dc=example,dc=com"
return $"CN={username},{_settings.BaseDN}";
}
// Implemented in the next section
private Task<LdapUser?> GetUserDetailsAsync(LdapConnection connection, string userDn)
{
// ...
}
}
public class LdapSettings
{
public string ServerAddress { get; set; } = string.Empty;
public int ServerPort { get; set; } = 389; // Default LDAP port
public bool UseSsl { get; set; } = false;
public string BaseDN { get; set; } = string.Empty;
public string SearchBase { get; set; } = string.Empty;
public string SearchFilter { get; set; } = string.Empty;
}
Configure LDAP settings in your appsettings.json
:
{
"LdapSettings": {
"ServerAddress": "ldap.example.com",
"ServerPort": 389,
"UseSsl": false,
"BaseDN": "OU=Users,DC=example,DC=com",
"SearchBase": "OU=Users,DC=example,DC=com",
"SearchFilter": "(&(objectClass=user)(sAMAccountName={0}))"
}
}
Register the LDAP service in Program.cs
:
builder.Services.Configure<LdapSettings>(
builder.Configuration.GetSection("LdapSettings"));
builder.Services.AddScoped<ILdapService, LdapService>();
3. Retrieving User Information and Roles
After authenticating, we need to fetch additional user information, particularly roles:
public class LdapUser
{
public string Username { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public List<string> Roles { get; set; } = new();
public Dictionary<string, string> Attributes { get; set; } = new();
}
// Implementation of GetUserDetailsAsync method in the LdapService class:
private async Task<LdapUser?> GetUserDetailsAsync(LdapConnection connection, string userDn)
{
// Create search request for user information
var searchRequest = new SearchRequest(
_settings.SearchBase,
string.Format(_settings.SearchFilter, Path.GetFileName(userDn)),
System.DirectoryServices.Protocols.SearchScope.Subtree,
// Specify which attributes to return
new string[] { "cn", "mail", "displayName", "memberOf", "givenName", "sn" }
);
try
{
// Execute the search request
var response = (SearchResponse)await connection.SendRequestAsync(searchRequest);
if (response.Entries.Count == 0)
{
_logger.LogWarning("No user found with DN: {UserDN}", userDn);
return null;
}
var searchEntry = response.Entries[0];
// Create and populate user object
var user = new LdapUser
{
Username = Path.GetFileName(userDn),
DisplayName = GetAttributeValue(searchEntry, "displayName") ?? string.Empty,
Email = GetAttributeValue(searchEntry, "mail") ?? string.Empty
};
// Extract roles from group memberships
if (searchEntry.Attributes.Contains("memberOf"))
{
var memberOfAttribute = searchEntry.Attributes["memberOf"];
for (int i = 0; i < memberOfAttribute.Count; i++)
{
string groupDn = memberOfAttribute[i]!.ToString()!;
// Extract the CN part from the DN
string groupName = ExtractCNFromDN(groupDn);
user.Roles.Add(groupName);
}
}
// Store additional attributes for potential use in claims
foreach (string attributeName in searchEntry.Attributes.AttributeNames)
{
var attribute = searchEntry.Attributes[attributeName];
if (attribute.Count > 0 && attribute[0] != null)
{
user.Attributes[attributeName] = attribute[0].ToString()!;
}
}
return user;
}
catch (LdapException ex)
{
_logger.LogError(ex, "Error retrieving user details for {UserDN}", userDn);
return null;
}
}
private string? GetAttributeValue(SearchResultEntry entry, string attributeName)
{
if (entry.Attributes.Contains(attributeName) && entry.Attributes[attributeName].Count > 0)
{
return entry.Attributes[attributeName][0]?.ToString();
}
return null;
}
private string ExtractCNFromDN(string dn)
{
// Extract CN value from a distinguished name
var match = Regex.Match(dn, @"^CN=([^,]+)");
if (match.Success)
{
return match.Groups[1].Value;
}
return dn; // Return the original DN if extraction fails
}
This method extracts user details, including roles from group memberships, which we'll use in our JWT token.
4. JWT Token Generation Service
Now let's create a service for generating and validating JWT tokens:
public interface ITokenService
{
string GenerateJwtToken(LdapUser user);
ClaimsPrincipal? ValidateToken(string token);
}
public class TokenService : ITokenService
{
private readonly JwtSettings _jwtSettings;
public TokenService(IOptions<JwtSettings> jwtSettings)
{
_jwtSettings = jwtSettings.Value;
}
public string GenerateJwtToken(LdapUser user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_jwtSettings.Secret);
// Create claims based on user information
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Email, user.Email)
};
// Add roles as claims
foreach (var role in user.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
// Add relevant additional claims from attributes
foreach (var attr in user.Attributes)
{
// Avoid duplicating existing claims
if (!claims.Any(c => c.Type == attr.Key))
{
claims.Add(new Claim(attr.Key, attr.Value));
}
}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(_jwtSettings.ExpirationMinutes),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public ClaimsPrincipal? ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_jwtSettings.Secret);
try
{
var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
// Set clockskew to zero for more accurate validation
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
return principal;
}
catch
{
return null;
}
}
}
public class JwtSettings
{
public string Secret { get; set; } = string.Empty;
public int ExpirationMinutes { get; set; } = 60;
public int RefreshTokenExpirationDays { get; set; } = 7;
}
Add JWT settings to your appsettings.json
:
{
"JwtSettings": {
"Secret": "your-super-secret-key-with-at-least-32-characters",
"ExpirationMinutes": 60,
"RefreshTokenExpirationDays": 7
}
}
Register the token service in Program.cs
:
builder.Services.Configure<JwtSettings>(
builder.Configuration.GetSection("JwtSettings"));
builder.Services.AddScoped<ITokenService, TokenService>();
5. Completing the Login Endpoint
Now we can complete the login endpoint implementation:
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Authenticate user against LDAP
var user = await _ldapService.AuthenticateAsync(request.Username, request.Password);
if (user == null)
return Unauthorized(new { message = "Invalid username or password" });
// Generate JWT token
var token = _tokenService.GenerateJwtToken(user);
// Generate refresh token for longer sessions
var refreshToken = GenerateRefreshToken();
// In a production application, store the refresh token in a database
// await _tokenRepository.SaveRefreshTokenAsync(user.Username, refreshToken);
return Ok(new
{
token,
refreshToken,
user = new
{
username = user.Username,
displayName = user.DisplayName,
email = user.Email,
roles = user.Roles
}
});
}
private string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
6. Configuring JWT Authentication
Now we need to configure JWT authentication in our application:
// In Program.cs
// Add JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.ASCII.GetBytes(builder.Configuration["JwtSettings:Secret"]!)),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
// Add the authentication middleware in the correct order
var app = builder.Build();
// ... other middleware
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
7. Implementing Refresh Token Functionality
To avoid frequent LDAP calls, let's implement a refresh token endpoint:
[HttpPost("refresh-token")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
if (string.IsNullOrEmpty(request.RefreshToken))
return BadRequest("Refresh token is required");
// In a production application, validate the refresh token from a database
// var storedToken = await _tokenRepository.GetByTokenAsync(request.RefreshToken);
var storedToken = ValidateStoredRefreshToken(request.RefreshToken);
if (storedToken == null)
return Unauthorized(new { message = "Invalid or expired refresh token" });
// Get the user associated with this refresh token
// In a production app, you would retrieve this from a database
var user = await GetUserForRefreshToken(storedToken);
if (user == null)
return Unauthorized(new { message = "User not found" });
// Generate new tokens
var newToken = _tokenService.GenerateJwtToken(user);
var newRefreshToken = GenerateRefreshToken();
// Update the stored refresh token
// await _tokenRepository.UpdateAsync(storedToken.Id, newRefreshToken);
return Ok(new
{
token = newToken,
refreshToken = newRefreshToken
});
}
public class RefreshTokenRequest
{
[Required]
public string RefreshToken { get; set; } = string.Empty;
}
// Example methods for token validation
// In a real application, these would interact with a database
private RefreshTokenInfo? ValidateStoredRefreshToken(string token)
{
// Implementation would validate the token against a database
// and check if it's expired
return null; // Placeholder
}
private async Task<LdapUser?> GetUserForRefreshToken(RefreshTokenInfo tokenInfo)
{
// Implementation would retrieve the user from a database or
// possibly from LDAP directly
return null; // Placeholder
}
public class RefreshTokenInfo
{
public string Id { get; set; } = string.Empty;
public string Token { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
public DateTime ExpiryDate { get; set; }
}
Note: In a production application, you would implement a proper repository for storing and retrieving refresh tokens and user information from a database.
Using Protected Resources
Now that we have authentication in place, we can create protected endpoints:
[ApiController]
[Route("api/[controller]")]
[Authorize] // This ensures the user is authenticated
public class ProtectedController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// Access the authenticated user's identity
var username = User.Identity?.Name;
return Ok(new { message = $"Hello, {username}! This is a protected endpoint." });
}
[HttpGet("admin")]
[Authorize(Roles = "Administrators")] // Role-based access control
public IActionResult GetAdminData()
{
return Ok(new { message = "This is admin-only data" });
}
}
This approach provides several benefits:
- Centralized Identity Management: Leverage your existing directory infrastructure
- Reduced Authentication Overhead: Authenticate once against LDAP, then use lightweight JWT tokens
- Stateless Authentication: No need to maintain server-side session state
- Role-Based Access Control: Easily implement authorization based on directory roles
- Security: Proper implementation of industry-standard security practices
Conclusion
By implementing LDAP, you can integrate your applications with enterprise directory services. The pattern of authenticating once against LDAP and then using JWT tokens for subsequent requests creates a secure, efficient authentication system that works well in modern web applications. I am aware of using JWT is currently discouraged tho. However, with the code example you can simply replace JWT part with cookie or ANY auth approach you want. :)
If you are an unfortunate soul who needs to integrate this into your app. I wish you good luck.
Top comments (0)