I've searched all over on how to register a UserService
with IdentityServer4 in asp.net core, but I cant seem to find the right way to do it.
This is the code to register InMemoryUsers found here, however I would like to access users from my MSSQL DB not static users defined in the sample.
var builder = services.AddIdentityServer(options =>
{
options.SigningCertificate = cert;
});
builder.AddInMemoryClients(Clients.Get());
builder.AddInMemoryScopes(Scopes.Get());
builder.AddInMemoryUsers(Users.Get());
So then I looked at this which is for IdentityServer3.
var factory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
var userService = new UserService();
factory.UserService = new Registration<IUserService>(resolver => userService);
From reading online it seems I need to use the DI system to register the UserService, but I'm not sure how it binds to the IdentityServer eg.
services.AddScoped<IUserService, UserService>();
So my question is:
How do I bind my UserService
to the builder (IdentityServer4 Users)? And how would I go about calling my database to access and authenticate my existing db users in the UserService
(I use repositories to connect to db)?
Taking into account this has to work with asp.net core.
Thanks!
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
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.