From bf30f11803ac311422b810ecf06c548edeaf5393 Mon Sep 17 00:00:00 2001 From: LD-Reborn Date: Fri, 9 Jan 2026 13:58:22 +0100 Subject: [PATCH] Added LDAP login, removed registration option --- Controllers/AccountController.cs | 6 -- Controllers/AuthController.cs | 92 +++++++++++++------- OneForMe.csproj | 1 + Program.cs | 3 + Services/LdapService.cs | 139 +++++++++++++++++++++++++++++++ Views/Account/Login.cshtml | 12 +-- appsettings.json | 17 +++- 7 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 Services/LdapService.cs diff --git a/Controllers/AccountController.cs b/Controllers/AccountController.cs index edda49b..96ec8d3 100644 --- a/Controllers/AccountController.cs +++ b/Controllers/AccountController.cs @@ -20,12 +20,6 @@ public class AccountController : Controller return View(); } - [HttpGet("Register")] - public IActionResult Register() - { - return View(); - } - [HttpGet("Logout")] public async Task Logout() { diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs index f4ce3d6..e12ba91 100644 --- a/Controllers/AuthController.cs +++ b/Controllers/AuthController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using OneForMe.Services; namespace OneForMe.Controllers; @@ -7,42 +8,78 @@ namespace OneForMe.Controllers; [Route("api/[controller]")] public class AuthController : ControllerBase { - private readonly UserManager _userManager; + private readonly ILdapService _ldapService; private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; - public AuthController(UserManager userManager, SignInManager signInManager) + public AuthController( + ILdapService ldapService, + SignInManager signInManager, + UserManager userManager, + ILogger logger) { - _userManager = userManager; + _ldapService = ldapService; _signInManager = signInManager; + _userManager = userManager; + _logger = logger; } /// - /// Register a new user - /// - [HttpPost("register")] - public async Task Register([FromBody] RegisterRequest request) - { - var user = new IdentityUser { UserName = request.Email, Email = request.Email }; - var result = await _userManager.CreateAsync(user, request.Password); - - if (result.Succeeded) - return Ok(new { message = "User registered successfully" }); - - return BadRequest(new { errors = result.Errors.Select(e => e.Description) }); - } - - /// - /// Login user + /// Login user with LDAP credentials /// [HttpPost("login")] public async Task Login([FromBody] LoginRequest request) { - var result = await _signInManager.PasswordSignInAsync(request.Email, request.Password, false, false); + try + { + // Authenticate with LDAP + var isAuthenticated = await _ldapService.AuthenticateAsync(request.Username, request.Password); + + if (!isAuthenticated) + { + _logger.LogWarning($"LDAP authentication failed for user {request.Username}"); + return Unauthorized(new { message = "Invalid username or password" }); + } - if (result.Succeeded) + // Get user details from LDAP + var ldapUser = await _ldapService.GetUserAsync(request.Username); + if (ldapUser == null) + { + _logger.LogWarning($"Could not retrieve LDAP user details for {request.Username}"); + return Unauthorized(new { message = "Could not retrieve user details" }); + } + + // Check if user exists in database, if not create them + var user = await _userManager.FindByNameAsync(request.Username); + if (user == null) + { + user = new IdentityUser + { + UserName = request.Username, + Email = ldapUser.Email, + EmailConfirmed = true + }; + var result = await _userManager.CreateAsync(user); + if (!result.Succeeded) + { + _logger.LogError($"Failed to create user {request.Username} in database"); + return BadRequest(new { message = "Failed to create user account" }); + } + _logger.LogInformation($"Created new user {request.Username} in database"); + } + + // Sign in the user + await _signInManager.SignInAsync(user, false); + _logger.LogInformation($"User {request.Username} signed in successfully"); + return Ok(new { message = "Login successful" }); - - return Unauthorized(new { message = "Invalid email or password" }); + } + catch (Exception ex) + { + _logger.LogError($"Login error: {ex.Message}"); + return StatusCode(500, new { message = "An error occurred during login" }); + } } /// @@ -52,18 +89,13 @@ public class AuthController : ControllerBase public async Task Logout() { await _signInManager.SignOutAsync(); + _logger.LogInformation("User logged out"); return Ok(new { message = "Logout successful" }); } } -public class RegisterRequest -{ - public string Email { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; -} - public class LoginRequest { - public string Email { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; } \ No newline at end of file diff --git a/OneForMe.csproj b/OneForMe.csproj index dbf48b5..2b48577 100644 --- a/OneForMe.csproj +++ b/OneForMe.csproj @@ -17,6 +17,7 @@ + diff --git a/Program.cs b/Program.cs index 0472b82..02fa2bc 100644 --- a/Program.cs +++ b/Program.cs @@ -54,6 +54,9 @@ builder.Services.AddIdentity(options => // Add LocalizationService builder.Services.AddScoped(); +// Add LDAP Service +builder.Services.AddScoped(); + var app = builder.Build(); // Create and migrate database on startup diff --git a/Services/LdapService.cs b/Services/LdapService.cs new file mode 100644 index 0000000..7d3e6b4 --- /dev/null +++ b/Services/LdapService.cs @@ -0,0 +1,139 @@ +using Novell.Directory.Ldap; +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; + +namespace OneForMe.Services; + +public interface ILdapService +{ + Task AuthenticateAsync(string username, string password); + Task GetUserAsync(string username); +} + +public partial class LdapService : ILdapService +{ + public Dictionary HashAlgorithms => new() { ["MD5"] = MD5.Create(), ["SHA1"] = SHA1.Create(), ["SHA256"] = SHA256.Create(), ["SHA384"] = SHA384.Create(), ["SHA512"] = SHA512.Create(), ["SSHA"] = SHA1.Create(), ["SSHA256"] = SHA256.Create(), ["SSHA384"] = SHA384.Create(), ["SSHA512"] = SHA512.Create()}; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public LdapService(IConfiguration configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + public async Task AuthenticateAsync(string username, string password) + { + try + { + LdapUser user = await GetUserAsync(username); + bool hashMatches = CompareStringToHashed(password, user.Password); + _logger.LogInformation("User {username} authenticated successfully via LDAP", [username]); + return hashMatches; + } + catch (LdapException ex) + { + _logger.LogWarning("LDAP authentication failed for user {username}: {ex.Message}", [username, ex.Message]); + return false; + } + catch (Exception ex) + { + _logger.LogError("LDAP authentication error for user {username}: {ex.Message}", [username, ex.Message]); + return false; + } + } + + public async Task GetUserAsync(string username) + { + try + { + var connection = new LdapConnection(); + var ldapSettings = _configuration.GetSection("LdapSettings"); + var serverName = ldapSettings["ServerName"]; + var port = int.Parse(ldapSettings["Port"] ?? "389"); + var baseDn = ldapSettings["BaseDn"]; + var bindDn = ldapSettings["BindDn"]; + var bindPassword = ldapSettings["BindPassword"]; + + connection.Connect(serverName, port); + connection.Bind(bindDn, bindPassword); + + var searchFilter = $"(uid={username})"; + var results = connection.Search( + baseDn, + LdapConnection.ScopeSub, + searchFilter, + new[] { "uid", "mail", "cn", "sn", "userPassword" }, + false + ); + + LdapUser user = null; + while (results.HasMore()) + { + var entry = results.Next(); + LdapAttributeSet attributeSet = entry.GetAttributeSet(); + attributeSet.TryGetValue("uid", out LdapAttribute? uid); + attributeSet.TryGetValue("mail", out LdapAttribute? mail); + attributeSet.TryGetValue("cn", out LdapAttribute? cn); + attributeSet.TryGetValue("sn", out LdapAttribute? sn); + attributeSet.TryGetValue("userPassword", out LdapAttribute? userPassword); + user = new LdapUser + { + Username = uid?.StringValue ?? username, //entry.GetAttribute("uid")?.StringValue ?? username, + Email = mail?.StringValue ?? "", //entry.GetAttribute("mail")?.StringValue ?? $"{username}@example.com", + FullName = cn?.StringValue ?? "", //entry.GetAttribute("cn")?.StringValue ?? username, + LastName = sn?.StringValue ?? "", //entry.GetAttribute("sn")?.StringValue ?? "", + Password = userPassword?.StringValue ?? "" //entry.GetAttribute("userPassword")?.StringValue ?? "" + }; + } + + connection.Disconnect(); + return user; + } + catch (Exception ex) + { + _logger.LogError($"Error retrieving LDAP user {username}: {ex.Message}"); + return null; + } + } + + public bool CompareStringToHashed(string sourcePassword, string targetPasswordHashed) + { + string algorithmName = CurlyBracesExtractor().Match(targetPasswordHashed).Groups[1].Value; + HashAlgorithm hashAlgorithm = HashAlgorithms[algorithmName.ToUpperInvariant()]; + byte[] sourcePasswordBytes = Encoding.UTF8.GetBytes(sourcePassword); + byte[] targetPasswordHashedBytes = Convert.FromBase64String(CurlyBracesRemover().Replace(targetPasswordHashed, "")); + int hashSize = hashAlgorithm.HashSize / 8; + if (targetPasswordHashedBytes.Length > hashSize) // Has salt + { + int saltLength = targetPasswordHashedBytes.Length - hashSize; + byte[] salt = new byte[saltLength]; + Array.Copy(targetPasswordHashedBytes, hashSize, salt, 0, saltLength); + Array.Resize(ref targetPasswordHashedBytes, hashSize); + byte[] newSourcePasswordBytes = new byte[sourcePasswordBytes.Length + salt.Length]; + Array.Copy(sourcePasswordBytes, 0, newSourcePasswordBytes, 0, sourcePasswordBytes.Length); + Array.Copy(salt, 0, newSourcePasswordBytes, sourcePasswordBytes.Length, salt.Length); + sourcePasswordBytes = newSourcePasswordBytes; + } + sourcePasswordBytes = hashAlgorithm.ComputeHash(sourcePasswordBytes); + return CryptographicOperations.FixedTimeEquals(sourcePasswordBytes, targetPasswordHashedBytes); + } + + [GeneratedRegex(@"\{.*?\}")] + private static partial Regex CurlyBracesRemover(); + [GeneratedRegex(@"\{([^}]*)\}")] + private static partial Regex CurlyBracesExtractor(); +} + +public class LdapUser +{ + public string Username { get; set; } + public string Password { get; set; } + public string Email { get; set; } + public string FullName { get; set; } + public string LastName { get; set; } +} diff --git a/Views/Account/Login.cshtml b/Views/Account/Login.cshtml index 4c7d7d1..7869027 100644 --- a/Views/Account/Login.cshtml +++ b/Views/Account/Login.cshtml @@ -14,8 +14,8 @@
- - + +
@@ -26,10 +26,6 @@ -
- -

@Localizer["DontHaveAccount"] @Localizer["RegisterHere"]

-
@@ -41,7 +37,7 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); - const email = document.getElementById('email').value; + const username = document.getElementById('username').value; const password = document.getElementById('password').value; const messageDiv = document.getElementById('message'); @@ -51,7 +47,7 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => { headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }) + body: JSON.stringify({ username, password }) }); if (response.ok) { diff --git a/appsettings.json b/appsettings.json index 1d529cb..ff688f7 100644 --- a/appsettings.json +++ b/appsettings.json @@ -9,5 +9,20 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:5000" + } + } + }, + "LdapSettings": { + "ServerName": "192.168.0.117", + "Port": "389", + "BaseDn": "dc=ham,dc=ld50,dc=dev", + "UserDnTemplate": "uid={0},ou=users,dc=ham,dc=ld50,dc=dev", + "BindDn": "cn=admin,dc=ham,dc=ld50,dc=dev", + "BindPassword": "TestLDAP" + } } -- 2.34.1