Engineering Software

Hi! I'm David, a full stack web developer.

Include user properties in IdentityServer4 with ASP.NET Identity

In this blog post I show how to include custom user properties on the default ApplicationUser (from ASP.NET Identity) as claims for further token usages. Per default these properties are not being included and not available in token consuming apps.

Why IdentityServer?

In a current web project I use IdentityServer as authorization endpoint. This well maintained open source project written by Brock Allen and Dominick Baier is currently in RC1 for version 4. It combines OpenID Connect with OAuth 2.0 which allows not only do authorization management (OAuth) but identity service (OpenID Connect) as well. IdentityServer can therefore authenticate users (via internal user store or external authentication services such as Twitter, Facebook, etc.) and later do authorization on a claim based level as defined in the spec. To read more about the project go to identityserver.io.

IdentityServer4 with ASP.NET Identity

As a user store I simply use ASP:NET Identity which persists the user data via Entity Framework in a database of your choice (MS SQL, SQLite, MySQL, you name it...). Microsoft provides almost everything for user management with the Visual Studio template and why should I reinvent the wheel? The glue between IdentityServer4 and ASP.NET Identity is provided by the makers of IdentityServer in a nuget package called IdentityServer4.AspNetIdentity.

Introducing IdentityUser

A fresh APS.NET Core Web Application template adds a predefined ApplicationUser as IdentityUser. This POCO (plain old class object) looks like this:

public class ApplicationUser : IdentityUser  
{
}

It is an empty class but derived from IdentityUser which depends on a type for the unique key, in this case a string:

public class IdentityUser : IdentityUser<string>  
{
    public IdentityUser();
    public IdentityUser(string userName);
}

Far up the inheritance chain you get some more properties added to your IdentityUser.

public class IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin> where TKey : IEquatable<TKey>  
    {
    public virtual int AccessFailedCount { get; set; }
    public virtual ICollection<TUserClaim> Claims { get; }
    public virtual string ConcurrencyStamp { get; set; }
    public virtual string Email { get; set; }
    public virtual bool EmailConfirmed { get; set; }
    public virtual TKey Id { get; set; }
    public virtual bool LockoutEnabled { get; set; }
    public virtual DateTimeOffset? LockoutEnd { get; set; }
    public virtual ICollection<TUserLogin> Logins { get; }
    public virtual string NormalizedEmail { get; set; }
    public virtual string NormalizedUserName { get; set; }
    public virtual string PasswordHash { get; set; }
    public virtual string PhoneNumber { get; set; }
    public virtual bool PhoneNumberConfirmed { get; set; }
    public virtual ICollection<TUserRole> Roles { get; }
    public virtual string SecurityStamp { get; set; }
    public virtual bool TwoFactorEnabled { get; set; }
    public virtual string UserName { get; set; }
}

Next to these properties you are invited to add custom properties like a users first name and last name etc.

Extending IdentityUser

In a typical Web Application scenario as Microsoft has foreseen, you integrate user management within the same application. You have a reference to your ApplicationUser class and access to the current user via UserManager<> service which you can inject into your controller.

In the world of IdentityServer and OpenID Connect user properties are being transmitted in form of claims (key value pairs). Claims usually are optional/additional information about a user. Since they represent only string based keys and values and might even change (and added or removed) during runtime, enforcement of claims is tricky.

To absolutely make sure on important information about a user I usually add required properties to the user like so:

public class ApplicationUser : IdentityUser  
{
    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }

    public string FullName { get { return $"{FirstName} {LastName}"; } }

    [Required]
    public int Age { get; set; }
}

I then can guarantee that a user has at least a first name, last name and an age (although, the latter might be 0 if implemented like this).

The Problem

The default implementation of the ProfileService in the ASP.NET package from IdentityServer includes all claims of the user but does not account for user properties.

public class ProfileService<TUser> : IProfileService  
    where TUser : class
{
    private readonly IUserClaimsPrincipalFactory<TUser> _claimsFactory;
    private readonly UserManager<TUser> _userManager;

    public ProfileService(UserManager<TUser> userManager,
        IUserClaimsPrincipalFactory<TUser> 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 cs = principal.Claims.ToList();
        if (!context.AllClaimsRequested)
        {
            cs = cs.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
        }

        context.IssuedClaims = cs;
    }

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

The GetProfileDataAsync method gets called when the identity token is being issued by the user info endpoint. The calling method is ProcessAsync within the registered UserInfoResponseGenerator class. It collects all claims of the ClaimsPrincipal and adds it to the ProfileDataRequestContext which will be later on serialized to a JSON object.

CustomProfileService as Solution

Unfortunately we can not derive from the predefined ProfileService as the GetProfileDataAsync method neither is marked virtual nor is abstract. You have to create your own profile service class.

The implementation is straight forward:

public class CustomProfileService : IProfileService  
{
    private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
    private readonly UserManager<ApplicationUser> _userManager;

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

    // not virtual or abstract, therefore not overridable
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    { 
        var sub = context.Subject.GetSubjectId();

        var user = await _userManager.FindByIdAsync(sub);
        var principal = await _claimsFactory.CreateAsync(user);

        var cs = principal.Claims.ToList();
        if (!context.AllClaimsRequested)
        {
            cs = cs.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
        }

        // Add User Properties
        cs.Add(new System.Security.Claims.Claim(StandardScopes.Email.Name, user.Email));
        cs.Add(new System.Security.Claims.Claim("full_name", user.FullName));

        context.IssuedClaims = cs;
    }

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

By making the CustomProfileService non-generic and specify the type of IUserClaimsPrincipalFactory and UserManager as ApplicationUser you have direct access to user properties and can choose the ones to be added as claims (see // Add User Properties in the code above).

Registering custom service

Within the Startup.cs class of your IdentityServer project you can now specify the profile service via the extension method:

// Add identity server services with your custom profile service
services.AddIdentityServer().AddProfileService<CustomProfileService>();  

This is it. Not much of code but IMHO an important part in combine ASP.NET Identity with IdentityServer.

Please feel free to comment on this or ask questions.

Comments is loading...

Comments is loading...