We can use claims to show identity-related information in our application but, we can use it for the authorization process as well. In this article, we are going to learn how to modify our claims and add new ones. Additionally, we are going to learn about the IdentityServer4 Authorization process and how to use Roles to protect our endpoints.

To download the source code for the client application, you can visit the IdentityServer4 Authorization repository.

To navigate through the entire series, visit the IdentityServer4 series page.

So, let’s get down to business.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Modifying Claims

If we inspect our decoded id_token with the claims on the Privacy page, we are going to find some naming differences:

Different claims - IdentityServer4 Authorization

So, what we want to do is to ensure that our claims stay the same as we define them, instead of being mapped to different claims. For example, the nameidentifier claim is mapped from the sub claim, and we want it to stay the sub claim. To do that, we have to slightly modify the constructor in the client’s Startup class:

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
} 

For this to work, we have to add the System.IdentityModel.Tokens.Jwt using statement.

Now, we can start our application, log out from the client, log in again, and check the Privacy page:

Correct mapping claims - IdentityServer4 Authorization

We can see our claims are the same as we defined them at the IDP (Identity Provider) level.

If there are some claims we don’t want to have in the token, we can remove them. To do that, we have to use the ClaimActions in the OIDC configuration:

.AddOpenIdConnect("oidc", opt =>
{
    opt.SignInScheme = "Cookies";
    opt.Authority = "https://localhost:5005";
    opt.ClientId = "mvc-client";
    opt.ResponseType = "code id_token";
    opt.SaveTokens = true;
    opt.ClientSecret = "MVCSecret";
    opt.GetClaimsFromUserInfoEndpoint = true;

    opt.ClaimActions.DeleteClaim("sid");
    opt.ClaimActions.DeleteClaim("idp");
});

The DeleteClaim method exists in the Microsoft.AspNetCore.Authentication namespace. As a parameter, we pass a claim we want to remove. Now, if we start our client again and navigate to the Privacy page, these claims will be missing for sure (Log out and log in prior to checking the Privacy page).

If you don’t want to use the DeleteClaim method for each claim you want to remove, you can always use the DeleteClaims method:

opt.ClaimActions.DeleteClaims(new string[] { "sid", "idp" });

Let’s move on.

Adding Additional Claims

If we want to add additional claims to our token (address, for example),  we can do that with a few simple steps. The first step is to support a new identity resource in the InMemoryConfig class in the IDP project :

public static IEnumerable<IdentityResource> GetIdentityResources() =>
    new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResources.Address()
    };

Then, we have to add it to our client’s allowed scopes:

new Client
{
    ClientName = "MVC Client",
    ClientId = "mvc-client",
    AllowedGrantTypes = GrantTypes.Hybrid,
    RedirectUris = new List<string>{ "https://localhost:5010/signin-oidc" },
    AllowedScopes =
    { 
       IdentityServerConstants.StandardScopes.OpenId, 
       IdentityServerConstants.StandardScopes.Profile, 
       IdentityServerConstants.StandardScopes.Address 
    },
    ClientSecrets = { new Secret("MVCSecret".Sha512()) },
    PostLogoutRedirectUris = new List<string> { "https://localhost:5010/signout-callback-oidc" }
}

And lastly, we have to add the address claim for our users:

new TestUser
{
    SubjectId = "a9ea0f25-b964-409f-bcce-c923266249b4",
    Username = "Mick",
    Password = "MickPassword",
    Claims = new List<Claim>
    {
        new Claim("given_name", "Mick"),
        new Claim("family_name", "Mining"),
        new Claim("address", "Sunny Street 4")
    }
},
new TestUser
{
    SubjectId = "c95ddb8c-79ec-488a-a485-fe57a1462340",
    Username = "Jane",
    Password = "JanePassword",
    Claims = new List<Claim>
    {
        new Claim("given_name", "Jane"),
        new Claim("family_name", "Downing"),
        new Claim("address", "Long Avenue 289")
    }
}

And, that’s all it takes regarding the InMemoryConfig class.

One more thing. If we want to see the consent page for a specific client, we can enable that in the Client configuration in the OAuth project:

ClientSecrets = { new Secret("MVCSecret".Sha512()) },
PostLogoutRedirectUris = new List<string> { "https://localhost:5010/signout-callback-oidc" },
RequireConsent = true

Now, we have to modify the Client application, by adding the new scope to the OIDC configuration:

.AddOpenIdConnect("oidc", opt =>
{
    //previous code

    opt.ClaimActions.DeleteClaim("sid");
    opt.ClaimActions.DeleteClaim("idp");

    opt.Scope.Add("address");
});

If we log out and log in again, we are going to see a new scope in the Consent screen:

Additional claim in the consent screen

But, if we inspect the Privacy page, we won’t be able to find the address claim there. That’s because we didn’t map it to our claims. Of course, we can inspect the console logs to make sure the IdentityServer returned our new claim:

Address claim

But if we want to include it, we can modify the OIDC configuration:

opt.ClaimActions.MapUniqueJsonKey("address", "address");

After we log in again, we can find the address claim on the Privacy page.

We just want to mention if you don’t need all the additional claims for your entire application but just for one part of it, the best practice is not to map all the claims. You can always get them with the IdentityModel package by sending the request to the /userinfo endpoint. By doing that, we ensure our cookies are small in size and that we get always up-to-date information from the userinfo endpoint.

Getting Claims Manually from the UserInfo Endpoint

So, let’s see how we can extract the address claim from the /userinfo endpoint. The first thing we have to do is to remove the MapUniqueJsonKey(„address“, „address“) statement from the OIDC configuration.

Then, let’s install the required package:

IdentityModel package

After that, let’s modify the Privacy action in the Home controller:

public async Task<IActionResult> Privacy()
{
    var client = new HttpClient();
    var metaDataResponse = await client.GetDiscoveryDocumentAsync("https://localhost:5005");

    var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);

    var response = await client.GetUserInfoAsync(new UserInfoRequest
    {
        Address = metaDataResponse.UserInfoEndpoint,
        Token = accessToken
    });

    if(response.IsError)
    {
        throw new Exception("Problem while fetching data from the UserInfo endpoint", response.Exception);
    }

    var addressClaim = response.Claims.FirstOrDefault(c => c.Type.Equals("address"));

    User.AddIdentity(new ClaimsIdentity(new List<Claim> { new Claim(addressClaim.Type.ToString(), addressClaim.Value.ToString()) }));

    return View();
}

So, we create a new client object and fetch the response from the IdentityServer with the GetDiscoveryDocumentAsync method. This response contains our required /userinfo endpoint’s address. After that, we extract the access token and use the UserInfo address and extracted token to fetch the required user information. If the response is successful, we extract the address claim from the claims list and just add it to the User.Claims list (this is the list of Claims we iterate through in the Privacy view).

Now, if we log in again, and navigate to the Privacy page, we are going to see the address claim again. But this time, we extracted it manually. So basically, we can use this code only when we need it in our application.

IdentityServer4 Authorization

Authorization is the process of determining what you are allowed to do once authenticated. The id_token helps us with the authentication process while the access_token helps us with the authorization process because it authorizes a web client application to communicate with the web api.

So, let’s start with the InMemoryConfig class modification, by adding roles to our users:

public static List<TestUser> GetUsers() =>
    new List<TestUser>
    {
        new TestUser
        {
	     //previous code

            Claims = new List<Claim>
            {
                new Claim("given_name", "Mick"),
                new Claim("family_name", "Mining"),
                new Claim("address", "Sunny Street 4"),
                new Claim("role", "Admin")
            }
        },
        new TestUser
        {
            //previous code

            Claims = new List<Claim>
            {
                new Claim("given_name", "Jane"),
                new Claim("family_name", "Downing"),
                new Claim("address", "Long Avenue 289"),
                new Claim("role", "Visitor")
            }
        }
   };

We have to create a new identity scope in the GetIdentityResources method:

public static IEnumerable<IdentityResource> GetIdentityResources() =>
    new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResources.Address(),
        new IdentityResource("roles", "User role(s)", new List<string> { "role" })
    };

And, we have to add roles scope to the allowed scopes for our MVC Client:

AllowedScopes =
{ 
    IdentityServerConstants.StandardScopes.OpenId, 
    IdentityServerConstants.StandardScopes.Profile, 
    IdentityServerConstants.StandardScopes.Address,
    "roles"
},

With this, we have finished the modification of the IDP application. Let’s continue with the client application by modifying the OIDC configuration to support roles scope:

.AddOpenIdConnect("oidc", opt =>
{
    //previous code

    opt.Scope.Add("address");
    //opt.ClaimActions.MapUniqueJsonKey("address", "address");

    opt.Scope.Add("roles");
    opt.ClaimActions.MapUniqueJsonKey("role", "role");
});

So, we want to allow Create, Edit, Details, and Delete actions only to users with the Admin role. To do that, we are going to modify the Index view:

@if (User.IsInRole("Admin"))
{
    <p>
        <a asp-action="Create">Create New</a>
    </p>
}
<table class="table">
    
    //previous code

    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                //previous code

                @if (User.IsInRole("Admin"))
                {
                    <td>
                        @Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) |
                        @Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ }) |
                        @Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })
                    </td>
                }
            </tr>
        }
    </tbody>
</table>

We use the IsInRole method to allow only Admin users to see these links.

Finally, we have to state where our framework can find the user’s role:

.AddOpenIdConnect("oidc", opt =>
{
    //previous code

    opt.Scope.Add("roles");
    opt.ClaimActions.MapUniqueJsonKey("roles", "role");

    opt.TokenValidationParameters = new TokenValidationParameters
    {
        RoleClaimType = "role"
    };
});

The TokenValidationParameters class exists in the Microsoft.IdentityModel.Tokens namespace.

Now, we can start our applications and login with Jane’s account:

Roles added - IdentityServer4 Authorization

We can see an additional scope in the Consent screen. Once we allow this, we can see the Index view but without additional actions. That’s because Jane is in the Visitor role. If we log out and log in with Mick, we are going to see those links for sure.

Excellent.

But can we protect our endpoints with roles as well? Of course, we can. Let’s see how to do it.

Using Roles to Protect Endpoints

Le’s say, for example, only the Admin users can access the Privacy page. Well, with the same action we did in a previous part, we can show the Privacy link in the _Layout view:

<ul class="navbar-nav flex-grow-1">
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
    </li>
    @if (User.IsInRole("Admin"))
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </li>
    }
    @if (User.Identity.IsAuthenticated)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-controller="Authentication" asp-action="Logout">Logout</a>
        </li>
    }
</ul>

If we log in as Jane, we won’t be able to see the Privacy link:

Hidden Link Privacy - IdentityServer4 Authorization

Even though we can’t see the Privacy link, we still have access to the Privacy page by entering a valid URI address:

Accessed Privacy over URI

So, what we have to do is to protect our Privacy endpoint with the user’s role:

[Authorize(Roles = "Admin")]
public async Task<IActionResult> Privacy()

Now, if we log out, log in again as Jane, and try to use the URI address to access the privacy page, we won’t be able to do that:

IdentityServer4 Authorization Access Denied

The application redirects us to the /Account/AccessDenied page, but we get 404 because we don’t have that page.

So, let’s create it.

The first thing we are going to do is to add a new action in the Account controller:

public IActionResult AccessDenied()
{
    return View();
}

And, let’s create a view for this action:

@{
    ViewData["Title"] = "AccessDenied";
}

<h1>AccessDenied</h1>

<h3>You are not authorized to view this page.</h3>

<p>
    You can always <a asp-controller="Account" asp-action="Logout">log in as someone else</a>.
</p>

After these changes, we can log out and log in as Jane. Once we navigate to the /Home/Privacy URI, we are going to be redirected to the AccessDenied page:

Access Denied Page - IdentityServer4 Authorization

So, this works as we expect it to do.

We want to mention one more thing. If you create this action in a controller with a different name, you have to add additional mapping in the AddCookie method in the Client application:

.AddCookie("Cookies", (opt) => 
{
    opt.AccessDeniedPath = "/ControllerName/AccessDenied";
})

With this configuration, we are adding a different address for the AccessDenied action.

Conclusion

Let’s sum up everything.

We have learned:

  • How to modify claims and add additional ones
  • The way to get claims manually from the /userinfo endpoint
  • How to setup authorization
  • And how to use roles for authorization purposes

In the next article, we are going to learn how to protect our Web API with the Hybrid flow.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!