Blazor Server – Basics Part 6 – Query the on-premise Active Directory
In Part 5 we saw how to authenticate and authorize users in Blazor Server, now in this post we use the user identity from the authenticated user to query our on-premise Active Directory to get more information about the user.
First we need to create a new C# class which will handle accessing our on-premise Active Directory.
In Blazor Server a C# class is called service or dependency and is placed into the Data folder from your project.
Introduction
When using the default project template for a new Blazor Server project, we already have two C# classes (services) included in the Data folder as shown below.
Before these C# classes (services) can be used by our Blazor Server web app, we first need to register these classes in the program.cs file. More about the program.cs file and changes since .NET 6.0 you will find in my following post.
When a Blazor Server web app starts (bootstrapping), the ASP.NET Core runtime is calling the program.cs file and configuring the dependency injection container.
The dependency injection container is creating instances from all registered C# classes (services) and making them available for the web app.
To register a C# class (service) we need to add the following code into our program.cs file. Here we also need to set the lifetime (Singleton, Scoped or Transient) for the service we want to register.
services.AddSingleton<name class>();
Will create a single instance of the service. All components requiring a Singleton service receive the same instance of the service.
services.AddScoped<name class>();
Scoped objects are the same within a request, but different across different requests.
services.AddTransient<name class>();
Whenever a component obtains an instance of a Transient service from the service container, it receives a new instance of the service.
Source: Service lifetime https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-7.0#service-lifetime
So later when our new C# class which will handle accessing our on-premise Active Directory is created, we need to register it in the program.cs file by using the scoped lifetime as shown below. Scoped because we need a new dedicated instance for each separate user which references user specific informations.
builder.Services.AddScoped<BcActiveDirectory();
Creating the C# class (service) for accessing the on-premise Active Directory
So first we add a new class to the Data folder where all C# classes (services) are placed.
I will name it BcActiveDirectory.cs
We will also need to import the System.DirectoryServices namespace.
using System.DirectoryServices;
Before we can use it, we first need to add the System.DirectoryServices package to our project by using the NuGet Package Manager.
So far just the Microsoft.AspNetCore.Authentication.Negotiation package was installed because we enabled authentication when using the project template and wizard to create the Blazor Server web app.
Right click on Packages and select Manage NuGet Packages …
Search for directory, select the System.DirectoryServices and click on Install. I will install the latest stable version as of today.
Now I will add the following code to my new BcActiveDirectory.cs class. You can adjust of course the AD attributes you need for your own web app.
using System.DirectoryServices; namespace BlazorApp1.Data { public class BcActiveDirectory { #region " PROPERTIES " public string Login { get; set; } public string UserPrincipalName { get; set; } // User Impersonation --> set actual logged in user public string UserOfOrigin { get; set; } public string Email { get; set; } public string Firstname { get; set; } public string Name { get; set; } public string BRAIN_NR { get; set; } public string INITIALS { get; set; } public string TELEPHONE { get; set; } public string FAX { get; set; } public string MOBILE { get; set; } public string TITLE { get; set; } public string OFFICE_NAME { get; set; } public string MANAGER_DistinguishedName { get; set; } public string MANAGER { get; set; } public string BIRTHDAY { get; set; } public string STREET { get; set; } public string POSTAL_CODE { get; set; } public string CITY { get; set; } public string BERATER { get; set; } public string GENDER { get; set; } public string START_DATE { get; set; } public string DistinguishedName { get; set; } public byte[] ThumbnailPhoto { get; set; } public string CREATED { get; set; } public string ADServerIP { get; set; } public string TopLevelDomain { get; set; } public string Domain { get; set; } public string OU { get; set; } public DateTime? whenCreated { get; set; } public string ErsterArbeitstag { get; set; } public string ADMINUSER { get; set; } public string ADMINKENNWORT { get; set; } #endregion #region " public methods " public string GetCurrentWindowsUser() { System.Security.Principal.WindowsIdentity Identity; string LoginCredentials; string[] Login; string Domain; Identity = System.Security.Principal.WindowsIdentity.GetCurrent(); LoginCredentials = Identity.Name; Login = LoginCredentials.Split('\'); return Login[1]; } public async void SetADUserInformation(string User) { string UserLogin; System.DirectoryServices.SearchResultCollection results = null; System.DirectoryServices.DirectorySearcher searcher = new(); DirectoryEntry root = GetDirectoryEntry(ADServerIP, OU, TopLevelDomain, Domain, ADMINUSER, ADMINKENNWORT); searcher.SearchRoot = root; try { UserLogin = User; searcher.Filter = "sAMAccountName=" + UserLogin.ToString(); searcher.SearchScope = System.DirectoryServices.SearchScope.Subtree; searcher.PropertiesToLoad.Add("sAMAccountName"); // Login searcher.PropertiesToLoad.Add("userPrincipalName"); // UserPrincipalName searcher.PropertiesToLoad.Add("mail"); // E-Mail Adress searcher.PropertiesToLoad.Add("givenName"); // Firstname searcher.PropertiesToLoad.Add("sn"); // Lastname searcher.PropertiesToLoad.Add("displayName"); // Display Name searcher.PropertiesToLoad.Add("description"); // Brain-Nr searcher.PropertiesToLoad.Add("mobile"); // Mobil-Nr searcher.PropertiesToLoad.Add("facsimileTelephoneNumber"); // Fax-Nr searcher.PropertiesToLoad.Add("TelephoneNumber"); // Phone number searcher.PropertiesToLoad.Add("initials"); // Initialen searcher.PropertiesToLoad.Add("physicalDeliveryOfficeName"); // Office searcher.PropertiesToLoad.Add("extensionAttribute1"); // Birthday searcher.PropertiesToLoad.Add("extensionAttribute2"); // Consultant 0=No 1=Yes searcher.PropertiesToLoad.Add("title"); // Title searcher.PropertiesToLoad.Add("streetaddress"); // Street searcher.PropertiesToLoad.Add("l"); // Location searcher.PropertiesToLoad.Add("postalCode"); // Postal Code searcher.PropertiesToLoad.Add("extensionAttribute3"); // Gender searcher.PropertiesToLoad.Add("extensionAttribute4"); // Start Date BC searcher.PropertiesToLoad.Add("manager"); // Manager searcher.PropertiesToLoad.Add("extensionAttribute6"); // Vacation first Year searcher.PropertiesToLoad.Add("thumbnailPhoto"); // AD Picture searcher.PropertiesToLoad.Add("whenCreated"); // AD Account created searcher.PropertiesToLoad.Add("distinguishedName"); // searcher.PropertiesToLoad.Add("extensionAttribute4"); // First Day at BC Login = null; UserPrincipalName = null; Email = null; Firstname = null; Name = null; BRAIN_NR = null; MOBILE = null; FAX = null; TELEPHONE = null; INITIALS = null; OFFICE_NAME = null; BIRTHDAY = null; TITLE = null; STREET = null; CITY = null; POSTAL_CODE = null; GENDER = null; START_DATE = null; MANAGER_DistinguishedName = null; ThumbnailPhoto = null; DistinguishedName = null; whenCreated = null; results = searcher.FindAll(); if (User != ADMINUSER) { foreach (System.DirectoryServices.SearchResult result in results) { if (!(result.Properties["sAMAccountName"].Count < 1)) { Login = result.Properties["sAMAccountName"][0].ToString(); } if (!(result.Properties["userPrincipalName"].Count < 1)) { UserPrincipalName = result.Properties["userPrincipalName"][0].ToString(); } if (!(result.Properties["mail"].Count < 1)) { Email = result.Properties["mail"][0].ToString(); } if (!(result.Properties["givenName"].Count < 1)) { Firstname = result.Properties["givenName"][0].ToString(); } if (!(result.Properties["sn"].Count < 1)) { Name = result.Properties["sn"][0].ToString(); } if (!(result.Properties["description"].Count < 1)) { BRAIN_NR = result.Properties["description"][0].ToString(); } if (!(result.Properties["mobile"].Count < 1)) { MOBILE = result.Properties["mobile"][0].ToString(); } if (!(result.Properties["facsimileTelephoneNumber"].Count < 1)) { FAX = result.Properties["facsimileTelephoneNumber"][0].ToString(); } if (!(result.Properties["TelephoneNumber"].Count < 1)) { TELEPHONE = result.Properties["TelephoneNumber"][0].ToString(); } if (!(result.Properties["initials"].Count < 1)) { INITIALS = result.Properties["initials"][0].ToString(); } if (!(result.Properties["physicalDeliveryOfficeName"].Count < 1)) { OFFICE_NAME = result.Properties["physicalDeliveryOfficeName"][0].ToString(); } if (!(result.Properties["extensionAttribute1"].Count < 1)) { BIRTHDAY = result.Properties["extensionAttribute1"][0].ToString(); } if (!(result.Properties["extensionAttribute2"].Count < 1)) { BERATER = result.Properties["extensionAttribute2"][0].ToString(); } if (!(result.Properties["title"].Count < 1)) { TITLE = result.Properties["title"][0].ToString(); } if (!(result.Properties["streetaddress"].Count < 1)) { STREET = result.Properties["streetaddress"][0].ToString(); } if (!(result.Properties["l"].Count < 1)) { CITY = result.Properties["l"][0].ToString(); } if (!(result.Properties["postalCode"].Count < 1)) { POSTAL_CODE = result.Properties["postalCode"][0].ToString(); } if (!(result.Properties["extensionAttribute3"].Count < 1)) { GENDER = result.Properties["extensionAttribute3"][0].ToString(); } if (!(result.Properties["extensionAttribute4"].Count < 1)) { START_DATE = result.Properties["extensionAttribute4"][0].ToString(); } if (!(result.Properties["manager"].Count < 1)) { MANAGER_DistinguishedName = result.Properties["manager"][0].ToString(); } //if (!(result.Properties["thumbnailPhoto"].Count < 1)) { ThumbnailPhoto = result.Properties["thumbnailPhoto"][0].ToString(); } if (!(result.Properties["whenCreated"].Count < 1)) { CREATED = result.Properties["whenCreated"][0].ToString(); } if (!(result.Properties["distinguishedName"].Count < 1)) { DistinguishedName = result.Properties["distinguishedName"][0].ToString(); } if (!(result.Properties["whenCreated"].Count < 1)) { whenCreated = Convert.ToDateTime(result.Properties["whenCreated"][0]); } if (!(result.Properties["extensionAttribute4"].Count < 1)) { ErsterArbeitstag = result.Properties["extensionAttribute4"][0].ToString(); } MANAGER = GetManager(MANAGER_DistinguishedName); } } else { foreach (System.DirectoryServices.SearchResult result in results) { if (!(result.Properties["mail"].Count < 1)) { Email = result.Properties["mail"][0].ToString(); } if (!(result.Properties["givenName"].Count < 1)) { Firstname = result.Properties["givenName"][0].ToString(); } } } } catch { } } public DirectoryEntry GetDirectoryEntry(string ADServerIP, string OU, string TopLevelDomain, string Domain, string Adminuser, string Adminkennwort) { DirectoryEntry DirEntry = new DirectoryEntry(); DirEntry.Path = "LDAP://" + ADServerIP + "/OU=" + OU + ";" + "DC=" + Domain + ";DC=" + TopLevelDomain; DirEntry.Username = Domain + "\" + Adminuser; DirEntry.Password = Adminkennwort; return DirEntry; } #endregion // Manager is returning a distinguished string, manipulate this string to get just the name protected string GetManager(string ManagerDistinguishedName) { string Manager = ManagerDistinguishedName; string chars = null; string ManagerDivided = null; int a = 3; int L = Manager.Length; for (int i = 0; i < L; i++) { chars = Manager.Substring(a, 1); a += 1; if (chars != ",") { ManagerDivided += chars; } else { break; } } return ManagerDivided; } } }
Register the new C# Class (service)
In order to use the class I need to register it in the program.cs file as already mentioned further above. Because the class is stored in the Data folder, we also need to import the namespace for this folder by using the following command.
@using BlazorApp1.Data;
BlazorApp1 is the name of the project.
builder.Services.AddScoped<BcActiveDirectory>();
Add services to a Blazor Server app
https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-7.0#add-services-to-a-blazor-server-app
In case you will using a C# class (service) without register it in the program.cs file, you will get an error message like the following.
InvalidOperationException: Cannot provide a value for property …..on type …… . There is no registered service of type …. .
Using the new C# Class (service) in Blazor Server
To finally use the class (service) in our razor components we need to inject it into by using the @inject Razor directive which has two parameters:
- Type: The type of the service to inject.
- Property: The name of the property receiving the injected app service. The property doesn’t require manual creation. The compiler creates the property.
I will use it in the LoginDisplay.razor component which is included on every page on my web app. Here I will add code to authenticate the user and therefore I will use the determined user identity to query my on-premise Active Directory for more information about the user.
By default the LoginDisplay.razor component already using the AuthorizeView component which exposes a context variable of type AuthenticationState (@context in Razor syntax), which you can use to access information about the signed-in user as shown in Part 5 below.
To obtain the user’s ClaimsPrincipal, I will use the Task<AuthenticationState> as described in the following article and recommended, if your app is required to check authorization rules as part of a procedural logic.
Procedural logic
https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-7.0#procedural-logic
I will also check authorization in my web app by checking if the users are member in a specific AD security group as shown below.
In case the user is not a member in the BRAINCOURTbraincontrol2011 security group, it will get forwarded to the not_authorized page.
LoginDisplay.razor component.
@using BlazorApp1.Data @inject BcActiveDirectory BcActiveDirectory @inject NavigationManager NavManager <AuthorizeView> Hello, @context.User.Identity?.Name! </AuthorizeView> @code{ [CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; } string UserName { get; set; } = null; string user { get; set; } = null; protected override async Task OnInitializedAsync() { var user = (await authenticationStateTask).User; UserName = user.Identity.Name; string[] Login; Login = UserName.Split('\'); UserName = Login[1]; if (!user.IsInRole("BRAINCOURT\braincontrol2011")) { // not authorized NavManager.NavigateTo("/not_authorized"); } else { // authorized // Code } } }
As you can see above in the code, I already injected my BcActiveDirectory class (service) and therefore can access it by using the property BcActiveDirectory I named identical as the type (my class). Further you also need to import the namespace from the Data folder where the class is stored.
@using BlazorApp1.Data
@inject BcActiveDirectory BcActiveDirectory
In order to access the on-premise Active Directory, I first need to initialize the parameters used to connect to and needed by the GetDirectoryEntry method shown further down.
Therefore I will use the Task SomeStartupTask() which I will call from the async Task OnInitializedAsync() as shown below.
OnInitializedAsync() is a lifecycle method in Blazor. It is a asynchronous method executed when the component is initialized.
The user just needs read permissions for the Active Directory.
In my BcActiveDirectory class (service), I have a method called SetADUserInformation(UserName) shown below which I will call after initializing the parameters to access the on-premise Active Directory.
The complete code from the BcActiveDirectory class (service) you will see in this post further above.
The method will receive the signed-in user identity as string we determined before by using the cascading parameter of type Task<AuthenticationState> and finally will initialize the AD attributes (here properties) for the given user identity.
Below you can see the GetDirectoryEntry() method which will establish the connection to the on-premise Active Directory and return the DirectoryEntry class.
DirectoryEntry Class
Use this class for binding to objects, or reading and updating attributes. The Active Directory Domain Services hierarchy contains up to several thousand nodes. Each node represents an object, such as a network printer or a user in a domain.
https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.directoryentry?view=dotnet-plat-ext-7.0
public DirectoryEntry GetDirectoryEntry(string ADServerIP, string OU, string TopLevelDomain, string Domain, string Adminuser, string Adminkennwort) { DirectoryEntry DirEntry = new DirectoryEntry(); DirEntry.Path = "LDAP://" + ADServerIP + "/OU=" + OU + ";" + "DC=" + Domain + ";DC=" + TopLevelDomain; DirEntry.Username = Domain + "\" + Adminuser; DirEntry.Password = Adminkennwort; return DirEntry; }
So from now on the Active Directory attributes for the signed-in user are initialized and can be requested from the class.
First I will change the welcome message from the LoginDisplay.razor component to use the initialized user AD attributes as show below.
I will also use further Active Directory user attributes on the footer which is also a razor component named BC_Footer.
Here I have also injected my BcActiveDirectory class (service) by using this time the property name BcAD.
In my web app I have also a feature, where users with admin privileges can impersonate other users, therefore the information displayed in the footer (BC_Footer.razor component) should be refreshed into the impersonated users information.
The information about the user will be stored in properties from the BcActiveDirectory class (service) and be rendered on the web app’s footer when the BC_Footer.razor component is initialized the first time the page is loaded by calling the OnInitializedAsync() task as shown below.
protected override async Task OnInitializedAsync() { // sAMAccountName Username = BcAD.Login; UserPrincipalName = BcAD.UserPrincipalName; EMail = BcAD.Email; Telefon = BcAD.TELEPHONE; Mobil = BcAD.MOBILE; Buero = BcAD.OFFICE_NAME; Firstname = BcAD.Firstname; Name = BcAD.Name; BrainNr = BcAD.BRAIN_NR; Title = BcAD.TITLE; Birthday = BcAD.BIRTHDAY; MANAGER = BcAD.MANAGER; whenCreated = BcAD.whenCreated; ErsterArbeitstag = BcAD.ErsterArbeitstag; Vorname = BcAD.Firstname; Nachname = BcAD.Name; BRAIN_NR = BcAD.BRAIN_NR; }
If now the signed-in user is changed because of a user impersonation, the information from the new user will not be rendered in the footer, even the new information are set in the properties from the BcActiveDirectory class (service), because the footer is alread rendered the first time the page is loaded with the OnInitializedAsync() task.
In order to inform the BC_Footer.razor component, that its state has changed, we need to execute the StateHasChanged() method when the properties in our BcActiveDirectory class (service) have changed.
Therefore we can use C# events and delegates to inform the BC_Footer.razor component, when properties in our BcActiveDirectory class (service) have changed and therefore the StateHasChanged() method should be executed.
Handle and raise events
https://learn.microsoft.com/en-us/dotnet/standard/events/
More about C# events, delegates and the EventCallback<T> class in Blazor Server, I will show in Part 7 below.
Blazor Server Series
Blazor Server – Basics Part 1
https://blog.matrixpost.net/blazor-server-basics-part-i/Blazor Server – Basics Part 2
https://blog.matrixpost.net/blazor-server-basics-part-ii/Blazor Server – Basics Part 3 – Custom Layout
https://blog.matrixpost.net/blazor-server-basics-part-iii-custom-layout/Blazor Server – Basics Part 4 – Program.cs File
https://blog.matrixpost.net/blazor-server-basics-part-iv-program-cs-file/Blazor Server – Basics Part 5 – Authentication and Authorization
https://blog.matrixpost.net/blazor-server-basics-part-v-authentication-and-authorization/Blazor Server – Basics Part 6 – Query the on-premise Active Directory
https://blog.matrixpost.net/blazor-server-basics-part-vi-query-the-on-premise-active-directory/Blazor Server – Basics Part 7 – C# Events, Delegates and the EventCallback Class
https://blog.matrixpost.net/blazor-server-basics-part-vii-c-events-and-delegates/Blazor Server – Basics Part 8 – JavaScript interoperability (JS interop)
https://blog.matrixpost.net/blazor-server-basics-part-viii-javascript-interoperability-js-interop/Blazor Server – Basics Part 9 – Responsive Tags and Chips
https://blog.matrixpost.net/blazor-server-basics-part-ix-responsive-tags-and-chips/Blazor Server – Basics Part 10 – MS SQL Server Access and Data Binding
https://blog.matrixpost.net/blazor-server-basics-part-10-ms-sql-server-access-and-data-binding/Blazor Server – Basics Part 11 – Create a Native Blazor UI Toggle Switch Component
https://blog.matrixpost.net/blazor-server-basics-part-11-native-blazor-toggle-switch-by-using-the-eventcallback-class-and-css/Blazor Server – Basics Part 12 – Create a Native Blazor UI Toggle Button Component
https://blog.matrixpost.net/blazor-server-basics-part-12-create-a-native-blazor-ui-toggle-button-component/
Links
ASP.NET Core Blazor authentication and authorization
https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-7.0ASP.NET Core Blazor dependency injection
https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-7.0Handle and raise events
https://learn.microsoft.com/en-us/dotnet/standard/events/