Identityserver4 Register Userservice and Get Users from Database in ASP.NET Core

IdentityServer4 register UserService and get users from database in asp.net core

Update - IdentityServer 4 has changed and replaced IUserService with IResourceOwnerPasswordValidator and IProfileService

I used my UserRepository to get all the user data from the database. This is injected (DI) into the constructors, and defined in Startup.cs. I also created the following classes for identity server (which is also injected):

First define ResourceOwnerPasswordValidator.cs:

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
//repository to get user from db
private readonly IUserRepository _userRepository;

public ResourceOwnerPasswordValidator(IUserRepository userRepository)
{
_userRepository = userRepository; //DI
}

//this is used to validate your user account with provided grant at /connect/token
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
//get your user model from db (by username - in my case its email)
var user = await _userRepository.FindAsync(context.UserName);
if (user != null)
{
//check if password match - remember to hash password if stored as hash in db
if (user.Password == context.Password) {
//set the result
context.Result = new GrantValidationResult(
subject: user.UserId.ToString(),
authenticationMethod: "custom",
claims: GetUserClaims(user));

return;
}

context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Incorrect password");
return;
}
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "User does not exist.");
return;
}
catch (Exception ex)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid username or password");
}
}

//build claims array from user data
public static Claim[] GetUserClaims(User user)
{
return new Claim[]
{
new Claim("user_id", user.UserId.ToString() ?? ""),
new Claim(JwtClaimTypes.Name, (!string.IsNullOrEmpty(user.Firstname) && !string.IsNullOrEmpty(user.Lastname)) ? (user.Firstname + " " + user.Lastname) : ""),
new Claim(JwtClaimTypes.GivenName, user.Firstname ?? ""),
new Claim(JwtClaimTypes.FamilyName, user.Lastname ?? ""),
new Claim(JwtClaimTypes.Email, user.Email ?? ""),
new Claim("some_claim_you_want_to_see", user.Some_Data_From_User ?? ""),

//roles
new Claim(JwtClaimTypes.Role, user.Role)
};
}

And ProfileService.cs:

public class ProfileService : IProfileService
{
//services
private readonly IUserRepository _userRepository;

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

//Get user profile date in terms of claims when calling /connect/userinfo
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
try
{
//depending on the scope accessing the user data.
if (!string.IsNullOrEmpty(context.Subject.Identity.Name))
{
//get user from db (in my case this is by email)
var user = await _userRepository.FindAsync(context.Subject.Identity.Name);

if (user != null)
{
var claims = GetUserClaims(user);

//set issued claims to return
context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
}
}
else
{
//get subject from context (this was set ResourceOwnerPasswordValidator.ValidateAsync),
//where and subject was set to my user id.
var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");

if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
{
//get user from db (find user by user id)
var user = await _userRepository.FindAsync(long.Parse(userId.Value));

// issue the claims for the user
if (user != null)
{
var claims = ResourceOwnerPasswordValidator.GetUserClaims(user);

context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
}
}
}
}
catch (Exception ex)
{
//log your error
}
}

//check if user account is active.
public async Task IsActiveAsync(IsActiveContext context)
{
try
{
//get subject from context (set in ResourceOwnerPasswordValidator.ValidateAsync),
var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "user_id");

if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
{
var user = await _userRepository.FindAsync(long.Parse(userId.Value));

if (user != null)
{
if (user.IsActive)
{
context.IsActive = user.IsActive;
}
}
}
}
catch (Exception ex)
{
//handle error logging
}
}
}

Then in Startup.cs I did the following:

public void ConfigureServices(IServiceCollection services)
{
//...

//identity server 4 cert
var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "idsrv4test.pfx"), "your_cert_password");

//DI DBContext inject connection string
services.AddScoped(_ => new YourDbContext(Configuration.GetConnectionString("DefaultConnection")));

//my user repository
services.AddScoped<IUserRepository, UserRepository>();

//add identity server 4
services.AddIdentityServer()
.AddSigningCredential(cert)
.AddInMemoryIdentityResources(Config.GetIdentityResources()) //check below
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddProfileService<ProfileService>();

//Inject the classes we just created
services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
services.AddTransient<IProfileService, ProfileService>();

//...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//...

app.UseIdentityServer();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

IdentityServerAuthenticationOptions identityServerValidationOptions = new IdentityServerAuthenticationOptions
{
//move host url into appsettings.json
Authority = "http://localhost:50000/",
ApiSecret = "secret",
ApiName = "my.api.resource",
AutomaticAuthenticate = true,
SupportedTokens = SupportedTokens.Both,

// required if you want to return a 403 and not a 401 for forbidden responses
AutomaticChallenge = true,

//change this to true for SLL
RequireHttpsMetadata = false
};

app.UseIdentityServerAuthentication(identityServerValidationOptions);

//...
}

You will also need Config.cs which defines your clients, api's and resources. You can find an example here: https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs

You should now be able to call IdentityServer /connect/token

Sample Image

For any further info, please check the documentation: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf


Old answer (this does not work for newer IdentityServer4 anymore)

Its pretty simple once you understand the flow of things.

Configure your IdentityService like this (in Startup.cs - ConfigureServices()):

var builder = services.AddIdentityServer(options =>
{
options.SigningCertificate = cert;
});

builder.AddInMemoryClients(Clients.Get());
builder.AddInMemoryScopes(Scopes.Get());

//** this piece of code DI's the UserService into IdentityServer **
builder.Services.AddTransient<IUserService, UserService>();

//for clarity of the next piece of code
services.AddTransient<IUserRepository, UserRepository>();

Then setup your UserService

public class UserService : IUserService
{
//DI the repository from Startup.cs - see previous code block
private IUserRepository _userRepository;

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

public Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
var user = _userRepository.Find(context.UserName);

//check if passwords match against user column
//My password was hashed,
//so I had to hash it with the saved salt first and then compare.
if (user.Password == context.Password)
{
context.AuthenticateResult = new AuthenticateResult(
user.UserId.ToString(),
user.Email,

//I set up some claims
new Claim[]
{
//Firstname and Surname are DB columns mapped to User object (from table [User])
new Claim(Constants.ClaimTypes.Name, user.Firstname + " " + user.Surname),
new Claim(Constants.ClaimTypes.Email, user.Email),
new Claim(Constants.ClaimTypes.Role, user.Role.ToString()),
//custom claim
new Claim("company", user.Company)
}
);
}

return Task.FromResult(0);
}

public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//find method in my repository to check my user email
var user = _userRepository.Find(context.Subject.Identity.Name);

if (user != null)
{
var claims = new Claim[]
{
new Claim(Constants.ClaimTypes.Name, user.Firstname + " " + user.Surname),
new Claim(Constants.ClaimTypes.Email, user.Email),
new Claim(Constants.ClaimTypes.Role, user.Role.ToString(), ClaimValueTypes.Integer),
new Claim("company", user.Company)
};

context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type));
}

return Task.FromResult(0);
}

public Task IsActiveAsync(IsActiveContext context)
{
var user = _userRepository.Find(context.Subject.Identity.Name);

return Task.FromResult(user != null);
}
}

Basically by injecting UserService into builder (of type IdentityServerBuilder) Services, allows it to call the UserService on auth.

I hope this helps others as it took me a couple of hours to get this going.

Identity Server 4 login existing database

You don't need to get rid of asp.net identity.
Jut implement the IUserStore and as shown in below implementation your own mechanism to get the data from existing database.following are some examples to implement IUserStore

 public async Task<Users> FindByIdAsync(string userId, CancellationToken cancellationToken)
{
//here you have to write the code to get data from existing db like ado.net or some third party api
}

In below method you have to create a poco class for your user table in existing db and pass it.

public Task<string> GetUserIdAsync(Users user, CancellationToken cancellationToken)
{
return Task.FromResult(user.user_id.ToString());//user_id will be replaced by your user id column name
}

Also add your User poco class as shown

 var builder = services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
//.AddInMemoryApiResources(Config.GetApis())
//.AddInMemoryClients(Config.GetClients())
.AddClientStore<ClientStore>()
.AddResourceStore<ResourceStore>()
.AddAspNetIdentity<Users>()//here

Entity in the API project has UserId how do I get, the username from IdentityServer4?

You need to access Username (Data stored in another database) from a different API.

Let's get some things straight. There are three different apps involved in this scenario.

  1. Identity Server: An MVC app or API which is responsible for providing the access_token.
  2. Resource Owner: The API which provides services related to the user info.
  3. Client: The API, which is trying to access the user info.

Here, the Identity Server and Resource Owner can be the same app in some cases.

There are two ways you can achieve it.

API way

  • Create an endpoint users\{userId} on the Resource Owner API.
  • Create a client on the Identity Server with Client Credential grant type. Which enables the machine to machine access.
  • Setup Identity Server on Resource Owner API with the client_id and client_secret configured in the Identity Server.
  • When the Client API receives a request on /artists/latest endpoint. It needs access to the information of the user x
    1. It sends a HTTP request to the Identity server and saves token(xyz) from the response somewhere so it can reuse it in the current request.
    2. Then sends a new HTTP request to the Resource Owner API. like www.my-api.com/api/users/x with the token as Authorizatoin: Bearer xyz. And from the response, it retrieves the required info.

You can also make an endpoint where you can send a list of user id and it returns list of user info.

Database way

The other way (Simpler way IMO)

If you have data access layer in place then you can just add it as a dependency of the Client API and use it directly.

If you don't have a DAL then you can just create a library and paste the DBContext in it then reuse it in any project you want.

IdentityServer4 + Asp Identity + EF Configure two UsersSet/Providers

As per @mxmissile, abstracting the UserManager out was the correct call. Then you can also abstract out other managers as needed for special functionality. This is in fact the only class in the inheritance layer for this part of the code that is virtual.

There are built in functions that let you register your custom managers:

services.AddIdentity<IdentityUser, IdentityRole>()
.AddUserManager<ApplicationUserManager<IdentityUser>>()
.AddSignInManager<ApplicationSignInManager<IdentityUser>>()

Hopefully this is a little help to any others that have a similar question. I ended up just overriding a couple of the functions from the base user manager and just calling the base method for anything that did not need my new logic. By default it looks like ASP Identity does not try to look up users by email - just fyi.

How to use existing DB with IdentityServer4

I started with the IdentityServer4 Quick Start sample 7_JavaScriptClient.

First I created a UserManager class that could connect to my data source and validate a user by user name and password and return that user object.

public class UserManager
{
private SecurityContext _context;

public UserManager(SecurityContext context)
{
_context = context;
}

public Task<User> Find(string username, string password)
{
...Logic to query your custom user table(s)...
}

public Task<List<Claim>> GetClaimsAsync(User user)
{
var claims = new List<Claim>();

//custom database call here to where you store your claims.
var myClaims = ...Your Database call here...
var claimGroupName = "SomeCustomName";

if (security != null && security.Count() > 0)
{
foreach (var claim in security)
{
//Add the value from the field Security_Id from the database to the claim group "SomeCustomName".
claims.Add(new Claim(claimGroupName , claim.SECURITY_ID));
}
}

return Task.FromResult(claims);

}
}

User Object

public class User
{
public string FIRST_NAME { get; set; }
public string LAST_NAME { get; set; }
public string EMAIL { get; set; }

...Other attributes/properties...
}

Second I used the UserManager Object from the AccountController in the sample.

private readonly UserManager _userManager;

public AccountController(
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IHttpContextAccessor httpContextAccessor,
UserManager userManager
)
{
_userManager = userManager;
}

Third in the AccountController.Login() HTTP Post method I called the UserManager.Find(username, password) to return a user.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
// validate username/password
var user = await _userManager.Find(model.Username, model.Password);

//sign the user in with a subject[user_id] and name[web_id]
await HttpContext.Authentication.SignInAsync(user.USER_ID, user.WEB_ID, props);
}

Fourth I implemented the IProfileService. [I used this article as a resource.]

public class ProfileService : IProfileService
{
UserManager _myUserManager;
private readonly ILogger<ProfileService> _logger;

public ProfileService(ILogger<ProfileService> logger)
{
_logger = logger;
_myUserManager = new UserManager(new SecurityContext());
}

//Called by IdentityServer Middleware.
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.FindFirst("sub")?.Value;
if (sub != null)
{
var user = await _myUserManager.FindByNameAsync(sub);

//Call custom function to get the claims from the custom database.
var cp = await getClaims(user);

var claims = cp.Claims;

...Optionaly remove any claims that don't need to be sent...

context.IssuedClaims = claims.ToList();
}
}

//Called by IdentityServer Middleware.
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _myUserManager.FindByNameAsync(sub);
context.IsActive = user != null;
return;
}

//Custom function to get claims from database via the UserManager.GetClaimsAsync() method.
private async Task<ClaimsPrincipal> getClaims(User user)
{
var id = new ClaimsIdentity();
//set any standard claims
id.AddClaim(new Claim(JwtClaimTypes.PreferredUserName, user.USER_ID));
//get custom claims from database or other source.
id.AddClaims(await _myUserManager.GetClaimsAsync(user));

return new ClaimsPrincipal(id);
}
}

Lastly in the Startup.cs setup any Objects for Dependency Injection.

public void ConfigureServices(IServiceCollection services)
{
builder.Services.AddTransient<IProfileService, ProfileService>();

//This is the DbContext to our Database where the users and their claims are stored.
services.AddTransient<SecurityContext>();
services.AddTransient<UserManager>();

}

Here is a similar question and anser as well.

Note that they also reference the IResourceOwnerPasswordValidator interface and implementation which is for authenticating OAuth 2.0 resource owner password credential grant (aka password). Used with GrantTypes.ResourceOwnerPasswordAndClientCredentials or GrantTypes.ResourceOwnerPassword.



Related Topics



Leave a reply



Submit