fbpx

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.

Comments
  • BOUCHER Guillaume says:

    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]’.

    • deblokt says:

      Please check maybe you are using “IdentityUser” instead of “ApplicationUser” somewhere. Make sure to always use “ApplicationUser” type when registering services.

      • Frank says:

        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.

        • deblokt says:

          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.

          • Frank says:

            I forgot to say in my answer that I did as you’ve said, but I got the same error 🙁

          • Ahmed Graihy says:

            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.

          • Mike says:

            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.

  • Felix says:

    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”);
    }
    }
    }

    • Stephan says:

      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”);
      }

  • Gigante says:

    The Main() method looks quite different as well, compared to the version I got when following the QuickStart tutorial. Later version, I assume.

  • Gigante says:

    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]’.

  • Frank says:

    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.

    • deblokt says:

      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

  • Darren Street says:

    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)

  • Piyush says:

    Is it possible to register users using ID4?

  • Serg says:

    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

  • Anthony says:

    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.