9
votes

I am using IdentityServer4 (v2.2.1) with .Net Core 2.0 and Asp.Net Core Identity. I have three projects in my Solution.

  1. IdentityServer
  2. Web API
  3. MVC Web Application

I am trying to implement Role Based Authorization on my Web API so that any client will pass an Access Token to Web API to access resources.

Currently i can implement Roles bases authorization on MVC Application controller but i cannot pass/Configure the same for WEB API Controller.

Below are the Identity Server Files: Config.cs

public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            //SCOPE - Resource to be protected by IDS 
            new ApiResource("TCSAPI", "TCS API")
            {
                UserClaims = { "role" }
            }
        };
    }


 public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
new Client
            {
                ClientId = "TCSIdentity",
                ClientName = "TCS Mvc Client Application .",
                AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
                RequireConsent = false,
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },

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

                AlwaysSendClientClaims= true,
                AlwaysIncludeUserClaimsInIdToken = true,

                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Email,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.OfflineAccess,
                    "TCSAPI",
                    "office",
                    "role",
                },
                AllowOfflineAccess = true
            }
  };
    }

public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new IdentityResource[]
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Email(),
            new IdentityResource
            {
                Name = "role",
                DisplayName="User Role",
                Description="The application can see your role.",
                UserClaims = new[]{JwtClaimTypes.Role,ClaimTypes.Role},
                ShowInDiscoveryDocument = true,
                Required=true,
                Emphasize = true
            }
        };
    }

Startup.cs

 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();

        // configure identity server with in-memory stores, keys, clients and scopes
        services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddInMemoryPersistedGrants()
            .AddInMemoryIdentityResources(Config.GetIdentityResources())
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients())
            .AddAspNetIdentity<ApplicationUser>();
    }

MVC WEB APP (Roles Base Authorization works well for MVC WEB APP):

RoleClaimAction.cs using this file to Add roles to Identity.

internal class RoleClaimAction : ClaimAction
{
    public RoleClaimAction()
        : base("role", ClaimValueTypes.String)
    {
    }

    public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
    {
        var tokens = userData.SelectTokens("role");
        IEnumerable<string> roles;

        foreach (var token in tokens)
        {
            if (token is JArray)
            {
                var jarray = token as JArray;
                roles = jarray.Values<string>();
            }
            else
                roles = new string[] { token.Value<string>() };

            foreach (var role in roles)
            {
                Claim claim = new Claim("role", role, ValueType, issuer);
                if (!identity.HasClaim(c => c.Subject == claim.Subject
                                         && c.Value == claim.Value))
                {
                    identity.AddClaim(claim);
                }
            }
        }
    }
} 

MVC WEB APP/Startup.cs

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

        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
        services.AddCors();
        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 = "TCSIdentity";

                //HYBRID FLOW
                options.ClientSecret = "secret";

                options.ClaimActions.Add(new RoleClaimAction()); // <-- 

                options.ResponseType = "code id_token token";
                options.GetClaimsFromUserInfoEndpoint = true;
                options.Scope.Add("TCSAPI");
                options.Scope.Add("offline_access");
                //END HYBRID FLOW
                options.SaveTokens = true;
                options.Scope.Add("role");

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

    }

MVC WEB APP/HomeController.cs this action methos works well with role base authorization but when when i try to pass Token to Web Api to access anythings with Role Base Authorization it cannot authorize. e.g. var content = await client.GetStringAsync("http://localhost:5001/user");

[Authorize(Roles = "User")]
    [Route("user")]
    public async Task<IActionResult> UserAccess()
    {

        var tokenClient = new TokenClient("http://localhost:5000/connect/token", "RoleApi", "secret");
        var tokenResponse = await tokenClient.RequestClientCredentialsAsync("TCSAPI");

        var client = new HttpClient();
        client.SetBearerToken(tokenResponse.AccessToken);
        var content = await client.GetStringAsync("http://localhost:5001/user");

        ViewBag.Json = JArray.Parse(content).ToString();
        return View("json");
    }
    [Authorize(Roles = "Admin")]
    [Route("admin")]
    public async Task<IActionResult> AdminAccess()
    {
        var accessToken = await HttpContext.GetTokenAsync("id_token");

        var client = new HttpClient();
        client.SetBearerToken(accessToken);
        var content = await client.GetStringAsync("http://localhost:5001/admin");
        ViewBag.Json = JArray.Parse(content).ToString();
        return View("json");
    }

WEBAPI/Startup.cs

 public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvcCore()
            .AddAuthorization()
            .AddJsonFormatters();
        services.AddAuthentication("Bearer")
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;
                options.ApiName = "TCSAPI";
            });

        services.AddCors(options =>
        {
            options.AddPolicy("default", policy =>
            {
                policy.WithOrigins("http://localhost:5002")
                .AllowAnyHeader()
                .AllowAnyMethod();
            });
        });
    }

WEB API/TestController.cs

[Route("admin")]
    [Authorize(Roles = "Admin")]
    public IActionResult AdminAccess()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
    [Route("user")]
    [Authorize(Roles = "User")]
    public IActionResult UserAccess()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
    [AllowAnonymous]
    [Route("public")]
    public IActionResult PublicAccess()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
1
How did you ended up with this problem ? please provide information if you done this. thank you - Mohammad Olfatmiri
I Added a Menu Tabble and a MenuPermissions table. and in UserProfileService i read user permissions from the database and add each Permission as a claim. on my controller actions i used the claim as permission. This way now i can control permission on controller and individual action methods. - Fawad Ali Siddiqi
@can you provide final code ? - Mohammad Olfatmiri
cant confirm. i will check if i can pull the required code than I will share. - Fawad Ali Siddiqi

1 Answers

5
votes

Your code is not exactly a policy based authorization. Yours looks like the .NET Framework Role Based Authorization.

For Policy Based Authorization, you need to do the following things:

1. In the Startup.cs of your Web API project, you need to add something like:

// more code
.AddMvcCore()
            .AddAuthorization(options =>
            {
                options.AddPolicy("Policy1",
                    policy => policy.Requirements.Add(new Policy1Requirement()));
                options.AddPolicy("Policy2",
                    policy => policy.Requirements.Add(new Policy2Requirement()));
                .
                .
                .
                .
            })
 // more code

2. Then you need to have a class for every Policy(X)Requirement:

public class Policy1Requirement : AuthorizationHandler<Policy1Requirement>, IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminUserRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == "role" && c.Value == "<YOUR_ROLE_FOR_THIS_POLICY>"))
        {
            context.Fail();
        }
        else
        {
            context.Succeed(requirement);
        }
        return Task.FromResult(0);
    }
}

3. And then at the end, where you are applying the policy, you need to have:

[Authorize(Policy = "Policy1")]
public class MyController : Controller
{
.
.    
}

Good luck!

PS:

The names Policy(X) and Policy(X)Requirement are just for clarification. You can use whatever names you want, as long as you implement the proper interface IAuthorizationRequirement, and inherit the class AuthorizationHandler