Compare commits
2 Commits
14-details
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d1dac1c8f | |||
| bf30f11803 |
@@ -20,12 +20,6 @@ public class AccountController : Controller
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("Register")]
|
|
||||||
public IActionResult Register()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("Logout")]
|
[HttpGet("Logout")]
|
||||||
public async Task<IActionResult> Logout()
|
public async Task<IActionResult> Logout()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using OneForMe.Services;
|
||||||
|
|
||||||
namespace OneForMe.Controllers;
|
namespace OneForMe.Controllers;
|
||||||
|
|
||||||
@@ -7,42 +8,78 @@ namespace OneForMe.Controllers;
|
|||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class AuthController : ControllerBase
|
public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly UserManager<IdentityUser> _userManager;
|
private readonly ILdapService _ldapService;
|
||||||
private readonly SignInManager<IdentityUser> _signInManager;
|
private readonly SignInManager<IdentityUser> _signInManager;
|
||||||
|
private readonly UserManager<IdentityUser> _userManager;
|
||||||
|
private readonly ILogger<AuthController> _logger;
|
||||||
|
|
||||||
public AuthController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
|
public AuthController(
|
||||||
|
ILdapService ldapService,
|
||||||
|
SignInManager<IdentityUser> signInManager,
|
||||||
|
UserManager<IdentityUser> userManager,
|
||||||
|
ILogger<AuthController> logger)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_ldapService = ldapService;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a new user
|
/// Login user with LDAP credentials
|
||||||
/// </summary>
|
|
||||||
[HttpPost("register")]
|
|
||||||
public async Task<IActionResult> 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) });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Login user
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
public async Task<IActionResult> Login([FromBody] LoginRequest request)
|
public async Task<IActionResult> 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" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
if (result.Succeeded)
|
|
||||||
return Ok(new { message = "Login successful" });
|
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" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -52,18 +89,13 @@ public class AuthController : ControllerBase
|
|||||||
public async Task<IActionResult> Logout()
|
public async Task<IActionResult> Logout()
|
||||||
{
|
{
|
||||||
await _signInManager.SignOutAsync();
|
await _signInManager.SignOutAsync();
|
||||||
|
_logger.LogInformation("User logged out");
|
||||||
return Ok(new { message = "Logout successful" });
|
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 class LoginRequest
|
||||||
{
|
{
|
||||||
public string Email { get; set; } = string.Empty;
|
public string Username { get; set; } = string.Empty;
|
||||||
public string Password { get; set; } = string.Empty;
|
public string Password { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.0-rc.2.25502.107" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.0-rc.2.25502.107" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0-rc.2.25502.107" />
|
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0-rc.2.25502.107" />
|
||||||
|
<PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="3.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
|
|||||||
// Add LocalizationService
|
// Add LocalizationService
|
||||||
builder.Services.AddScoped<LocalizationService>();
|
builder.Services.AddScoped<LocalizationService>();
|
||||||
|
|
||||||
|
// Add LDAP Service
|
||||||
|
builder.Services.AddScoped<ILdapService, LdapService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Create and migrate database on startup
|
// Create and migrate database on startup
|
||||||
|
|||||||
139
Services/LdapService.cs
Normal file
139
Services/LdapService.cs
Normal file
@@ -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<bool> AuthenticateAsync(string username, string password);
|
||||||
|
Task<LdapUser> GetUserAsync(string username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class LdapService : ILdapService
|
||||||
|
{
|
||||||
|
public Dictionary<string, HashAlgorithm> 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<LdapService> _logger;
|
||||||
|
|
||||||
|
public LdapService(IConfiguration configuration, ILogger<LdapService> logger)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> 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<LdapUser> 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; }
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
|
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">@Localizer["Email"]</label>
|
<label for="username" class="form-label">@Localizer["Username"]</label>
|
||||||
<input type="email" class="form-control" id="email" name="email" required>
|
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -26,10 +26,6 @@
|
|||||||
<button type="submit" class="btn btn-primary w-100">@Localizer["Login"]</button>
|
<button type="submit" class="btn btn-primary w-100">@Localizer["Login"]</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<p class="text-center">@Localizer["DontHaveAccount"] <a asp-controller="Account" asp-action="Register">@Localizer["RegisterHere"]</a></p>
|
|
||||||
|
|
||||||
<div id="message" class="mt-3"></div>
|
<div id="message" class="mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,7 +37,7 @@
|
|||||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const email = document.getElementById('email').value;
|
const username = document.getElementById('username').value;
|
||||||
const password = document.getElementById('password').value;
|
const password = document.getElementById('password').value;
|
||||||
const messageDiv = document.getElementById('message');
|
const messageDiv = document.getElementById('message');
|
||||||
|
|
||||||
@@ -51,7 +47,7 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email, password })
|
body: JSON.stringify({ username, password })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|||||||
@@ -9,5 +9,20 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user