ASP.NET Core IdentityServer4 Resource Owner Password Flow with custom UserRepository

This article shows how a custom user store or repository can be used in IdentityServer4. This can be used for an existing user management system which doesn’t use Identity or request user data from a custom source. The Resource Owner Flow using refresh tokens is used to access the protected data on the resource server. The client is implemented using IdentityModel.

Code: https://github.com/damienbod/AspNetCoreIdentityServer4ResourceOwnerPassword

History

2019-09-14 Updated to .NET Core 3.0

2019-03-29 Updated to .NET Core 2.2

2018-09-23 Updated to .NET Core 2.1

Setting up a custom User Repository in IdentityServer4

To create a custom user store, an extension method needs to be created which can be added to the AddIdentityServer() builder. The .AddCustomUserStore() adds everything required for the custom user management.

services.AddIdentityServer()
		.AddSigningCredential(cert)
		.AddInMemoryIdentityResources(Config.GetIdentityResources())
		.AddInMemoryApiResources(Config.GetApiResources())
		.AddInMemoryClients(Config.GetClients())
		.AddCustomUserStore();
}

The extension method adds the required classes to the ASP.NET Core dependency injection services. A user respository is used to access the user data, a custom profile service is added to add the required claims to the tokens, and a validator is also added to validate the user credentials.

using CustomIdentityServer4.UserServices;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class CustomIdentityServerBuilderExtensions
    {
        public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder)
        {
            builder.Services.AddSingleton<IUserRepository, UserRepository>();
            builder.AddProfileService<CustomProfileService>();
            builder.AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>();

            return builder;
        }
    }
}

The IUserRepository interface adds everything required by the application to use the custom user store throughout the IdentityServer4 application. The different views, controllers, use this interface as required. This can then be changed as required.

namespace CustomIdentityServer4.UserServices
{
    public interface IUserRepository
    {
        bool ValidateCredentials(string username, string password);

        CustomUser FindBySubjectId(string subjectId);

        CustomUser FindByUsername(string username);
    }
}

The CustomUser class is the the user class. This class can be changed to map the user data defined in the persistence medium.

namespace CustomIdentityServer4.UserServices
{
    public class CustomUser
    {
            public string SubjectId { get; set; }
            public string Email { get; set; }
            public string UserName { get; set; }
            public string Password { get; set; }
    }
}

The UserRepository implements the IUserRepository interface. Dummy users are added in this example to test. If you using a custom database, or dapper, or whatever, you could implement the data access logic in this class.

using System.Collections.Generic;
using System.Linq;
using System;

namespace CustomIdentityServer4.UserServices
{
    public class UserRepository : IUserRepository
    {
        // some dummy data. Replce this with your user persistence. 
        private readonly List<CustomUser> _users = new List<CustomUser>
        {
            new CustomUser{
                SubjectId = "123",
                UserName = "damienbod",
                Password = "damienbod",
                Email = "damienbod@email.ch"
            },
            new CustomUser{
                SubjectId = "124",
                UserName = "raphael",
                Password = "raphael",
                Email = "raphael@email.ch"
            },
        };

        public bool ValidateCredentials(string username, string password)
        {
            var user = FindByUsername(username);
            if (user != null)
            {
                return user.Password.Equals(password);
            }

            return false;
        }

        public CustomUser FindBySubjectId(string subjectId)
        {
            return _users.FirstOrDefault(x => x.SubjectId == subjectId);
        }

        public CustomUser FindByUsername(string username)
        {
            return _users.FirstOrDefault(x => x.UserName.Equals(username, StringComparison.OrdinalIgnoreCase));
        }
    }
}

The CustomProfileService uses the IUserRepository to get the user data, and adds the claims for the user to the tokens, which are returned to the client, if the user/application was validated.

using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;

namespace CustomIdentityServer4.UserServices
{
    public class CustomProfileService : IProfileService
    {
        protected readonly ILogger Logger;


        protected readonly IUserRepository _userRepository;

        public CustomProfileService(IUserRepository userRepository, ILogger<CustomProfileService> logger)
        {
            _userRepository = userRepository;
            Logger = logger;
        }


        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();

            Logger.LogDebug("Get profile called for subject {subject} from client {client} with claim types {claimTypes} via {caller}",
                context.Subject.GetSubjectId(),
                context.Client.ClientName ?? context.Client.ClientId,
                context.RequestedClaimTypes,
                context.Caller);

            var user = _userRepository.FindBySubjectId(context.Subject.GetSubjectId());

            var claims = new List<Claim>
            {
                new Claim("role", "dataEventRecords.admin"),
                new Claim("role", "dataEventRecords.user"),
                new Claim("username", user.UserName),
                new Claim("email", user.Email)
            };

            context.IssuedClaims = claims;
        }

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

The CustomResourceOwnerPasswordValidator implements the validation.

using IdentityServer4.Validation;
using IdentityModel;
using System.Threading.Tasks;

namespace CustomIdentityServer4.UserServices
{
    public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
    {
        private readonly IUserRepository _userRepository;

        public CustomResourceOwnerPasswordValidator(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
        {
            if (_userRepository.ValidateCredentials(context.UserName, context.Password))
            {
                var user = _userRepository.FindByUsername(context.UserName);
                context.Result = new GrantValidationResult(user.SubjectId, OidcConstants.AuthenticationMethods.Password);
            }

            return Task.FromResult(0);
        }
    }
}

The AccountController is configured to use the IUserRepository interface.

[SecurityHeaders]
public class AccountController : Controller
{
	private readonly IIdentityServerInteractionService _interaction;
	private readonly AccountService _account;
	private readonly IUserRepository _userRepository;

	public AccountController(
		IIdentityServerInteractionService interaction,
		IClientStore clientStore,
		IHttpContextAccessor httpContextAccessor,
		IAuthenticationSchemeProvider schemeProvider,
		IUserRepository userRepository)
	{
		_interaction = interaction;
		_account = new AccountService(interaction, httpContextAccessor, schemeProvider, clientStore);
		_userRepository = userRepository;
	}

        /// <summary>
        /// Show login page
        /// </summary>
        [HttpGet]

Setting up a grant type ResourceOwnerPasswordAndClientCredentials to use refresh tokens

The grant type ResourceOwnerPasswordAndClientCredentials is configured in the GetClients method in the IdentityServer4 application. To use refresh tokens, you must add the IdentityServerConstants.StandardScopes.OfflineAccess to the allowed scopes. Then the other refresh token settings can be set as required.

public static IEnumerable<Client> GetClients()
{
	return new List<Client>
	{
		new Client
		{
			ClientId = "resourceownerclient",

			AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
			AccessTokenType = AccessTokenType.Jwt,
			AccessTokenLifetime = 120, //86400,
			IdentityTokenLifetime = 120, //86400,
			UpdateAccessTokenClaimsOnRefresh = true,
			SlidingRefreshTokenLifetime = 30,
			AllowOfflineAccess = true,
			RefreshTokenExpiration = TokenExpiration.Absolute,
			RefreshTokenUsage = TokenUsage.OneTimeOnly,
			AlwaysSendClientClaims = true,
			Enabled = true,
			ClientSecrets=  new List<Secret> { new Secret("dataEventRecordsSecret".Sha256()) },
			AllowedScopes = {
				IdentityServerConstants.StandardScopes.OpenId, 
				IdentityServerConstants.StandardScopes.Profile,
				IdentityServerConstants.StandardScopes.Email,
				IdentityServerConstants.StandardScopes.OfflineAccess,
				"dataEventRecords"
			}
		}
	};
}

When the token client requests a token, the offline_access must be sent in the HTTP request, to recieve a refresh token.

private static async Task<TokenResponse> RequestTokenAsync(string user, string password)
{
	Log.Logger.Verbose("begin RequestTokenAsync");
	var response = await _httpClient.RequestPasswordTokenAsync(new PasswordTokenRequest
	{
		Address = _disco.TokenEndpoint,

		ClientId = "resourceownerclient",
		ClientSecret = "dataEventRecordsSecret",
		Scope = "email openid dataEventRecords offline_access",

		UserName = user,
		Password = password
	});

	return response;
}

Running the application

When all three applications are started, the console application gets the tokens from the IdentityServer4 application and the required claims are returned to the console application in the token. Not all the claims need to be added to the access_token, only the ones which are required on the resource server. If the user info is required in the UI, a separate request can be made for this info.

Here’s the token payload returned from the server to the client in the token. You can see the extra data added in the profile service, for example the role array.

{
  "nbf": 1492161131,
  "exp": 1492161251,
  "iss": "https://localhost:44318",
  "aud": [
    "https://localhost:44318/resources",
    "dataEventRecords"
  ],
  "client_id": "resourceownerclient",
  "sub": "123",
  "auth_time": 1492161130,
  "idp": "local",
  "role": [
    "dataEventRecords.admin",
    "dataEventRecords.user"
  ],
  "username": "damienbod",
  "email": "damienbod@email.ch",
  "scope": [
    "email",
    "openid",
    "dataEventRecords",
    "offline_access"
  ],
  "amr": [
    "pwd"
  ]
}

The token is used to get the data from the resource server. The client uses the access_token and adds it to the header of the HTTP request.

HttpClient httpClient = new HttpClient();
httpClient.SetBearerToken(access_token);

var payloadFromResourceServer = await httpClient.GetAsync("https://localhost:44365/api/DataEventRecords");
if (!payloadFromResourceServer.IsSuccessStatusCode)
{
	Console.WriteLine(payloadFromResourceServer.StatusCode);
}
else
{
	var content = await payloadFromResourceServer.Content.ReadAsStringAsync();
	Console.WriteLine(JArray.Parse(content));
}

The resource server validates each request using the UseIdentityServerAuthentication middleware extension method.

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
IdentityServerAuthenticationOptions identityServerValidationOptions = new IdentityServerAuthenticationOptions
{
	Authority = "https://localhost:44318/",
	AllowedScopes = new List<string> { "dataEventRecords" },
	ApiSecret = "dataEventRecordsSecret",
	ApiName = "dataEventRecords",
	AutomaticAuthenticate = true,
	SupportedTokens = SupportedTokens.Both,
	// TokenRetriever = _tokenRetriever,
	// required if you want to return a 403 and not a 401 for forbidden responses
	AutomaticChallenge = true,
};

app.UseIdentityServerAuthentication(identityServerValidationOptions);

Each API is protected using the Authorize attribute with policies if needed. The HttpContext can be used to get the claims sent with the token, if required. The username is sent with the access_token in the header.

[Authorize("dataEventRecordsUser")]
[HttpGet]
public IActionResult Get()
{
	var userName = HttpContext.User.FindFirst("username")?.Value;
	return Ok(_dataEventRecordRepository.GetAll());
}

The client gets a refresh token and updates periodically in the client. You could use a background task to implement this in a desktop or mobile application.

public static async Task RunRefreshAsync(TokenResponse response, int milliseconds)
{
	var refresh_token = response.RefreshToken;

	while (true)
	{
		response = await RefreshTokenAsync(refresh_token);

		// Get the resource data using the new tokens...
		await ResourceDataClient.GetDataAndDisplayInConsoleAsync(response.AccessToken);

		if (response.RefreshToken != refresh_token)
		{
			ShowResponse(response);
			refresh_token = response.RefreshToken;
		}

		Task.Delay(milliseconds).Wait();
	}
}

The application then loops forever.

Links:

https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow

https://github.com/IdentityModel/IdentityModel2

https://github.com/IdentityServer/IdentityServer4

https://github.com/IdentityServer/IdentityServer4.Samples

19 comments

  1. […] ASP.NET Core IdentityServer4 Resource Owner Password Flow with custom UserRepository (Damien Bowden) […]

  2. Bill Moniz · · Reply

    Many thanks for this post! I found it very helpful. Cheers.

  3. Your article is so complete and just what I was looking for. Many thanks for your hard work and sharing.

  4. Kamel Jabber · · Reply

    Maybe you can help me. This article was super helpful and things seem to be working well.
    However, via VS2017 if I stop debugging w/o logging out then restart the application I’m still logged into the Identity Server.
    This does not happen when using the in memory test users.
    I guess I’m confused, how does the IDP think the user is logged in when it was completely restarted (via the debugger to boot)?

  5. I am confused… Getting 401 unauthorized. Created server (without cors, policies. A simple server with inmemory clients and custom user repository ) and I am able to get access token. But when I pass the access token in headers to my resource API, its giving me 401.

    1. Sorry was referring wrong scope, Now I am able to get output.

  6. Hey man,

    Excellent post – saved me a bunch of time and effort.

    Thanks!

  7. […] a Identityserver by following this link But in resource server side, I am unable to authorize an […]

  8. What is being returned from CustomProfileService.GetProfileDataAsync?

  9. Debashis Chowdhury · · Reply

    Hi, thanks for this post. But i can’t run the CustomIdentityServer4, getting message HTTP Error 502.5 – Process Failure. What is the reason behind this?

    1. Hi Debashis

      I updated this to .NET Core 2.1, should work now. Thanks for reporting.

      Greetings Damien

      1. Hi Damien, thanks for your reply. I have converted the full code to .net core 2. it is now working fine. Even i had related the users with multiple ApiResources and authorize by considering their association with the ApiResource. Now, how to authorizing a user with it’s role? if the user do not have the role, i do not want to give him access the ApiResource.

  10. i found that solution to authorize users with their specific role on a specific Resource from your code. Here’s the explanation which can help people.

    in CustomProfileServices.cs=> I had modified the GetProfileDataAsync(ProfileDataRequestContext context) function in bellow ways. I populate the claim list as per User’s roll for a specific ApiResource. It is now authorizing user with it’s Role. In other wards, though userName and password is correct, if the user do not have the role, he can’t get access to my API.

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {

    var user = _userRepository.FindBySubjectId(context.Subject.GetSubjectId());

    var resources = context.RequestedResources.ApiResources.GetEnumerator();
    resources.MoveNext();
    var resource = resources.Current;
    var role = _userRepository.FindRoleByUsername(user.UserName, resource.Name);

    var claims = new List
    {
    new Claim(“username”, user.UserName),
    new Claim(“email”, user.Email)
    };

    if (role != null)
    {
    claims.Add(new Claim(“role”, role));
    }

    context.IssuedClaims = claims;
    }

    To validate the user against the Resource, I had modified the method named “ValidateCredentials()” that is being called in CustomResourceOwnerPasswordValidator.cs. Also there’s modification in models to meet my requirement.

    public bool ValidateCredentials(string username, string password,IEnumerable scope, ICollection apiResources)
    {
    var user = FindByUsername(username);
    if (user != null)
    {
    // find the user has the access to the Resource or not.
    var resources = apiResources.GetEnumerator();
    resources.MoveNext();
    var resource = resources.Current;

    var hasResource = _apiResources.Where(r => r.Name.Equals(resource.Name, StringComparison.CurrentCultureIgnoreCase) && r.Users.Find(u=>u.SubjectId==user.SubjectId)!=null).FirstOrDefault();
    if (hasResource != null)
    return user.Password.Equals(password);
    else return false;
    }

    return false;
    }

    Hope these modifications will help people to authorize users with their specific role on a specific Resource. Thanks for your hard working post. It helped me in custom user authorization. I’ll share my solution in GitHub. I’ll be happy if you share my work which had done over your project.

  11. Hi Damien, can you tell me what to do if my resource server is on ASP.net MVC 5?

  12. Hi Damien, i need to use ApplicationUser in your custom defined class UserRepository.

    Please tell me how to get initialize the bellow property to validate my user with UserManager
    private readonly UserManager _userManager;

  13. Robert Frey · · Reply

    Damien,

    I am trying to create a micro-service that wraps Identity server 4 with .NET core. I have API to API working.

    I have implemented the above code, but what I actually need is a way to generate the token after I have validated the user / password. Is there a method I can call and pass the Client scopes to generate the token after the validation is successful?

  14. Can we do multi-factor authentication for custom users stores.

  15. Ankit · · Reply

    thank you …thank you…thank you

  16. Thanks! Pretty straight forward approach. All I needed in a nutshell.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.