jwtrole-base-authorizationduende-identity-serverangular15asp.net-core-7.0

How do you modify the client and api scopes that are predefined in IdentityServer 7 when creating a web app with individual accounts?


I have been stuck on this problem for a few days now. I have a web application being built on .NET Core 7, IdentityServer 7, EntityFramework 7, and Angular 15 and written in C#. The scope in the JWT contains a scope of (MyAppAPI, openid, and profile). I am trying to find a way to add roles to the scope. I've tried several approaches, but all of them are directed towards creating new IdentityResources, Clients, and ApiScopes. This approach throws errors because they already exist from IdentityServer 7.

Hoping someone can help. Thanks.

My latest effort consisted of applying option arguments to the builder.Services.AddIdentityServer().AddApiAuthorization<ApplicationUser, ApplicationDbContext>() method in the Program.cs file. But I get an error saying "Can't determine the type for the client type". So I don't know if I'm close to getting this all resolved or am way off track.

Here are the contents of my Program.cs file:

using Duende.IdentityServer.AspNetIdentity;
using Duende.IdentityServer.EntityFramework.Entities;
using Duende.IdentityServer.Models;
using AdminPortal.Areas.Identity.Data;
using AdminPortal.Areas.Identity.Models;
using AdminPortal.Framework;
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.AzureAppServices;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);
string envName = string.IsNullOrEmpty(builder.Configuration["configEnvName"]) ? "development" : builder.Configuration["configEnvName"].ToString();

builder.Configuration.AddJsonFile("appsettings.json").AddJsonFile($"appsettings.{envName}.json");

builder.Logging.AddAzureWebAppDiagnostics();
builder.Services.Configure<AzureFileLoggerOptions>(options =>
{
    options.FileName = "AdminPortal-diagnostics-";
    options.FileSizeLimit = 50 * 1024;
    options.RetainedFileCountLimit = 5;
});
builder.Services.Configure<AzureBlobLoggerOptions>(options =>
{
    options.BlobName = "log.txt";
});

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();


builder.Services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
        {
            options.IdentityResources = Config.IdentityResources;
            options.Clients = Config.Clients;
            options.ApiScopes = Config.ApiScopes;
        })
        .AddProfileService<ProfileService>();

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

//builder.Services.AddScoped<IClaimsTransformation, ClaimsTransformer>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

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

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

app.MapFallbackToFile("index.html"); ;

app.Run();

And here are the contents of Config.cs:

using Duende.IdentityServer.Models;
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
using System.Collections.Generic;

namespace AdminPortal.Framework
{
    public static class Config
    {
        public static IdentityResourceCollection IdentityResources =>
            new IdentityResourceCollection(
                new IdentityResource[]
                {
                    new IdentityResources.OpenId(),
                    new IdentityResources.Profile(),
                    //new IdentityResources.Email(), // Can implement later if needed
                    //new IdentityResources.Phone(), // Can implement later if needed
                    //new IdentityResources.Address(), // Can implement later if needed
                    new IdentityResource("roles", "User roles", new List<string> { "role" })
                });



        public static ApiScopeCollection ApiScopes => 
            new ApiScopeCollection(
                new ApiScope[]
                {
                    new ApiScope("AdminPortalAPI"),
                    new ApiScope("openid"),
                    new ApiScope("profile"),
                    new ApiScope("roles")
                }
            );

        public static ClientCollection Clients => 
            new ClientCollection(
                new Client[]
                {
                    new Client
                    {
                        ClientId = "AdminPortalAPI",
                        ClientName = "AdminPortal Credentials Client",
                        AllowedGrantTypes = GrantTypes.ClientCredentials,
                        AccessTokenType = AccessTokenType.Jwt,
                        ClientSecrets = { new Secret("AdminPortal_client_secret".Sha256()) },
                        AllowedScopes =
                        {
                            "AdminPortalAPI"
                        }
                    },
                    new Client
                   {
                        ClientId = "AdminPortal",
                        ClientName = "AdminPortal SPA",
                        AllowedGrantTypes = GrantTypes.Code,
                        AccessTokenType = AccessTokenType.Jwt,
                        RequirePkce = true,
                        RequireClientSecret = false,
                        AllowedScopes = { "openid", "profile", "AdminPortalAPI", "roles" },
                        RedirectUris = { https://localhost:44463/auth-callback },
                        PostLogoutRedirectUris = { https://localhost:44463/ },
                        AllowedCorsOrigins = { https://localhost:44463 },
                        AllowOfflineAccess = true
                    }
                }
            );
    }
}


Solution

  • I found the solution to my problem. So I'll report it here for anyone else that finds themselves struggling with this issue or similar.

    If you create a new project in Visual Studio and tell it to include Individual Accounts, it will use IdentityServer to build out an authentication framework that will make it easy to manage user accounts and authenticate users. However, if you want to implement role-based security, you'll have to build it out manually because the preconfigured code only partially implements IdentityServer and is not designed to let you customize the scopes (reference link: https://github.com/dotnet/aspnetcore/issues/16939).

    To resolve this issue, I found a great tutorial that helped me build out the authentication and authorization framework using IdentityServer for my Angular 15 .NET Core 7 web application. Here is the link to it: https://code-maze.com/angular-security-with-asp-net-core-identity/