3

I have a question and simple request. I am trying to create simple authentication and authorization mechanism in blazor. Problem is base for that mechanism is not database but ldap (all examples and tutorial are based on database storage).

For what I understand now this in blazor it looks like this

In startup.cs I am adding default entity and storage (I have written a simple library based on novell LDAP library to get credentials to check if user exists in LDAP and get user group).

Using database it would look like (create default identity and setup storage)

// replace this with LDAP account validation
services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
            .AddEntityFrameworkStores<ApplicationDbContext>()

I know It would be possible using controller with routing to do this, but I wonder is there a more elegant way then adding a controller to blazor server app.

next I add revalidate to check user every single period of time:

services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();

and I add authorization and authentication to app:

app.UseAuthentication();
app.UseAuthorization();

But how would it look when I want to use other source of user data storage (in my example LDAP)?

Wojciech Szabowicz
  • 3,646
  • 5
  • 43
  • 87
  • I don't know much about Blazor but I found this post : [Blazor server AD/LDAP integration](https://elefantnet.dk/blazor-server-ad-ldap-integration/), which looks promising. – EricLavault Nov 23 '21 at 15:10

1 Answers1

0

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.

Ash K
  • 1,802
  • 17
  • 44