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:
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:
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:
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.