Supporting Core Identity Roles in IdentityServer4

595 Views Asked by At

I'm creating a Single-Sign-on server using IdentiyServer4. I've looked at their QuickStarts showing how to integrate MS Core Identity with ASP.NET Core 3.1 apps. But there's no examples showing whether ASP.NET roles are natively supported in MVC controllers. A few experiments seemed to indicate that they aren't. But when I discovered that role data can be returned in the Access Token, I wrote my own action filter that authorises users.

However, looking at the documentation for IdentityServer3, they do briefly show roles being used in MVC controllers. So now I'm completely confused. But apart from that, there's no documentation that I can find, and the only mention online I could find about roles with IdentityServer were about a different issue - using roles to control access to remote APIs.

My filter isn't working that well, and I'm worried it's the wrong approach and unnecessary. Can anyone either enlighten me, or point me to any resources that would help.

2

There are 2 best solutions below

0
Tore Nestenius On

One gotcha, is that you need to configure and tell ASP.NET Core what the name of the roles claim is in the incoming token.

Out of the box IdentityServer and Microsoft does not agree on the name of the roles claim.

So, you need to set the RoleClaimType.

   .AddOpenIdConnect(options =>
   {
       // other options...
       options.TokenValidationParameters = new TokenValidationParameters
       {
         NameClaimType = "email", 
         RoleClaimType = "role"
       };
   });

To complement this answer, I wrote a blog post that goes into more detail about this topic: Debugging OpenID Connect claim problems in ASP.NET Core

0
Mahmood On

I hope these codes will be useful for you.

I added ASP.NET Core Identity in the IdentityServer project.

Statup.cs in API Client

public class Startup
{
    public IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        services.AddControllers();
        services.AddAuthentication("Bearer")
                .AddJwtBearer("Bearer", options =>
                {
                    options.Authority = "https://localhost:5001";
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateAudience = false
                    };
                });
        services.AddAuthorization(options =>
        {
            options.AddPolicy("ApiScope", policy =>
            {
                policy.RequireAuthenticatedUser();
                policy.RequireClaim("scope", "api1");
            });
        });
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers()
                      .RequireAuthorization("ApiScope");
        });
    }
}

Startup.cs in MVC Client

public class Startup
{
  public IConfiguration Configuration { get; }
  public Startup(IConfiguration configuration)
  {
    Configuration = configuration;
  }
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddControllersWithViews();
    JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
    services.AddAuthentication(options =>
    {
      options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
      options.Authority = "https://localhost:5001";
      options.ClientId = "mvc";
      options.ClientSecret = "secret";
      options.ResponseType = "code id_token";
      options.Scope.Add("email");
      options.Scope.Add("roles");
      options.ClaimActions.DeleteClaim("sid");
      options.ClaimActions.DeleteClaim("idp");
      options.ClaimActions.DeleteClaim("s_hash");
      options.ClaimActions.DeleteClaim("auth_time");
      options.ClaimActions.MapJsonKey("role", "role");
      options.Scope.Add("api1");
      options.SaveTokens = true;
      options.GetClaimsFromUserInfoEndpoint = true;
      options.TokenValidationParameters = new TokenValidationParameters
      {
        NameClaimType = "name",
        RoleClaimType = "role"
      };
    });
    services.AddTransient<AuthenticationDelegatingHandler>();
    services.AddHttpClient("ApplicationAPI", client =>
    {
      client.BaseAddress = new Uri("https://localhost:5002/");
      client.DefaultRequestHeaders.Clear();
      client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
    }).AddHttpMessageHandler<AuthenticationDelegatingHandler>();

    services.AddHttpClient("ApplicationIdentityServer", client =>
    {
      client.BaseAddress = new Uri("https://localhost:5001/");
      client.DefaultRequestHeaders.Clear();
      client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
    });
    services.AddHttpContextAccessor();
  }
  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  {
    if (env.IsDevelopment())
    {
      app.UseDeveloperExceptionPage();
    }
    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: "{area=Admin}/{controller=Home}/{action=Index}/{id?}");
    });
  }
}

AuthenticationDelegatingHandler in MVC Application

To prevent getting token again.

public class AuthenticationDelegatingHandler : DelegatingHandler
{
  private readonly IHttpContextAccessor _httpContextAccessor;

  public AuthenticationDelegatingHandler(IHttpContextAccessor httpContextAccessor)
  {
    _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
  }

  protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  {
    var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);

    if (!string.IsNullOrWhiteSpace(accessToken))
    {
      request.SetBearerToken(accessToken);
    }

    return await base.SendAsync(request, cancellationToken);
  }
}

Config.cs in IdentityServer

public static class Config
{
  public static IEnumerable<IdentityResource> IdentityResources =>
      new List<IdentityResource>
      {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResources.Email(),
        new IdentityResource("roles", "Your role(s)", new List<string>() { "role" })
      };
  public static IEnumerable<ApiScope> ApiScopes =>
      new List<ApiScope>
      {
        new ApiScope("api1", "My API")
      };

  public static IEnumerable<Client> Clients =>
    new List<Client>
    {
      new Client
      {
          ClientId = "client",
          ClientSecrets = { new Secret("secret".Sha256()) },
          AllowedGrantTypes = GrantTypes.ClientCredentials,
          AllowedScopes = { "api1" }
      },
      new Client
      {
          ClientId = "mvc",
          ClientName = "Application Web",
          AllowedGrantTypes = GrantTypes.Hybrid,
          ClientSecrets = { new Secret("secret".Sha256()) },
          RequirePkce = false,
          AllowRememberConsent = false,
          RedirectUris = { "https://localhost:5003/signin-oidc" },
          PostLogoutRedirectUris = { "https://localhost:5003/signout-callback-oidc" },

          AllowedScopes = new List<string>
          {
              IdentityServerConstants.StandardScopes.OpenId,
              IdentityServerConstants.StandardScopes.Profile,
              IdentityServerConstants.StandardScopes.Email,
              "api1",
              "roles"
          }
      }
    };
}

Startup.cs in IdentityServer

public class Startup
{
  public IWebHostEnvironment Environment { get; }
  public IConfiguration Configuration { get; }
  public Startup(IWebHostEnvironment environment, IConfiguration configuration)
  {
    Environment = environment;
    Configuration = configuration;
  }
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddControllersWithViews();
    services.AddRazorPages()
        .AddRazorPagesOptions(options =>
        {
          options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
        });
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
    {
      options.SignIn.RequireConfirmedEmail = true;
    })
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    var builder = services.AddIdentityServer(options =>
    {
      options.Events.RaiseErrorEvents = true;
      options.Events.RaiseInformationEvents = true;
      options.Events.RaiseFailureEvents = true;
      options.Events.RaiseSuccessEvents = true;
      options.EmitStaticAudienceClaim = true;
      options.UserInteraction.LoginUrl = "/Account/Login";
      options.UserInteraction.LogoutUrl = "/Account/Logout";
      options.Authentication = new AuthenticationOptions()
      {
        CookieLifetime = TimeSpan.FromHours(10),
                CookieSlidingExpiration = true
      };
    })
        .AddInMemoryIdentityResources(Config.IdentityResources)
        .AddInMemoryApiScopes(Config.ApiScopes)
        .AddInMemoryClients(Config.Clients)
        .AddAspNetIdentity<ApplicationUser>();

    if (Environment.IsDevelopment())
    {
      builder.AddDeveloperSigningCredential();
    }
    services.AddAuthentication()
        .AddGoogle(options =>
        {
          options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
          options.ClientId = "copy client ID from Google here";
          options.ClientSecret = "copy client secret from Google here";
        });

    services.AddTransient<IEmailSender, EmailSender>();
  }
  public void Configure(IApplicationBuilder app)
  {
    if (Environment.IsDevelopment())
    {
      app.UseDeveloperExceptionPage();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseIdentityServer();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
      endpoints.MapControllerRoute(
                  name: "default",
                  pattern: "{controller=Home}/{action=Index}/{id?}");
      endpoints.MapRazorPages();
    });
  }
}