fbpx

05. IdentityServer4 Adding custom properties to User .NET Core 3.1

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 -o Data/Migrations/AspNetIdentity/AspNetIdentityDb
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
  • Ugo Montefiori says:

    Hello and thank you for the excellent tutorial you have prepared. I followed every step from the first lesson. I’d say all right. But regarding this lesson I was unable to view the token with the claim “employee_id”. Do you have any suggestions? thank you.

    • deblokt says:

      Hi Ugo. Could you please be more specific about the issue you have? You can inspect your tokens using jwt.io website.

      • Ugo Montefiori says:

        Hello,
        I am referring to the last step of this chapter where it is said:

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

        and a 15-line json is shown.

        I have not been able to find or produce this token in the solution.

        Thank you very much.

        • deblokt says:

          You need to invoke the authorization and/or token endpoints of the Identity Server in order to acquire a token. You can use MVC, SPA or any other client (like Postman) to obtain a token.

  • Jan Madsen says:

    The -o parameter in the Add-Migration command should include the ApsNetIdentity folder like:
    Add-Migration IdentityUserCustomProperties -c IdentityDbContext -o Data/Migrations/AspNetIdentity/AspNetIdentityDb

  • Moses says:

    Thanks so much for taking your time to write these great tuts. I’m trying to integrate IdentityServer4 into an existing asp.net core 3.1 app and so far everything has worked well except for the discovery document. My app runs at url “https://localhost:44317”. When I try to call the discovery document url “https://localhost:44317/.well-known/openid-configuration”, I get a 404 page not found. Am I missing a config somewhere? I have followed your instructions to the dot. Thanks.

    • Moses says:

      I solved it. I had forgotten to add app.UseIdentityServer() to the Configure method in Startup.cs. Thanks

  • EJ says:

    Hello, thanks for the great tutorial! In my case, putting AddProfileService at the very end when configuring Identity Server worked for me. Not sure if I’m doing anything else wrong but here’s the GitHub thread that helped me out https://github.com/IdentityServer/IdentityServer4/issues/430

    Just putting it here in case someone else needs it.

  • Miguel Borquez says:

    how can I address the table “cs_user” instead of “dbo.AspNetUsers”.

  • Praveen Kumar says:

    I gone through all previous chapters and setup accordingly but i am still confused How to use identity pages like register, forget password.

    Please suggest How to use ASP.Net Identity generated razor pages/Model ?

  • Panos Skyvalidas says:

    Hi and thanks for your great tutorial.
    My solution contains 2 projects, one with the identity server and asp .net identity (like in your demo) and one that is my web api that is secured through the identity server. In this case (when adding custom properties) how can i can declare that the “EmployeeId” is foreign key to the Employee table defined in my web api’s DbContext?

    • deblokt says:

      I wouldn’t recommend adding FK. Databases should be separate, Identity database from your project databases. Even if they are the same database I wouldn’t add FK as this creates tight coupling between your app and Identity solution.

  • Jason says:

    I ran the Update-Database commands from the migrations from the source. I had no problem updating and seeding the IdentityDbContext and the PersistedGrantDbContext, but I can’t get the ConfigurationDbContext to Initialize and Seed… I am getting this when trying to UpdateDB…

    Update-Database -Context ConfigurationDbContext

    “More than one DbContext named ‘ConfigurationDbContext’ was found. Specify which one to use by providing its fully qualified name using its exact case.”

    What am I doing wrong here? How can I fix this? Thanks!

    • Jason says:

      Update-Database -Context IdentityServer.Data.ConfigurationDbContext

      Got it. Thank you though!

  • Fuji says:

    Great tutorial!!! I was able to download the source code from chapter 5 and setup to run local. I posted the steps I went thru in Medium site https://medium.com/@fuji.nguyen/identityserver4-adding-custom-properties-to-user-net-core-3-1-87031991b64d. I hope it would help others who want to try out your tutorial starting at chapter 5.

  • Narayan says:

    Hello There
    Greetings. Thank you very much for a this wonderful tutorial. It really has helped a lot. I have few questions in addition to this chapter. As we added the custom user properties “EmployeeId” and “IsEnabled”. In addition to this I also want to add some additional properties such as FirstName, LastName, Company Name, Department, Website, Address, PostalCode, City, Country.
    My questions are:
    1. Some fields I want to include are already in”IdentityClaims” table. does it affects adding the same properties also in user model
    2. In chapter 04-part-3, ApplicationUser has the Email = “BobSmith@email.com”, and the IdentityUserClaim has ClaimValue = “BobSmith@email.com” both of them have the same value entered twice. I could not understand it, can’t we expose the user properties in the token? How do we pass the Claim values if they are not in any Signup form in UI?
    3. How should I customise the UI to support the signup and in edit profile including all these custom fields ?
    4. Lets say if I register the custom properties in“User” model, How do I expose them in the id or access tokens if I have multiple properties from this line?
    // Add custom claims in token here based on user properties or any other source
    claims.Add(new Claim(“employee_id”, user.EmployeeId ?? string.Empty));
    Or is it a good practise to expose them in the id or access tokens ?
    Thank you very much again for your kindness.
    Waiting forward from you.

    • deblokt says:

      1. It doesn’t affect it
      2. Claims are always optional where adding properties to User model binds these values to the User.
      3. The administration is out of scope of ID4 you can use the Skoruba Admin for that and extend it as you wish
      4. You need to add each property into the token. The better practice is to use claims but it depends on the property/claim meaning.

Comments are closed.