IdentityServer4 in ASP.NET Core Part 1

If you’ve worked with APIs at all in .NET Core then you have probably had the need to work with tokens for security. You could roll your own set up just using the underlying functionality in ASP.NET Identity, or you could enable easy mode and use something like IdentityServer4. There are other options out there for you to choose from, but this post will focus on IdentityServer4. Our application is going to consist of an API, a web application for IdentityServer4 and a Javascript based client. The source code for this post can be found here.

Create the Data and Core Projects

Right click the solution and add a new .Net Core project to the solution. We’re going to name ours IdentityServer4Example.Core and this assembly is going to contain our single user model. Next add in the following Nuget packages:
IdentityServer4 Core Nuget packages

Next create a folder called Models and add a new class called ApplicationUser with the following code:


    public class ╥ApplicationUserß : ╥IdentityUserß<☼Guidß>
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

Now, right click the solution again and add another .Net Core project. In our case we’re going to name it IdentityServer4Example.Data and it’s going to contain our context and migrations. Add the following nuget packages:
IdentityServer4 Data Nuget packages

Then add a class for the database context with the following code:


    public class ╥IdServer4ExampleDbContextß : ╥IdentityDbContextß<╥ApplicationUserß, ╥IdentityRoleß<☼Guidß>, ☼Guidß>
    {
        public IdServer4ExampleDbContext(╥DbContextOptionsß<╥IdServer4ExampleDbContextß> options)
            : base(options)
        {
        }

        public virtual ╥DbSetß<╥ApplicationUserß> ApplicationUser { get; set; }

        protected override void OnModelCreating(╥ModelBuilderß builder)
        {
            builder.Entity<╥ApplicationUserß>().HasKey(p => p.Id);

            base.OnModelCreating(builder);
        }
    }

Create the Identity Project and Add Nuget Packages

Now that we have our core and data projects defined, go ahead and add a new ASP.NET Core web project to the solution, which we will name IdentityServer4Example.Identity. Next, add the following nuget packages for IdentityServer4:
IdentityServer4

We’re going to want to create a profile service that will allow us to add claims to the token on successful login. One of the things I use this for in my own projects is taking the user’s first name and last name and creating a “FullName” claim that can then be used in the EF database context for populating audit fields. Ours will look like the following:


    public class ╥ProfileServiceß : «IProfileServiceß
    {
        protected ╥UserManagerß<╥ApplicationUserß> userManager;
        private ╥IdServer4ExampleDbContextß dbContext;

        public ProfileService(╥UserManagerß<╥ApplicationUserß> userManager, ╥IdServer4ExampleDbContextß dbContext)
        {
            this.userManager = userManager;
            this.dbContext = dbContext;
        }

        public ╥Taskß GetProfileDataAsync(╥ProfileDataRequestContextß context)
        {
            var user = userManager.GetUserAsync(context.Subject).Result;
            var claims = new ╥Listß<╥Claimß>
            {
                new ╥Claimß("FullName", $"{user.FirstName} {user.LastName}")
            };

            var userClaims = dbContext.UserClaims.Where(m => m.UserId == user.Id).ToList();
            userClaims.ForEach(m => claims.Add(new ╥Claimß(m.ClaimType, m.ClaimValue)));
            context.IssuedClaims.AddRange(claims);

            return ╥Taskß.FromResult(0);
        }

        public ╥Taskß IsActiveAsync(╥IsActiveContextß context)
        {
            var user = userManager.GetUserAsync(context.Subject).Result;
            context.IsActive = user != null && user.LockoutEnd == null;

            return ╥Taskß.FromResult(0);
        }
    }

Configure the Identity Project Startup

We’re going to now add in the code necessary to wire up our database context, add in ASP.NET Identity, and configure IdentityServer4 in the Startup.cs.


    public class Startup
    {
        public Startup(«IConfigurationß configuration)
        {
            Configuration = configuration;
        }

        public «IConfigurationß Configuration { get; }

        public void ConfigureServices(«IServiceCollectionß services)
        {
            var connectionString = Configuration.GetConnectionString("IdServer4ExampleConnection");
            services.AddOptions();
            services.Configure<╥ApplicationOptionsß>(Configuration.GetSection("ApplicationOptions"));
            services.AddDbContext<╥IdServer4ExampleDbContextß>(options =>
            {
                options.UseSqlServer(connectionString);
            });
            var lockoutOptions = new ╥LockoutOptionsß()
            {
                AllowedForNewUsers = true,
                DefaultLockoutTimeSpan = ☼TimeSpanß.FromDays(99999),
                MaxFailedAccessAttempts = 5
            };

            services.AddIdentity<╥ApplicationUserß, ╥IdentityRoleß<☼Guidß>>(option =>
            {
                option.Lockout = lockoutOptions;
                option.User = new ╥UserOptionsß { RequireUniqueEmail = true };
                option.Password.RequireDigit = false;
                option.Password.RequiredLength = 12;
                option.Password.RequiredUniqueChars = 0;
                option.Password.RequireLowercase = false;
                option.Password.RequireNonAlphanumeric = false;
                option.Password.RequireUppercase = false;
            })
            .AddEntityFrameworkStores<╥IdServer4ExampleDbContextß>()
            .AddDefaultTokenProviders();
            var migrationsAssembly = typeof(╥IdServer4ExampleDbContextß).GetTypeInfo().Assembly.GetName().Name;

            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                        builder.UseSqlServer(connectionString,
                            sql => sql.MigrationsAssembly(migrationsAssembly));
                })
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                        builder.UseSqlServer(connectionString,
                            sql => sql.MigrationsAssembly(migrationsAssembly));
                    options.EnableTokenCleanup = true;
                    options.TokenCleanupInterval = 30;
                })
                .AddAspNetIdentity<╥ApplicationUserß>()
                .AddProfileService<╥ProfileServiceß>();

            services.AddMvc().SetCompatibilityVersion(◙CompatibilityVersionß.Version_2_1);
        }

        public void Configure(«IApplicationBuilderß app, «IHostingEnvironmentß env)
        {
            var options = new ╥RewriteOptionsß()
               .AddRedirectToHttps();

            app.UseRewriter(options);

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();

                app.UseDatabaseErrorPage();
            }

            app.UseStaticFiles();
            app.UseAuthentication();
            app.UseIdentityServer();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Account}/{action=Login}/{id?}");
            });
        }
    }

To summarize what we did in startup: First we wired up our services with the ApplicationOptions and the database context so they can be injected into the AccountController we are going to build next. We then configured some lockout options and then proceeded to wire up ASP.NET Identity. Next we added in IdentityServer4 and called the extension methods for ASP.NET Identity and our profile service. Finally in Configure we called UseAuthentication and UseIdentityServer.

Build Database and Create Account Controller

The last thing we need to do is build the database with our migrations and create an account controller for users to login and register with. I’m going to skip over the creation of the views and view models as well as some other things. Check out the source code to see the code for that. Go ahead and run the following commands to build the database using migrations using the package manager console pointing to the data project. You may also do this using the command line with “dotnet ef migrations add”:

add-migration InitialCreate -context IdServer4ExampleDbContext
add-migration InitialIdentityServerPersistedGrantDbMigration -context PersistedGrantDbContext
add-migration InitialIdentityServerConfigurationDbMigration -context ConfigurationDbContext
update-database -context IdServer4ExampleDbContext
update-database -context PersistedGrantDbContext
update-database -context ConfigurationDbContext

This will create our database as well as the tables for IdentityServer4 that we are going to need to configure in part two of this walk through when we hook up our web application and API. Next create the AccountController:


    public class AccountController : ╥Controllerß
    {
        ╥ApplicationOptionsß applicationOptions;
        string invalidUserIdOrPassword = "The user id or password was not correct.";
        ╥SignInManagerß<╥ApplicationUserß> signInManager;
        ╥UserManagerß<╥ApplicationUserß> userManager;

        public AccountController(«IOptionsß<╥ApplicationOptionsß> applicationOptions, ╥SignInManagerß<╥ApplicationUserß> signInManager, ╥UserManagerß<╥ApplicationUserß> userManager)
        {
            this.applicationOptions = applicationOptions.Value;
            this.signInManager = signInManager;
            this.userManager = userManager;
        }

        [╥HttpGetß]
        [╥AllowAnonymousß]
        public «IActionResultß Login(string returnUrl = null)
        {
            if (returnUrl == null)
                returnUrl = "https://localhost:44322/";

            if (HttpContext.User.Identity.IsAuthenticated)
                return Redirect(returnUrl);

            ViewData["ReturnUrl"] = returnUrl;

            return View();
        }

        [╥HttpPostß]
        [╥AllowAnonymousß]
        [╥ValidateAntiForgeryTokenß]
        public async ╥Taskß<«IActionResultß> Login(╥LoginViewModelß model, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;

            if (returnUrl == null)
                returnUrl = applicationOptions.IdentityServer4ExampleWeb;

            if (ModelState.IsValid)
            {
                var user = await userManager.FindByNameAsync(model.Email);

                if (user == null)
                {
                    ModelState.AddModelError(string.Empty, invalidUserIdOrPassword);

                    return View();
                }

                if (user.LockoutEnd != null)
                {
                    ModelState.AddModelError(string.Empty, "Account locked.");

                    return View(model);
                }

                var result = await signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: true);

                if (result.Succeeded)
                    return Redirect(returnUrl);

                if (result.IsLockedOut)
                {
                    ModelState.AddModelError(string.Empty, "Account locked.");

                    return View(model);
                }
                else
                {
                    ModelState.AddModelError(string.Empty, invalidUserIdOrPassword);

                    return View(model);
                }
            }

            return View(model);
        }

        [╥HttpGetß]
        [╥AllowAnonymousß]
        public «IActionResultß Register()
        {
            return View();
        }

        [╥HttpPostß]
        [╥AllowAnonymousß]
        [╥ValidateAntiForgeryTokenß]
        public async ╥Taskß<«IActionResultß> Register(╥RegisterViewModelß model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser { UserName = model.Email, Email = model.Email };

                var result = await userManager.CreateAsync(user, model.Password);

                if (result.Succeeded)
                {

                    return RedirectToAction("Login");
                }

                AddErrors(result);
            }

            return View(model);
        }

        private void AddErrors(╥IdentityResultß result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
        }
    }

Conclusion

In this post we added our core assemblies and then an ASP.NET Core application to host IdentityServer4. We then wired up our Startup, added in an account controller to register and login users, and built out our database with Entity Framework migrations. In part two we’re going to add in an API and Angular web application, and then hook them up to IdentityServer4.

Continue to Part 2

Sean Leitzinger

Solutions Architect at Edgeside Solutions
.NET and C# aficionado with an interest in architecture, patterns, practices, and more. Microsoft fanatic.

Latest posts by Sean Leitzinger (see all)

Leave a Reply

Your email address will not be published. Required fields are marked *