fbpx

05. IdentityServer4 Adding custom properties to User

You can find the project here.

Custom User properties vs claims

Initial user properties are set by ASP.NET Core Identity. These properties like “Username”, “Email”, “AccessFailedCount” etc are defined for each user. Claims could be used to add additional user information in tokens for a specified identity scope. But there are scenarios where adding claims is not optimal. For example, adding the “IsEnabled” property to a “User” model makes sense, but adding the “IsEnabled” claim is kind of weird. “IsEnabled” property should be much closer to the user. It should be stored in the same table where the user information is stored. “IsEnabled” flag should be available to us in login flows where we validate user information. For example, if the user is disabled we don’t want a successful login result.

 

Adding custom properties to User

I will continue from my previous tutorial where we migrated user store to a Sql database. Open the “Quickstart” solution in Visual Studio. Navigate to “Models/ApplicationUser.cs”. “ApplicationUser” class is currently empty but it inherits from the “IdentityUser” class. Let’s add two properties, the “IsEnabled” and the “EmployeeId” property like so

public class ApplicationUser : IdentityUser
{
	public bool IsEnabled { get; set; }
	public string EmployeeId { get; set; }
}

We need to create and execute a data migration to create the “IsEnabled” and “EmployeeId” columns in the SQL table “dbo.AspNetUsers”. We are actually expanding the user store and adding a custom property to a “User” model and backing data store. Each time we grab a “User” model from the database we will grab the “IsEnabled” and “EmployeeId” properties along. This is extremely helpful if we want to control the login flow with these properties.

Before moving forward let’s create and execute the data migration. Open the Package Manager Console and type in:

Add-Migration IdentityUserCustomProperties -c IdentityDbContext
Update-Database -Context IdentityDbContext

 

Successful migration should produce the output like so:

If you now check the “dbo.AspNetUsers” table you will see two new columns corresponding to the specified custom properties.

 

Modify login flow to check the IsEnabled property

Let’s try to use the “IsEnabled” property for something useful. We will check the “IsEnabled” property during the local and external login process to stop the login if the user is disabled. Open the “Quickstart/Account/AccountController.cs” and find “Login” action method that handles the POST request. We don’t want to add any code prior to input model validation. After the model validation we can try to find the user and check the “IsEnabled” flag like so: 

if (ModelState.IsValid)
{
	var user = await _userManager.FindByNameAsync(model.Username);
	if (user != null && user.IsEnabled)
	{
		var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberLogin, lockoutOnFailure: true);
		if (result.Succeeded)
		{
			await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName));

			if (context != null)
			{
				if (await _clientStore.IsPkceClientAsync(context.ClientId))
				{
					// if the client is PKCE then we assume it's native, so this change in how to
					// return the response is for better UX for the end user.
					return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
				}

				// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
				return Redirect(model.ReturnUrl);
			}

			// request for a local page
			if (Url.IsLocalUrl(model.ReturnUrl))
			{
				return Redirect(model.ReturnUrl);
			}
			else if (string.IsNullOrEmpty(model.ReturnUrl))
			{
				return Redirect("~/");
			}
			else
			{
				// user might have clicked on a malicious link - should be logged
				throw new Exception("invalid return URL");
			}
		}
	}

	await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
	ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}

We can also modify the “InvalidCredentialsErrorMessage” to include the message that the user is disabled to avoid user confusion as a user might provide correct credentials but still can’t sign in. Open the “AccountOptions.cs” in same folder and modify public static string “InvalidCredentialsErrorMessage” like so:

public static string InvalidCredentialsErrorMessage = "Invalid credentials or disabled user";

This handles the local login flow. We also need to modify the external login flow to not allow disabled users to sign in. Open the “Quickstart/Account/ExternalController.cs” and find the “Callback” method. Once we find (or provision) the local user based on the external login we can check the “IsEnabled” flag like so:

// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
if (user == null)
{
	// this might be where you might initiate a custom workflow for user registration
	// in this sample we don't show how that would be done, as our sample implementation
	// simply auto-provisions new external user
	user = await AutoProvisionUserAsync(provider, providerUserId, claims);
}

if (!user.IsEnabled)
{
	throw new Exception("External authentication error. Local user is disabled.");
}

That is it. Now we can enable or disable a user based on the custom “IsEnabled” flag which can easily be set in “dbo.AspNetUsers” table. Run the IdentityServer4 and try to login http://localhost:5000/Account/Login with user “alice” and password “My long 123$ password”. You will get an error because the “IsEnabled” flag is set to 0 (false).

 

Update the “IsEnabled” flag in database to 1 (true) to enable the user login for the user “alice” like so:

UPDATE [dbo].[AspNetUsers] SET [IsEnabled] = 1 WHERE [UserName] = 'alice'

 

Expose custom User property in tokens

Adding custom properties to a “User” model doesn’t mean that these custom properties are going to get exposed in id or access tokens. To expose a custom user property in tokens we need to create the “IProfileService” implementation. In the project root create a new folder called “Services” and add a new class named “ProfileService”. Open the “ProfileService.cs” and modify it like so:

using IdentityServer.Models;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityServer.Services
{
    public class ProfileService : IProfileService
    {
        private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
        private readonly UserManager<ApplicationUser> _userManager;

        public ProfileService(UserManager<ApplicationUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory)
        {
            _userManager = userManager;
            _claimsFactory = claimsFactory;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            var principal = await _claimsFactory.CreateAsync(user);

            var claims = principal.Claims.ToList();
            claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();

            // Add custom claims in token here based on user properties or any other source
            claims.Add(new Claim("employee_id", user.EmployeeId ?? string.Empty));

            context.IssuedClaims = claims;
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            context.IsActive = user != null;
        }
    }
}

All that is left to do is to add a profile service dependency injection. Open “Startup.cs” and add a scoped service at the end of the “ConfigureServices” method like so:

services.AddScoped<IProfileService, ProfileService>();

Add missing “using” directives like so:

using IdentityServer4.Services;
using IdentityServer.Services;

Done. Now when the user logs in the “employee_id” claim will be present in the token:

{
  "sid": "420f3d324b47c392d30d5b02aa699324",
  "sub": "6649e236-ae2f-41ec-89e1-0285377b8284",
  "auth_time": 1569521858,
  "idp": "local",
  "amr": [
    "pwd"
  ],
  "name": "Alice Smith",
  "given_name": "Alice",
  "family_name": "Smith",
  "website": "http://alice.com",
  "preferred_username": "alice",
  "employee_id": ""
}

Note: the same effect could be achieved by adding the claim to a user and expose it in a token. The custom user property approach has the advantage of keeping the custom property value directly in the “dbo.AspNetUsers” table which simplifies maintenance, migration and value modification. Adding custom properties to a user is not a preferred way of adding claims to a token. Sometimes it makes much more sense to store it alongside the user data.

 

Wrap up

It was pretty easy, wasn’t it? You successfully extended the “IdentityUser” model, modified the login flow based on a custom “IsEnabled” property and exposed the “EmployeeId” property in tokens for clients to use. In the next tutorial we are going to do something completely different. We are going to work on adding external providers like Azure AD and Okta to connect to the world. Stay funky!

You can find the project here.

Support

For direct assistance schedule a technical meeting with Ivan to talk about your requirements. For a general overview of our services and a live demo schedule a meeting with Maja.

Comments
  • Mike says:

    There is maybe better option to replace IUserClaimsPrincipalFactory in asp.net identity.
    Something like:
    class CustomUserClaimsPrincipalFactory: UserClaimsPrincipalFactory {

    protected override async Task GenerateClaimsAsync(ApplicationUser user)
    {
    var id = await base.GenerateClaimsAsync(user);
    id.AddClaim(new Claim(“my-claim”, “value”));
    return id;
    }
    }

    And register in Startup:
    services.AddIdentity().AddClaimsPrincipalFactory()…

    • deblokt says:

      Interesting idea. Seems like it would be easier but then it affects user principal, not just the generated tokens. If that is what you need, go for it.

    • Darcy says:

      One difference with Mike’s approach, is that Identity Server (or ASP.NET) automatically takes care of only returning an added claim if the caller has requested it and has access to it (so just add the claim and let the system handle the rest). The ProfileService could perhaps do some security/context checking, but does not handle it for you.

  • Tom Regan says:

    A question. you write:
    “Done. Now when the user logs in the “employee_id” claim will be present in the token:” and you display a blob of json. Where are you getting that blob of json from?

    Great tutorial by the way, helped me immensely. This is the only part I’ve not been able to get working (the code runs without error, but my custom claim does not persist).

  • Sadek Hussein says:

    It’s better to separate Identity Server View Layer into two separate Front End services? what are the advantages? what are the disadvantages?

  • Sergio says:

    I think the services.AddScoped() can be replaced by AddProfileService() in services.AddIdentityServer which will take care of the DI.

  • Jon Karlsen says:

    After hours of tribulation, this saved my bacon today. Great stuff!

  • Mahdi says:

    nice tutorial. thanks.

    in the line 33 of ProfileService:
    // Add custom claims in token here based on user properties or any other source
    claims.Add(new Claim(“employee_id”, user.EmployeeId ?? string.Empty));

    consider the “Context.Caller”, the customClaim (employee_id or whatever) is adding to the final claims (issuedClaims), I mean that if the context.caller is TokenEndpoint or AccessTokenEndpoint or sth else, is that ok to add the custom claims for all context.Callers?? or should I filter the context.caller and do sth else?

Comments are closed.