Using Active Directory (AD) as the repository for authentication with identityserver4

There are questions about this all over the web and most of them say:

“To do this, make your changes in the AccountController”.

That’s all well and good but not very helpful. I couldn’t find a single, good code example anywhere hence this post that hopefully points you in the right direction.

Note that this is about authentication; not about user provisioning.

I based this on the is4inmem template.

Disclaimer: This is very much a “Proof of Concept” (PoC). Do not use as is in Production. Use at your own risk.

In .NET, the library to access AD is:

using System.DirectoryServices.AccountManagement;

These are the packages I used:

As usual, the gist for AccountController.cs (and the other classes described below) is here.

Essentially, to authenticate against AD using your local domain controller:

var adContext = new PrincipalContext(ContextType.Domain);

if (adContext.ValidateCredentials(model.Username, model.Password))

In launchSettings.json, I set:

“windowsAuthentication”: true,

In Startup.cs:

Comment out setting up test users,

//.AddTestUsers(TestUsers.Users);

This is because we are using the users that are already in AD.

Also add a ProfileService to derive claims from AD.

services.AddScoped<IProfileService, ADProfileService>();

In Config.cs:

Add new IdentityResouces and APIResources.

public static IEnumerable<IdentityResource> Ids =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResources.Address(),
};
public static IEnumerable<ApiResource> Apis =>
new ApiResource[]
{
// new ApiResource("api1", "My API #1")
new ApiResource("api1", "My API", new[] { JwtClaimTypes.Subject, JwtClaimTypes.Email, JwtClaimTypes.Address, "upn_custom"})
};

And setup your client.

In order to get all the claims in the JWT:

AlwaysIncludeUserClaimsInIdToken = true,

Now for the profile service — ADProfileService.cs.

The documentation states:

“Profile Service

Often IdentityServer requires identity information about users when creating tokens or when handling requests to the userinfo or introspection endpoints. By default, IdentityServer only has the claims in the authentication cookie to draw upon for this identity data.

It is impractical to put all of the possible claims needed for users into the cookie, so IdentityServer defines an extensibility point for allowing claims to be dynamically loaded as needed for a user. This extensibility point is the IProfileService and it is common for a developer to implement this interface to access a custom database or API that contains the identity data for users”.

You also need to add:

“IsActiveContext

Models the request to determine is the user is currently allowed to obtain tokens”.

We can derive most of the claims from the UserPrincipal:

var adContext = new PrincipalContext(ContextType.Domain);

//var user = Users.FindBySubjectId(context.Subject.GetName);

var user = context.Subject.GetDisplayName();

uPrincipal = UserPrincipal.FindByIdentity(adContext, IdentityType.SamAccountName, user);

var claims = new Claim[]
{
new Claim(JwtClaimTypes.Name, uPrincipal.Name),
new Claim(JwtClaimTypes.GivenName, uPrincipal.GivenName),
new Claim(JwtClaimTypes.FamilyName, uPrincipal.DisplayName),
new Claim(JwtClaimTypes.Email, uPrincipal.EmailAddress),
new Claim(JwtClaimTypes.Address, "123 Main Street"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean)
};

Note that you can also hard code claims.

Update

To get groups, use:

// Get all groups user is a "memberOf"
PrincipalSearchResult<System.DirectoryServices.AccountManagement.Principal> oPrincipalSearchResult = null;

oPrincipalSearchResult = uPrincipal.GetGroups();

// Add groups as a claim type of "role"

foreach (System.DirectoryServices.AccountManagement.Principal oResult in oPrincipalSearchResult)
{
// Getting all groups causes JWT to be far too big so just using one as an example.
// To see if a user is a "memberOf" a group, use "uPrincipal.IsMemberOf"

if (oResult.Name == "Domain Users")
cl.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role", oResult.Name));
}

For other claims:

// To get another AD attribute not in "UserPrincipal" e.g. "Department"

string department = "";

if (uPrincipal.GetUnderlyingObjectType() == typeof(DirectoryEntry))
{
// Transition to directory entry to get other properties
using (var entry = (DirectoryEntry)uPrincipal.GetUnderlyingObject())
{
if (entry.Properties["department"] != null)
department = entry.Properties["department"].Value.ToString();
}
}

For isActiveContext:

var adContext = new PrincipalContext(ContextType.Domain);

var user = context.Subject;

Claim userClaim = user.Claims.FirstOrDefault(claimRecord => claimRecord.Type == "sub");

uPrincipal = UserPrincipal.FindByIdentity(adContext, IdentityType.SamAccountName, userClaim.Value);

// To be active, user must be enabled and not locked out

var isLocked = uPrincipal.IsAccountLockedOut();

context.IsActive = (bool)(uPrincipal.Enabled & !isLocked);

Now lets run it.

To test it, we’ll use one of the test clients in the samples.

So we login using our AD credentials.

Then we get the consent screen:

Accept and then you will see the claims:

And by adding groups:

Let’s try with invalid credentials:

And, as expected, we get an error.

There is a school of thought that suggests that it is best practice to keep the claims in the token as small as possible and get any additional claims required via the OpenID Connect User Info API.

If you would prefer to go down that route, it’s discussed here.

All good!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store