Fawad Ali Siddiqi
Fawad Ali Siddiqi

Reputation: 421

Role Based Authorization for Web API with IdentityServer4

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

Upvotes: 9

Views: 6025

Answers (1)

m3n7alsnak3
m3n7alsnak3

Reputation: 3166

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

Upvotes: 5

Related Questions