IdentityServer4 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core which acts as a middleware layer for managing authentication and authorization.

Configuration data for the IdentityServer4 service can be persisted in a variety of storage mediums including Microsoft SQL Server, MySQL and PostgreSQL and if you want to use IdentityServer4 in a production enviroment then you’re going to need some way to manage users and permissions that doesn’t involve inserting directly into the underlying tables. That’s where Skoruba/IdentityServer4.Admin comes in, this is an open source project to provide a web front end for administrating IdentityServer4 and ASP.NET Core Identity services.

Skoruba.IdentityServer4.Admin is a very useful project but I found its documentation a bit confusing so I figured I’d try and document all the steps required to get an ASP.NET Core Identity service up and running with a couple of web applications secured with role and claim authorization and management via the admin interface.

Creation

Create an empty solution, I called mine “Identity”.

Install the Skoruba.IdentityServer4.Admin templates via developer command prompt as in the documentation.

dotnet new -i Skoruba.IdentityServer4.Admin.Templates::1.0.0-rc1-update2

Scaffold the required projects using the below command (obviously use a different password in a production enviroment).

dotnet new skoruba.is4admin --name Identity --title Identity --adminemail "admin@example.com" --adminpassword "Pa$$word123" --adminrole Administrator --adminclientid AdministratorClientId --adminclientsecret AdministratorClientSecret --dockersupport false

Add the created projects to your solution.

I used a Microsoft SQL Server database hosted in Azure to persist the data, I created a user and connection string and replaced all the placeholder connection strings in the projects.

In the solution properties set the Identity.STS.Identity, Identity.Admin and Identity.Admin.Api projects to run at startup.

Run the solution, this will start the above 3 projects and create the required tables. For me this fails initially but then works as expected on the 2nd attempt. The following 3 web browser windows will then open.

Identity.Admin.Api

Identity.STS.Identity

Identity.Admin

The Identity.STS.Identity instance runs on http://localhost:5000 and Identity.Admin runs on http://localhost:9000 but then redirects to the Identity.STS.Identity on 5000 for authentication before redericting back to 9000.

You can log into Identity.Admin using the credentials you provided in the scaffolding arguments, these can also be found in Identity.Admin > identitydata.json.

Upon initial login the admin client specified in the scaffolding arguments “AdministratorClientId” requests delegated permissions to log in on you behalf.

Once granted you can then manage all the resources in your IdentityServer4 instance running in Identity.STS.Identity.

To use this STS for authentication and role based authorization add a new ASP.NET Core Web Aplication to your solution. I created this as an MVC project with the name “Identity.Web.Role”.

To add the necessary files for authentication and authorization follow the instructions here and point it at your _Layout.cshtml file.

Once this has completed update the connection string in appsettings.json and install the NuGet package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.

In Startup.cs file update the ConfigureServices and Configure methods.

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllersWithViews();
	services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
		app.UseDatabaseErrorPage();
	}
	else
	{
		app.UseExceptionHandler("/Home/Error");
		app.UseHsts();
	}
	app.UseHttpsRedirection();
	app.UseStaticFiles();

	app.UseRouting();

	app.UseAuthentication();
	app.UseAuthorization();

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapControllerRoute(
			name: "default",
			pattern: "{controller=Home}/{action=Index}/{id?}");
		endpoints.MapRazorPages();
	});
}

In Views/Shared/_Layout.cshtml at the login partial.

<partial name="_LoginPartial" />

By default ASP.NET Core Identity uses slightly different table names to IdentityServer4 so if you want this project to correctly map to the IdentityServer4 tables created previously you need to specify the table names for the classes in the OnModelCreating method of Areas/Identity/Data/IdentityWebRoleContext.cs.

protected override void OnModelCreating(ModelBuilder builder)
{
	base.OnModelCreating(builder);
	// Customize the ASP.NET Identity model and override the defaults if needed.
	// For example, you can rename the ASP.NET Identity table names and more.
	// Add your customizations after calling base.OnModelCreating(builder);

	builder.Entity<IdentityRoleClaim<string>>()
				.ToTable("RoleClaims", "dbo");

	builder.Entity<IdentityRole>()
				.ToTable("Roles", "dbo");

	builder.Entity<IdentityUserClaim<string>>()
				.ToTable("UserClaims", "dbo");

	builder.Entity<IdentityUserLogin<string>>()
				.ToTable("UserLogins", "dbo");

	builder.Entity<IdentityUserRole<string>>()
				.ToTable("UserRoles", "dbo");

	builder.Entity<IdentityUser>()
				.ToTable("Users", "dbo");

	builder.Entity<IdentityUserToken<string>>()
				.ToTable("UserTokens", "dbo");
}

To tell ASP.NET Core Identity to use roles they need to be added to the default identity model in Areas/Identity/IdentityHostingStartup.cs

public void Configure(IWebHostBuilder builder)
{
	builder.ConfigureServices((context, services) => {
		services.AddDbContext<IdentityWebContext>(options =>
			options.UseSqlServer(
				context.Configuration.GetConnectionString("IdentityWebRoleContextConnection")));

		services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
			.AddRoles<IdentityRole>()
			.AddEntityFrameworkStores<IdentityWebContext>();
	});
}

ASP.NET Core Identity defaults to using email and password to login whereas IdentityServer4 expects username and password. Either could be changed but I decided to change my login page to take username rather than email address. The login page and code behind it can be found in Areas/Pages/Account/Login.cshtml and Areas/Pages/Account/Login.cshtml.cs.

public class InputModel
{
    [Required]
    [Display(Name = "User Name")]
    public string UserName { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Display(Name = "Remember me?")]
    public bool RememberMe { get; set; }
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation("User logged in.");
            return LocalRedirect(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning("User account locked out.");
            return RedirectToPage("./Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return Page();
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}
<div class="form-group">
    <label asp-for="Input.UserName"></label>
    <input asp-for="Input.UserName" class="form-control" />
    <span asp-validation-for="Input.UserName" class="text-danger"></span>
</div>
<div class="form-group">
    <label asp-for="Input.Password"></label>
    <input asp-for="Input.Password" class="form-control" />
    <span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-group">
    <div class="checkbox">
        <label asp-for="Input.RememberMe">
            <input asp-for="Input.RememberMe" />
            @Html.DisplayNameFor(m => m.Input.RememberMe)
        </label>
    </div>
</div>

In the Admin portal add a new user and a new role, WebUser in my case, and assign this role to the new user. In Controllers/HomeController.cs in your web application add the “Authorize” attribute to the Index method to allow your new user to access this page but not the Privacy page.

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    [Authorize(Roles = "WebUser")]
    public IActionResult Index()
    {
        return View();
    }

    public IActionResult Privacy()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

Once done run the solution to startup the identity serverces and then debug the Identity.Web.Role project and log in with the newly created user, you should be allowed access to both the Index page and the Privacy page. If you then log in as the administrator you should only have access to the Privacy page and not the Index page.


4 Comments

Houssam · 30 July 2023 at 12:52 pm

Is the source code for the project with the client available? Please provide it

    Shinigami · 31 July 2023 at 2:24 pm

    Hi, sorry there’s no source code available for this project but you might be able to find more details on the Skoruba.IdentityServer4.Admin page.

    https://github.com/skoruba/IdentityServer4.Admin

      Houssam · 5 August 2023 at 7:44 am

      The problem is in the settings of the client application.. This you did not explain in your article..! And it’s very vague in the SkORUba documents, Thanks

Houssam · 9 August 2023 at 12:53 pm

In your article dotnet core 5 used, In my project I don’t have IdentityHostingStartup file in dotnet core 6 , can i create it manually and how to convert it to dotnet cor 6?

Leave a Reply

Avatar placeholder

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