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 navigate through the entire series, visit the IdentityServer4 series page.
So, let’s get down to business.
Modifying Claims
If we inspect our decoded id_token
with the claims on the Privacy page, we are going to find some naming differences:
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:
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:
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:
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:
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:
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:
Even though we can’t see the Privacy link, we still have access to the Privacy page by entering a valid URI address:
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:
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:
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.
Test User Mick was revoked the “Admin” claim. If User don’t re-login, how to update access_token
There is always the option of refreshing a token. It is a bit of a process with IS4/Duende which we didn’t cover in our articles, but we covered it in our Security book.
Hi Marinko,
I would like to ask you about using access tokens in a distributed app where we do not use HTTPClient and its headers to attach the access-token to reach the webapi but through a message bus facility such as Rabbitmq or Masstransit on top of Rabbitmq. In such cases, how could we send the token along with the real data? Do you think it is good idea to provide a field for the access-token in the data object transmitted f and decode it in the receiving microservice? Is there such a sample example that you know of? Thank you.
Hi Lokumas. I really can’t help you with that as I didn’t work with RabbitMQ nore MassTransit. I know the concepts behind these two, but still I don’t have enough knowledge to even try answering your question.
Hi, I forgot to mention in my previous message when I get the error. I run the mvc client app and be redirected to the ID server login page. I login and a consent page appears. I accept the defaults and hit the button. That is where I get login failed with the remote server. Correlation error or something like that.
Hi Marinko, I have been following the chapter securing the mvc web client from your book. I am using asp.net core 5 and ID server package 4.11. When I register the client with ID server and try to get data from the unsecured webapi, I get Correlation Exception in the frontend. I searched in the internet about the possible remedies. Some folks say it is something to do with new versions of the Google chrome’s SameSite policy but I am having the same problem with other browsers as well. Any idea how to solve this problem.
Thank you for your help beforehand.
Hi Lokumas. To be honest, I didn’t face the error like that. All I can advice in this moment is to try running our source code. That one works 100%. If it doesn’t work on your machine, then it is probably related to your local machine. Also, try inspecting the logs in the console. This can tell even more about the error.
Hello,
after starting the three projects from the Github repo, and logging in with Mick’s credentials, again I have the error: “Exception: Problem with fetching data from the API: Internal Server Error
CompanyEmployees.Client.Controllers.HomeController.Index() in HomeController.cs, line 44”
I think you still haven’t applied migrations on the API side. Just do it as I described in the previous comment. This should solve the problem.
It works, thanks.
Hi Marinko, Thank you very much for the very useful series of articles on the ID4. I have one question, however… If we had wanted to use the Asp.netCore Identity for persisting the users with roles instead of using the TestUser given by the ID4, how could we move one? Probably we would need to bring into the scene IdentityUser and the IdentiyRole from the aspnetcore identity. Perhaps instead of using the IdentityRole we could use the role as a claim so that the ID4 understands it. Can you elaborate on this issue, if you do not mind?
Hello Lokumas. You are right if you want to deal with the User Management actions, you will have to bring the ASP.NET Core Identity library into whole story. We have just published a great Bonus book “Mastering ASP.NET Core Security” which can be bought separately or you can buy it as part of our Premium package with Web API and Docker books (https://code-maze.com/ultimate-aspnet-core-3-web-api/). There, we have described the entire process of securing your application with IS4, OAuth2, OIDC and Identity all together. It is a process it self, but at the end, not so hard. Just please don’t get me wrong, I am not trying to sell you our books, just if I am to explain the process here in a comment, it would take me few hours for sure 😀 😀 Bottom line is you need to include Identity and to use IdentityUser, IdentityRole and to use UserManager for actions, create some data seeding, modify built in controllers and actions, etc…
Hello Marinko, Thank you for your reply. I suppose the book delivers more content than what you already tell here in the articles. The Security book – I have just looked at- is priced at 37USD. Is there any possibility of offering some discount?
Hi Lokumas. On the sales page you can find the entire table of content for the Security book. And yes we provide different implementation, different flow, additional content and all in one project (IS4 with Identity). For more information, please use the chat on the sales page and we can talk more.
How is it that your TestUser has a claims property? My custom user class (called BejebejeUser) inherits from IdentityUser and I do not get a Claims property.
I have done everything else, and role authorisation isn’t working for me, I suspect because I couldn’t add claims onto the user.
Any ideas?
Hi. Well, role auth is not working if you don’t have claims, you are right about that.
The TestUser class is not a custom class that needs to inherit from the IdentityUser class. This is not ASP.NET Core Identity, this is IdentityServer4. So you have to use the TestClass from the IdentityServer4.Test namespace. Please check the source code for more detailes.