There are 2 solutions to your problem. Pick whichever one you like.
Solution 1:
You use LDAP to authenticate users, but use Identity to store roles, claims etc. and authorize users that way.
If that's the case, you can simply override CheckPasswordAsync
method to check password against some LDAP server such as Active Directory.
Take a look at this answer, it does exactly that:
https://stackoverflow.com/a/74734478/8644294
If you decide to go with Solution 2, the easy way is to start from Solution 1(as it has a complete example project) and remove Identity from there and just implement Cookie authentication. Read on, as this will be clear afterwards.
Solution 2:
You use LDAP to authenticate and authorize users without an Identity database. In this case, you're looking at Cookie authentication.
For that, start a new app, do not choose any authentication. And follow this guide:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-7.0
You don't need to add any Controllers. Just create a Razor page for eg: Login.cshtml.
For eg:
@page
@model LoginModel
@{
ViewData["Title"] = "Log in";
}
<h1>@ViewData["Title"]</h1>
<div class="row">
<div class="col-md-4">
<section>
<form id="account" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-floating">
<input asp-for="Input.Username" class="form-control" autocomplete="username" aria-required="true" />
<label asp-for="Input.Username" class="form-label"></label>
<span asp-validation-for="Input.Username" class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
<label asp-for="Input.Password" class="form-label"></label>
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div>
<div class="checkbox">
<label asp-for="Input.RememberMe" class="form-label">
<input class="form-check-input" asp-for="Input.RememberMe" />
@Html.DisplayNameFor(m => m.Input.RememberMe)
</label>
</div>
</div>
<div>
<button id="login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
</form>
</section>
</div>
</div>
And implement login in the code behind:
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.DirectoryServices.AccountManagement;
public class LoginModel : PageModel
{
private readonly ILogger<LoginModel> _logger;
public LoginModel(ILogger<LoginModel> logger)
{
_logger = logger;
}
[BindProperty]
public InputModel Input { get; set; }
public string ReturnUrl { get; set; }
[TempData]
public string ErrorMessage { get; set; }
public class InputModel
{
[Required]
[Display(Name = "User name")]
public string Username { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
public async Task OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
ModelState.AddModelError(string.Empty, ErrorMessage);
}
returnUrl ??= Url.Content("~/");
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
if (ModelState.IsValid)
{
// Write your logic on how to sign in using LDAP here.
// For an example, I'm using Active Directory as LDAP server here.
using PrincipalContext principalContext = new(ContextType.Domain);
bool adSignOnResult = principalContext.ValidateCredentials(Input.Username.ToUpper(), Input.Password);
if (!adSignOnResult)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
// If LDAP login is successful:
var roles = // Write logic to grab roles of this user from LDAP server such as Active directory. Homework for you!
var claims = new List<Claim>();
foreach (var role in roles)
{
var claim = new claim(ClaimTypes.Role, role);
claims.add(claim);
}
// Populate other claims
claims.Add(new Claim(ClaimTypes.Name, Input.Username));
// For eg: If it's an employee, add that claim
claims.Add(new Claim("EmployeeNumber", "PutEmployeeNumberYouGotFromLDAPServerHere"));
// Create claims identity:
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
// Create claims principal
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
// Now signin this claimsPrincipal:
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
claimsPrincipal,
new AuthenticationProperties()
{
isPersistent = Input.RememberMe
});
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
// If we got this far, something failed, redisplay form
return Page();
}
}
Similarly create Logout.cshtml
following guide at Microsoft Learn. It's very straightforward.
Now to use this, go to Program.cs
and create policies with the claims you added during login:
For eg:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber")); // For eg: EmployeeNumber was the claim you added earlier.
});
For more info, take a look at this:
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/claims?view=aspnetcore-7.0
Now use them in your Blazor components:
@attribute [Authorize(Policy = "EmployeeOnly")]
But how would it look when I want to use other source of user data
storage (in my example LDAP)?
Even though I haven't done this yet, this should be straightforward. Just hit the LDAP server to get the modifyTimestamp
of the User and compare that with the claims you added while you created claimsIdentity
during Login.
For eg: Taking snippet from LoginModel.OnPostAsync
:
// Populate other claims
claims.Add(new Claim(ClaimTypes.Name, username));
claims.Add(new Claim("EmployeeNumber", "PutEmployeeNumberYouGotFromLDAPServerHere"));
// HERE grab the modifyTimestamp of this user from LDAP server to add it to the claims
claims.Add(new Claim("ModifyTimestamp", "PutModifyTimestampYouGotFromLDAPServerHere"));
Now, in your RevalidatingIdentityAuthenticationStateProvider
, you can use this claim to revalidate the user:
var principalModifyTimeStamp = authenticationState.User.FindFirstValue("ModifyTimestamp"); // ModifyTimestamp is the claim you added earlier during Login
var userModifyTimeStamp = // Get modifyTimestamp from LDAP server here
return principalModifyTimeStamp == userModifyTimeStamp; //Check if they're equal
More info about this class here.
You don't even have to use RevalidatingIdentityAuthenticationStateProvider
. You can just use ValidatePrincipal
event instead. Checkout this example on how to implement it at Microsoft Learn.