Search code examples
c#asp.net-identityidentityserver4claims-based-identity

IdentityServer4 custom AuthenticationHandler can't find all claims for a user


I am using the IdentityServer4 sample that uses Asp.Net Identity and EntityFramework.

I am trying to create group controls using custom policies based on claims/roles.

My problem is that when I try and get the users claims in the authorization handler the claims I am looking for are not returned.

Looking at the database in SSMS I find the claims/roles I created are in tables called "AspNetRoles", "AspNetRoleClaims", "AspNetUserClaims" along with the user I created being in "AspNetUsers" and keys for the user and role being in "AspNetUserRoles". When I call to get the users claims for authorization the list of claims seems to come from the "IdentityClaims" table.

There doesn't seem to be a simple way to check for claims in "AspNetClaims" like there is for claims in "IdentityClaims" so I assume I've made an error somewhere.

I've looked around a fair bit for a solution and tried a fair few things but I can't find anything that works.

Below is the code i thought would me most relevant to the question along with some screenshots taken of the running code.

Any help would be much appreciated, thanks in advance.


Code

MvcClient.Startup

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    services.AddAuthorization(options =>
    {
        options.AddPolicy("AdminRights", policy =>
        {                 
            policy.Requirements.Add(new AdminRequirement());
            policy.RequireAuthenticatedUser();
            policy.AddAuthenticationSchemes("Cookies");
        });
    });

    services.AddSingleton<IAuthorizationHandler, AdminRequirementHandler>();

    services.AddAuthentication(options =>
        {
            options.DefaultScheme = "Cookies";
            options.DefaultChallengeScheme = "oidc";
        })
        .AddCookie("Cookies")
        .AddOpenIdConnect("oidc", options =>
        {
            options.SignInScheme = "Cookies";

            options.Authority = "http://localhost:5000";
            options.RequireHttpsMetadata = false;

            options.ClientId = "mvc";
            options.ClientSecret = "secret";
            options.ResponseType = "code id_token token"; // NEW CHANGE (token)

            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;

            options.Scope.Add("api1");
            options.Scope.Add("AdminPermission"); // NEW CHANGE
            options.Scope.Add("offline_access");
        });
}

MvcClient.Controllers.HomeController

[Authorize(Policy = "AdminRights")]
public IActionResult Administrator()
{
    return View();
}

IdentityServerWithAspIdAndEF.Startup (Called last in Configure)

private async Task CreateSuperuser(IServiceProvider serviceProvider, ApplicationDbContext context)
{
    //adding custom roles
    var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
    var UserManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();

    string[] roleNames = { "Administrator", "Internal", "Customer" };

    foreach (var roleName in roleNames)
    {
        //creating the roles and seeding them to the database
        var roleExist = await RoleManager.RoleExistsAsync(roleName);

        if (roleExist)
            await RoleManager.DeleteAsync( await RoleManager.FindByNameAsync(roleName) );

        var newRole = new IdentityRole(roleName);
        await RoleManager.CreateAsync(newRole);

        if(roleName == "Administrator")
            await RoleManager.AddClaimAsync(newRole, new Claim("AdminPermission", "Read"));
    }

    //creating a super user who could maintain the web app
    var poweruser = new ApplicationUser
    {
        UserName = Configuration.GetSection("UserSettings")["UserEmail"],
        Email = Configuration.GetSection("UserSettings")["UserEmail"]
    };

    string UserPassword = Configuration.GetSection("UserSettings")["UserPassword"];


    var _user = await UserManager.FindByEmailAsync(Configuration.GetSection("UserSettings")["UserEmail"]);

    if (_user != null)
        await UserManager.DeleteAsync( await UserManager.FindByEmailAsync(Configuration.GetSection("UserSettings")["UserEmail"]) );

    var createPowerUser = await UserManager.CreateAsync(poweruser, UserPassword);

    if (createPowerUser.Succeeded)
    {
        //here we tie the new user to the "Admin" role 
        await UserManager.AddToRoleAsync(poweruser, "Administrator");
        await UserManager.AddClaimAsync(poweruser, new Claim("AdminPermission", "Create"));
        await UserManager.AddClaimAsync(poweruser, new Claim("AdminPermission", "Update"));
        await UserManager.AddClaimAsync(poweruser, new Claim("AdminPermission", "Delete"));

    }
}

IdentityServerWithAspIdAndEF.Config

public class Config
{
    // scopes define the resources in your system
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResource()  // NEW CHANGE
            {
                Name = "AdminPermission",
                DisplayName = "Admin Permission",
                UserClaims =
                {
                    "AdminPermission",
                }
            }
        };
    }

    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("api1", "My API")
            {
                Scopes = // NEW CHANGE
                {
                    new Scope("AdminPermission", "Admin Permission")
                    {
                        UserClaims = { "AdminPermission" }
                    }
                }
            }
        };
    }

    // clients want to access resources (aka scopes)
    public static IEnumerable<Client> GetClients()
    {
        // client credentials client
        return new List<Client>
        {
            // OpenID Connect hybrid flow and client credentials client (MVC)
            new Client
            {
                ClientId = "mvc",
                ClientName = "MVC Client",
                AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

                RequireConsent = false, // NEW CHANGE (false)

                ClientSecrets = 
                {
                    new Secret("secret".Sha256())
                },

                RedirectUris = { "http://localhost:5002/signin-oidc" },
                PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "api1"
                    "AdminPermission", // NEW CHANGE
                },
                AllowOfflineAccess = true,
            },

            // Other Clients omitted as not used.
        };
    }
}

IdentityServerWithAspIdAndEF.Startup

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    // Add application services.
    services.AddTransient<IEmailSender, EmailSender>();

    services.AddMvc();

    string connectionString = Configuration.GetConnectionString("DefaultConnection");
    var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

    // configure identity server with in-memory stores, keys, clients and scopes
    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddAspNetIdentity<ApplicationUser>()
        // this adds the config data from DB (clients, resources)
        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = builder =>
                    builder.UseSqlServer(connectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
        })
        // this adds the operational data from DB (codes, tokens, consents)
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = builder =>
                    builder.UseSqlServer(connectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));

            // this enables automatic token cleanup. this is optional.
            options.EnableTokenCleanup = true;
            options.TokenCleanupInterval = 30;
        });

    services.AddAuthentication()
        .AddGoogle("Google", options =>
        {
            options.ClientId = "434483408261-55tc8n0cs4ff1fe21ea8df2o443v2iuc.apps.googleusercontent.com";
                options.ClientSecret = "3gcoTrEDPPJ0ukn_aYYT6PWo";
        })
        .AddOpenIdConnect("oidc", "OpenID Connect", options =>
        {
            options.Authority = "https://demo.identityserver.io/";
            options.ClientId = "implicit";
            options.SaveTokens = true;

         // options.GetClaimsFromUserInfoEndpoint = true; // NEW CHANGE
         // options.ResponseType = "code id_token token";  // NEW CHANGE

            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "name",
                RoleClaimType = "role"
            };
        });
}

MvcClient.Authorization

public class AdminRequirementHandler : AuthorizationHandler<AdminRequirement>
{

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement)
    {

        Console.WriteLine("User Identity: {0}", context.User.Identity);
        Console.WriteLine("Role is 'Administrator'? : {0}", context.User.IsInRole("Administrator"));
        Console.WriteLine("Identities of user:-");
        foreach (var v in context.User.Identities)
        {
            Console.WriteLine("\tName: {0},\tActor: {1},\tAuthType: {2},\tIsAuth: {3}", v.Name, v.Actor, v.AuthenticationType, v.IsAuthenticated);

            Console.WriteLine("\n\tClaims from Identity:-");
            foreach (var c in v.Claims)
                Console.WriteLine("\t\tType: {0},\tValue: {1},\tSubject: {2},\tIssuer: {3}", c.Type, c.Value, c.Subject, c.Issuer);
        }

        Console.WriteLine("Claims from other source:-");

        foreach(Claim c in context.User.Claims)
        {
            Console.WriteLine("\t\tType: {0},\tValue: {1},\tSubject: {2},\tIssuer: {3}", c.Type, c.Value, c.Subject, c.Issuer);
        }

        Console.WriteLine("\n *** Starting Authroization. ***\n");

        Claim
            role = context.User.FindFirst("role"),
            accessLevel = context.User.FindFirst("AdminPermission");


        if (role == null)
            Console.WriteLine("\tUser as no 'role' : '{0}'", role == null ? "null" : role.Value);
        else
            Console.WriteLine("\tUser has 'role' : '{0}'", role.Value);

        if (accessLevel == null)
            Console.WriteLine("\tUser has no claim 'AdminPermission' : '{0}'", accessLevel == null ? "null" : accessLevel.Value);
        else
            Console.WriteLine("\tUser has 'AdminPermission' : '{0}'", accessLevel.Value);

        if (role != null && accessLevel != null)
        {
            if (role.Value == "Administrator" && accessLevel.Value == "Read")
                context.Succeed(requirement);
        }
        else
            Console.WriteLine("\n *** Authorization Failue. ***\n");



        return Task.CompletedTask;
    }

}

Data

ApiClaims                       : 
ApiResources                    : api1
ApiScopeClaims                  : AdminPermission, ApiScopeId = 2
ApiScopes                       : api1, ApiResourceId = 1
                                : AdminPermission, ApiResourceId = 1
ApiSecrets                      :
AspNetRoleClaims                : AdminPermission, Read, RoleId = b2f03...
AspNetRoles                     : Administrator, b2f03... 
                                : Customer, 779f7...
                                : Internal, 10d5d...
AspNetUserClaims                : AdminPermission, Create, UserId = 8ee62...
                                : AdminPermission, Update, UserId = 8ee62...
                                : AdminPermission, Delete, UserId = 8ee62...
AspNetUserLogins                :
AspNetUserRoles                 : UserId = 8ee62..., RoleId = b2f03... 
AspNetUsers                     : [email protected], Id = 8ee62...
AspNetUserTokens                :
ClientClaims                    :
ClientCorsOrigins               :
ClientGrantTypes                : hybrid, ClientId = 1
                                : client_credentials, ClientId = 1
                                : client_credentials, ClientId = 2
                                : password, ClientId = 3
ClientIdPRestrictions           :
ClientPostLogoutRedirectUris    : http://localhost:5002/signout-callback-oidc, ClientId = 1
ClientProperties                :
ClientRedirectUris              : http://localhost:5002/signin-oidc, ClientId = 1
Clients                         : mvc, AllowAccessTokenViaBrowser = 1, AllowOfflineAccess = 1, RequireConsent = 0
                                : client ...
                                : ro.client ...
ClientScopes                    : openid, ClientId = 1
                                : profile, ClientId = 1
                                : AdminPermission, ClientId = 1
ClientSecrets                   : Type = SharedSecret
IdentityClaims                  :   Id  IdentityResourceId  Type
                                    1   1                   sub
                                    2   2                   name
                                    3   2                   family_name
                                    4   2                   given_name
                                    5   2                   middle_name
                                    6   2                   nickname
                                    7   2                   preferred_username
                                    8   2                   profile
                                    9   2                   picture
                                    10  2                   website
                                    11  2                   gender
                                    12  2                   birthdate
                                    13  2                   zoneinfo
                                    14  2                   locale
                                    15  2                   updated_at
                                    16  3                   AdminPermission
IdentityResources               : openid
                                : profile
                                : AdminPermission
PersistedGrants                 : [8x] Type = refresh_token

Images

Autos at the beginning of the AuthorizationHandler

Claims shown on the identity server

Claims shows on the MVC client

Log from when an authenticate user attempts to access

JWT received by MVC client

Database tables


Solution

  • --EDIT--

    I've Forked your code and solved the issue.

    Here is a link to my repo.

    https://github.com/derekrivers/IdentityServer4

    In order to fix your solution, i changed the server authentication response type too :-

    "code Id_token"
    

    In the MVC Client setup in your config.cs I added the following properties :-

     AlwaysSendClientClaims = true, 
     AlwaysIncludeUserClaimsInIdToken = true 
    

    I've also removed the adminpermission scope from the mvc client, as it isn't required.

    I've also amended the AdminRequirementHandler.cs slightly, but i will let you explore that in my repo.

    Basically, We have ensured that the user claims are in the Identity token, and by doing this they are then accessible within you AdminRequirementHandler

    Hope this helps.