04. PART 3 IdentityServer4 ASP.NET Core Identity
You can find the project here.
Test data
In order to start playing with the IdentityServer4, later on, we must populate test config and user data into our database tables. Without it, we can’t start using the IdentityServer4. Changes are pretty straight forward and require adding the seed data in code and adding a setting in appsettings. When seeding is enabled the application will start, create the database, run the migrations to create both IdentityServer4 and ASP.NET Core Identity tables and populate the test data for configuration (resources and clients) and user data (users and claims).
Add test configuration and users
I will continue with the “Quickstart” solution from the previous tutorial. Open the solution and navigate to the “Data” folder. Create a new subfolder called “Seed”. Add two new classes, “Config.cs” and “Users.cs”.
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using IdentityServer4; using IdentityServer4.Models; using System.Collections.Generic; namespace IdentityServer.Data.Seed { public static class Config { public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; } public static IEnumerable<ApiResource> GetApis() { return new List<ApiResource> { new ApiResource("web_api", "My Web API") }; } public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientId = "client", // no interactive user, use the clientid/secret for authentication AllowedGrantTypes = GrantTypes.ClientCredentials, // secret for authentication ClientSecrets = { new Secret("secret".Sha256()) }, // scopes that client has access to AllowedScopes = { "web_api" } }, // resource owner password grant client new Client { ClientId = "ro.client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "web_api" } }, // OpenID Connect hybrid flow client (MVC) new Client { ClientId = "mvc", ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.Hybrid, ClientSecrets = { new Secret("secret".Sha256()) }, RedirectUris = { "http://localhost:5002/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "web_api" }, AllowOfflineAccess = true }, // JavaScript Client new Client { ClientId = "js", ClientName = "JavaScript Client", AllowedGrantTypes = GrantTypes.Code, RequirePkce = true, RequireClientSecret = false, RedirectUris = { "http://localhost:5003/callback.html" }, PostLogoutRedirectUris = { "http://localhost:5003/index.html" }, AllowedCorsOrigins = { "http://localhost:5003" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "web_api" } } }; } } }
“Users.cs” code:
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; using System.Linq; using System.Security.Claims; using IdentityModel; using IdentityServer.Models; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace IdentityServer.Data.Seed { public class Users { public static void EnsureSeedData(string connectionString) { var services = new ServiceCollection(); services.AddDbContext<IdentityDbContext>(options => options.UseSqlServer(connectionString)); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<IdentityDbContext>() .AddDefaultTokenProviders(); using (var serviceProvider = services.BuildServiceProvider()) { using (var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { var context = scope.ServiceProvider.GetService<IdentityDbContext>(); context.Database.Migrate(); var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var alice = userMgr.FindByNameAsync("alice").Result; if (alice == null) { alice = new ApplicationUser { UserName = "alice", Email = "AliceSmith@email.com", EmailConfirmed = true }; var result = userMgr.CreateAsync(alice, "My long 123$ password").Result; if (!result.Succeeded) { throw new Exception(result.Errors.First().Description); } result = userMgr.AddClaimsAsync(alice, new Claim[]{ new Claim(JwtClaimTypes.Name, "Alice Smith"), new Claim(JwtClaimTypes.GivenName, "Alice"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://alice.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json) }).Result; if (!result.Succeeded) { throw new Exception(result.Errors.First().Description); } Console.WriteLine("alice created"); } else { Console.WriteLine("alice already exists"); } var bob = userMgr.FindByNameAsync("bob").Result; if (bob == null) { bob = new ApplicationUser { UserName = "bob", Email = "BobSmith@email.com", EmailConfirmed = true }; var result = userMgr.CreateAsync(bob, "My long 123$ password").Result; if (!result.Succeeded) { throw new Exception(result.Errors.First().Description); } result = userMgr.AddClaimsAsync(bob, new Claim[]{ new Claim(JwtClaimTypes.Name, "Bob Smith"), new Claim(JwtClaimTypes.GivenName, "Bob"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://bob.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), new Claim("location", "somewhere") }).Result; if (!result.Succeeded) { throw new Exception(result.Errors.First().Description); } Console.WriteLine("bob created"); } else { Console.WriteLine("bob already exists"); } } } } } }
Modify appsettings.json
We will add new section to “appsettings.json” config file. “Seed” boolean value will enable or disable the data seeding process on application startup. Modify the “appsettings.json” like so:
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=IdentityServerQuickstart;Trusted_Connection=True;MultipleActiveResultSets=true" }, "Data": { "Seed": false } }
Modify Startup.cs and Program.cs
We need to modify the startup process of the IdentityServer4 application to take seeding into consideration. What better place to do it than “Startup.cs” and “Program.cs”. In “Startup.cs” add the “InitializeDatabase” method like so:
private void InitializeDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) { serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate(); var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>(); context.Database.Migrate(); if (!context.Clients.Any()) { foreach (var client in Config.GetClients()) { context.Clients.Add(client.ToEntity()); } context.SaveChanges(); } if (!context.IdentityResources.Any()) { foreach (var resource in Config.GetIdentityResources()) { context.IdentityResources.Add(resource.ToEntity()); } context.SaveChanges(); } if (!context.ApiResources.Any()) { foreach (var resource in Config.GetApis()) { context.ApiResources.Add(resource.ToEntity()); } context.SaveChanges(); } } }
Now we need to modify the “Configure” method to invoke the “InitializeDatabase” method like so:
public void Configure(IApplicationBuilder app) { // this will do the initial DB population bool seed = Configuration.GetSection("Data").GetValue<bool>("Seed"); if (seed) { InitializeDatabase(app); throw new Exception("Seeding completed. Disable the seed flag in appsettings"); } if (Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // uncomment if you want to support static files app.UseStaticFiles(); app.UseIdentityServer(); // uncomment, if you wan to add an MVC-based UI app.UseMvcWithDefaultRoute(); }
And last change for “Startup.cs” is to add the missing “using” directives
using System.Linq; using IdentityServer.Data.Seed; using IdentityServer4.EntityFramework.DbContexts; using IdentityServer4.EntityFramework.Mappers;
In “Program.cs” we need to change the “Main” method like so:
public static void Main(string[] args) { Console.Title = "IdentityServer4"; var host = CreateWebHostBuilder(args).Build(); var config = host.Services.GetRequiredService<IConfiguration>(); bool seed = config.GetSection("Data").GetValue<bool>("Seed"); if (seed) { var connectionString = config.GetConnectionString("DefaultConnection"); Users.EnsureSeedData(connectionString); } host.Run(); }
And now add the missing “using” directives
using IdentityServer.Data.Seed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;
Run the application to seed the data
Prior to running the IdentityServer4 with seeding enabled go ahead and drop the “IdentityServerQuickstart” database. Seeding will create the database, structure, and data.
Now enable the seeding in “appsettings.json” like so:
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=IdentityServerQuickstart;Trusted_Connection=True;MultipleActiveResultSets=true" }, "Data": { "Seed": true } }
Run the IdentityServer4 and after a short while you should see an error like this:
The error message informs us that the seeding is completed and we need to disable seeding. Stop the IdentityServer4 and change “appsettings.json” like so:
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=IdentityServerQuickstart;Trusted_Connection=True;MultipleActiveResultSets=true" }, "Data": { "Seed": false } }
Run the application to log in
Run the IdentityServer4 and navigate to http://localhost:5000/Account/Login
You should be able to login now with user “alice” and password “My long 123$ password”. After login, you should be redirected back to the IdentityServer4 home page with the name “Alice Smith” displayed in the upper left corner.
Explore the database
I encourage you to take a look at the “IdentityServerQuickstart” database. You will see it is re-created will all the tables, migrations and data. Explore the data in tables and compare it to data in “Users.cs” and “Config.cs” in the “Data/Seed” folder to learn how and where the data is stored. In the next tutorial we will take a look at the custom properties we can add to extend the “User” model.
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.
Thanks for your post, our article is realy good., I still have an exception may be you can help :
in the Users class with the EnsureSeedData methode
when i call :
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager>();
i have : Unable to resolve service for type ‘Microsoft.Extensions.Logging.ILogger`1[Microsoft.AspNetCore.Identity.UserManager`1[IdentityServer.Models.ApplicationUser]]’ while attempting to activate ‘Microsoft.AspNetCore.Identity.UserManager`1[IdentityServer.Models.ApplicationUser]’.
Please check maybe you are using “IdentityUser” instead of “ApplicationUser” somewhere. Make sure to always use “ApplicationUser” type when registering services.
Well, I’ve followed the tutorial until this point but I’ve got the same error, I’ve called GetRequiredService like this:
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager>();
and I dunno how to proceed.
The error happens when I activate the seed flag on appsettings.json.
Thank you in advance.
Breaking changes in .NET Core 3.0+ see here: https://github.com/aspnet/Announcements/issues/353
These new injection limitations broke our data seeding code. We will have future tutorials where we update to .NET Core 3.1 and this part will be refactored to work with the latest version. If you want to follow the tutorial, for now, stay on .NET Core 2.1 or 2.2.
I forgot to say in my answer that I did as you’ve said, but I got the same error 🙁
Hello @Deblokt, Thanks for the great tutorials.
I’ve faced the same issue mentioned by Frank. the error happened because UserManager class depends on Ilogger which was not added to DI. I solved the problem by adding services.AddLogging();
to the EnsureSeedData method in Users class.
then run and everyting went as expected.
Hello @Ahmed Graihy. Your suggestion worked for me (I am going through tutorial with 3.1). Just to clarify for others who read this the services.AddLogging(); needs to be added in the Data/Seed/Users.cs above the using (var serviceProvider = services.BuildServiceProvider()) line.
Frank, move the code to start-up like so:
private void EnsureSeedData(IApplicationBuilder app, string connectionString)
{
using (var scope = app.ApplicationServices.GetService().CreateScope())
{
//var context = scope.ServiceProvider.GetService();
//context.Database.Migrate();
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager>();
var alice = userMgr.FindByNameAsync(“alice”).Result;
if (alice == null)
{
alice = new ApplicationUser
{
UserName = “alice”,
Email = “AliceSmith@email.com”,
EmailConfirmed = true
};
var result = userMgr.CreateAsync(alice, “alice”).Result;
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
result = userMgr.AddClaimsAsync(alice, new Claim[]
{
new Claim(JwtClaimTypes.Name, “Alice Smith”),
new Claim(JwtClaimTypes.GivenName, “Alice”),
new Claim(JwtClaimTypes.FamilyName, “Smith”),
new Claim(JwtClaimTypes.Email, “AliceSmith@email.com”),
new Claim(JwtClaimTypes.EmailVerified, “true”, ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.WebSite, “http://alice.com”),
new Claim(JwtClaimTypes.Address, @”{ ‘street_address’: ‘One Hacker Way’, ‘locality’: ‘Heidelberg’, ‘postal_code’: 69118, ‘country’: ‘Germany’ }”,
IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json)
}).Result;
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
Console.WriteLine(“alice created”);
}
else
{
Console.WriteLine(“alice already exists”);
}
var bob = userMgr.FindByNameAsync(“bob”).Result;
if (bob == null)
{
bob = new ApplicationUser
{
UserName = “bob”,
Email = “BobSmith@email.com”,
EmailConfirmed = true
};
var result = userMgr.CreateAsync(bob, “bob”).Result;
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
result = userMgr.AddClaimsAsync(bob, new Claim[]
{
new Claim(JwtClaimTypes.Name, “Bob Smith”),
new Claim(JwtClaimTypes.GivenName, “Bob”),
new Claim(JwtClaimTypes.FamilyName, “Smith”),
new Claim(JwtClaimTypes.Email, “BobSmith@email.com”),
new Claim(JwtClaimTypes.EmailVerified, “true”, ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.WebSite, “http://bob.com”),
new Claim(JwtClaimTypes.Address, @”{ ‘street_address’: ‘One Hacker Way’, ‘locality’: ‘Heidelberg’, ‘postal_code’: 69118, ‘country’: ‘Germany’ }”,
IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json),
new Claim(“location”, “somewhere”)
}).Result;
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
Console.WriteLine(“bob created”);
}
else
{
Console.WriteLine(“bob already exists”);
}
}
}
Felix, your code does not compile for me in 3.0 or 3.1. With these changes it does:
The first few lines:
using (var scope = app.ApplicationServices.CreateScope())
{
var userMgr = (UserManager)scope.ServiceProvider.GetService(typeof(UserManager));
//var context = scope.ServiceProvider.GetService();
//context.Database.Migrate();
var alice = userMgr.FindByNameAsync(“alice”).Result;
and then you need a longer password otherwise there is an exception:
// at least 6 characters or there is an exception !!
var result = userMgr.CreateAsync(alice, “My long 123$ password”).Result;
and just for completness in Configure() in startup.cs the method has to be called
bool seed = Configuration.GetSection(“Data”).GetValue(“Seed”);
if (seed)
{
string connectionString = Configuration.GetConnectionString(“DefaultConnection”);
InitializeDatabase(app);
EnsureSeedData(app, connectionString);
throw new Exception(“Seeding completed. Disable the seed flag in appsettings”);
}
The Main() method looks quite different as well, compared to the version I got when following the QuickStart tutorial. Later version, I assume.
Yes. We are in the process of updating tutorials to .NET Core 3.1.
Getting this while trying to seed: System.InvalidOperationException: Unable to resolve service for type ‘Microsoft.Extensions.Logging.ILogger`1[Microsoft.AspNetCore.Identity.UserManager`1[IdentityServer.Models.ApplicationUser]]’ while attempting to activate ‘Microsoft.AspNetCore.Identity.UserManager`1[IdentityServer.Models.ApplicationUser]’.
We are in the process of updating tutorials to .NET Core 3.1.
See my reply
I solved the problem by adding services.AddLogging();
to the EnsureSeedData method in Users class.
then run and everything went as expected.
The workarounds said in the comments worked for me, thank you.
I was trying to use the forgot password pages (and other pages under the Identity area) but the “Identity” area seems to not work using services.AddControllersWithViews(). What can I do to get these pages to work?
Thank you in advance.
The best advice I can give you at the moment is to take a look at our repo with ID4 demos for .NET Core 3.1 https://github.com/Deblokt/IdentityServer4Demos.NETCore31 (still work in progress and we need to update the tutorials) but you might find what you need in there. Or the alternative is to hold tight and let us resolve all issues and update the tutorials to .NET Core 3.1. It should be done in a week or two. Thanks
I fixed this by adding…
services.AddLogging();
below newing up the services object.
var services = new ServiceCollection();
services.AddLogging();
services.AddDbContext(options =>
options.UseSqlServer(connectionString));
services.AddIdentity()
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
using (var serviceProvider = services.BuildServiceProvider())
{
using (var scope = serviceProvider.GetRequiredService().CreateScope())
{
var context = scope.ServiceProvider.GetService();
context.Database.Migrate();
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager>();
var alice = userMgr.FindByNameAsync(“alice”).Result;
if (alice == null)
Is it possible to register users using ID4?
ID4 doesn’t care much about the users. ASP.NET Core Identity is used for user management. Try http://localhost:5000/Identity/Account/Register
Hi,
Thank you for good tutorials.
Any indication when this one will be updated? The source in NetCore31 is very different to this tutorial now. For newbie like me it’s a show stopper. While it states User seeding was added, when I change the connection string to point to a non-existent database, nothing happens and it’s not being created. Don’t know what I’m missing. Thanks
Hi. Thank you. Tutorials for .NET Core 3.1 will be published today.
I was having the same Ilogger problem and I fix it by fixing the program.cs :
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
//.WriteTo.RollingFile(Path.Combine(Directory.GetCurrentDirectory() + @”\Logs”, “Serilog-{Date}.txt”)) //Store log in a file
.WriteTo.Console() // Display on the console as well
//.WriteTo.Console(new RenderedCompactJsonFormatter())
//.WriteTo.RollingFile(new RenderedCompactJsonFormatter(), Path.Combine(Directory.GetCurrentDirectory() + @”\Logs”, “Serilog-{Date}.ndjson”))
.CreateLogger();
try
{
Log.Information(“Starting up”);
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, “Application start-up failed”);
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
});
}
Comments are closed.