In this part of the series, we are going to learn how to secure our web application (a client application) by using the Hybrid flow. We are going to start by introducing our pre-created client application. Then, we are going to learn how to modify the in-memory configuration to support our new client application. Furthermore, we are going to talk about authentication using Hybrid flow and the way to sign out from our application. Finally, we are going to show you how to use /userinfo
endpoint, to get additional claims in the token.
To navigate through the entire series, visit the IdentityServer4 series page.
So, let’s start.
Client Web Application Overview
We have already prepared a client application that you can download from our repository in the Start folder. If we open the Solution Explorer window, we are going to find a standard structure for the MVC project with an additional Services folder:
So, the Home
controller contains two actions Index
and Privacy
and we can see views for those actions as well. In the Services
folder, we can find the CompanyHttpClient
class:
We have injected the IHttpContextAccessor
interface, which we are going to use later in the series to access HttpContext
, and created a new HttpClient
object. There is a single GetClient
method, where we create an Http object to communicate with our API project. It is created as async, even though we don’t have an await keyword. This will be resolved later on in the series.
Note: If you want, you can modify this code and register HttpClient service in the StartUp.cs class and then inject it where you need it.
If you inspect the launchSettings.json
file, you will see our client runs on https://localhost:5010
address.
The last part we have to review is the ConfigureServices
method in the Startup
class:
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped<ICompanyHttpClient, CompanyHttpClient>(); services.AddControllersWithViews(); }
Here, we just registered the IHttpContextAccessor and ICompanyHttpClient as services in IOC.
Additional Note: Before we continue, let’s just comment out the [Authorize]
attribute from the Web API’s GetCompanies
action (project from the previous article) because for this article, we require free access to the resource. Later on in the series, we are going to protect our API using the Hybrid flow and we will uncomment this attribute.
So, that’s it for the client introduction part. We can move on to the configuration part.
About the Hybrid Flow
We are using the Hybrid Flow when we want to acquire our tokens over the front and back channels. When we receive a token via /authorization
endpoint over URI or Form POST, we are talking about the front-channel. When we receive a token via /token
endpoint, we are talking about the back-channel. The Hybrid flow uses both channels in the process.
There are three combinations for hybrid flow defined by the ResponseType
property in the configuration:
- The first one is
code token
- Then we have
code id_token
- And finally,
code id_token token
Each of these combinations provides a slightly different way of getting a token from the authorization server.
Code Token
The client requests access_token and a code from the /authorization
endpoint. The identity server issues them both. After the client validates the token, it sends the code to the /token
endpoint and the identity server responds with the id_token, access_token and it can include the refresh_token if requested.
Code Id_Token
The request is issued to the /authorization
endpoint with the request for the code and the id_token. The identity server issues them both. After successful validation of id_token, the code is sent to the /token
endpoint and the identity server responds with the access_token, id_token and refresh_token if requested.
Code Id_Token Token
The client requests the code, id_token and access_token from the /authorization
server. The identity server issues all three of them. After successful validation, the code is sent to the /token
endpoint and the identity server responds with the access_token, id_token and refresh_token if requested.
Recommendation by RFC
The main recommendation for the clients is to avoid using a response type that causes an access_token to be returned from the /authorization
endpoint. In other words, it is not recommended to return access_token via front-channel. Obviously, this means RFC recommends using flows that returns the access_token via back-channel (/token endpoint). For that, we can use the code
response type (Authorization code flow) or we can use the code id_token
response type (Hybrid flow). In this example, we are going to use the Hybrid Flow.
The Authorization Code flow is quite similar to the Hybrid flow (code id_token). The main difference is that the client requests only the code from the /authorization
server and not both code and id_token as the Hybrid flow (code id_token) does. Additionally, for the code grant, we should include the PKCE. Now, as RFC recommends, we should use PKC for web applications and not only for mobile applications.
Identity Server Configuration to Support Hybrid Flow
After this theory, we can start with the IdentityServer configuration. The first thing we are going to do is to modify the InMemoryConfig
class by adding additional client:
public static IEnumerable<Client> GetClients() => new List<Client> { new Client { ClientId = "company-employee", ClientSecrets = new [] { new Secret("codemazesecret".Sha512()) }, AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, "companyApi" } }, new Client { ClientName = "MVC Client", ClientId = "mvc-client", AllowedGrantTypes = GrantTypes.Hybrid, RedirectUris = new List<string>{ "https://localhost:5010/signin-oidc" }, RequirePkce = false, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile }, ClientSecrets = { new Secret("MVCSecret".Sha512()) } } };
So, we register our client with the name and id (ClientName
and ClientId
properties). With the AllowedGrantTypes
property, we define a flow we want to use. It’s obvious we use the Hybrid flow. The Hybrid flow is a redirection based flow (tokens are delivered to the browser in the URI via redirection) and therefore, we have to populate the RedirectUris
property. This address is our client application’s address with the addition of /signin-oidc
. Also, we have to state we don’t want to use PKCE. Then, we add allowed scopes, and these scopes are already supported in the GetIdentityResource
method. Finally, we add a secret which is hashed with the Sha512
algorithm.
Authentication Process with the Hybrid Flow
We have to configure our client application as well.
So, let’s first modify the ConfigureServices
method:
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped<ICompanyHttpClient, CompanyHttpClient>(); services.AddAuthentication(opt => { opt.DefaultScheme = "Cookies"; opt.DefaultChallengeScheme = "oidc"; }).AddCookie("Cookies"); services.AddControllersWithViews(); }
Here, we register authentication as a service and populate the DefaultScheme
and DefaultCHallengeScheme
properties. Finally, we call the AddCookie
method with the name of the scheme, to register the cookie handler and the cookie-based authentication for our default scheme.
Next, we have to add the OpenID Connect support.
To do that, we have to install a new NuGet package:
Now, we can modify the code:
services.AddAuthentication(opt => { opt.DefaultScheme = "Cookies"; opt.DefaultChallengeScheme = "oidc"; }).AddCookie("Cookies") .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"; });
So, to register and configure the OIDC handler, we call the AddOpenIdConnect
method. The SignInScheme
property has the same value as our DefaultScheme
. The Authority
property has the value of our IdentityServer address. The ClientId
and ClientSecret
properties have to be the same as the id and secret from the InMemoryConfig
class for this client. We set the SaveTokens
property to true to store the token after successful authorization. Certainly, we can see the ResponseType
property. We set it to the code id_token value and therefore, we expect the code and the id_token to be returned from the /authorization
endpoint.
We have to add one more modification, but this time in the Configure
class:
app.UseAuthentication(); app.UseAuthorization();
Finally, we want to protect our Home controller. To do that, we have to add the [Authorize]
attribute on top of it:
[Authorize] public class HomeController : Controller { …
Testing Our Functionality
Now, let’s start the OAuth application, then the API application, and lastly the Client application. As soon as we start the Client application, we are going to be redirected to the authorization page because we have protected the Home controller with the [Authorize] attribute:
Now, let’s inspect our URI:
We can see the redirect_uri
parameter and more important we can see the response_type
set to code id_token
. So, as we explained, the client sends the request to the /authorization endpoint for the code and id_token with the form_post response mode (Front Channel). We can confirm this with the console logs:
After we enter valid credentials, we are going to be directed to the consent page, we are going to see our requested data.
Let’s inspect the console logs:
We can see that via back-channel, the request was sent to the /token endpoint and that validation was successful.
Excellent. We have seen the Hybrid flow in action, not just in theory.
Displaying Claims
Now, let’s turn of the client application and modify the Privacy
view:
@using Microsoft.AspNetCore.Authentication <h2>Claims</h2> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> <h2>Properties</h2> <dl> @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) { <dt>@prop.Key</dt> <dd>@prop.Value</dd> } </dl>
We can start the application again and navigate to the Privacy
menu:
We can see all the different information about the Claims and Properties. There are also the access_token
, id_token
and token_type
. Additionally, we can see the nameidentitier
which stands for the sub
claim, and the identityprovider
and authmethodreferences
which is password(pwd).
So, this all works as expected. But, once we are logged in, we can’t log out. Well, we are going to change that.
Logout from the Client and IdentityServer
The first thing we require is the Logout button. So, let’s modify the _Layout
view in the Client application:
<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="Account" asp-action="Logout">Logout</a> </li> }
Once we have our button, we can create a new Authentication
controller and a required action:
public class AccountController : Controller { public async Task Logout() { await HttpContext.SignOutAsync("Cookies"); await HttpContext.SignOutAsync("oidc"); } }
Now we can test this by clicking the Logout button:
So, this works great, but we can make it better.
We can’t do anything from this screen. So, let’s fix this by modifying the client configuration in the InMemoryConfig
class:
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 }, ClientSecrets = { new Secret("MVCSecret".Sha512()) }, PostLogoutRedirectUris = new List<string> { "https://localhost:5010/signout-callback-oidc" } }
With this, we specify a redirect URI after the logout action. Now, let’s test this again:
If we click on the „here“ link, we are going to be redirected to the Login page. But if we want, we can improve this even further. We don’t have to click the „here“ button. Let’s add one more modification to automatize this process for us.
In the IdentityServer project, we can find the AccountOptions
class (QuickStart/Account). In this class, we can set automatic redirection to true:
public static bool AutomaticRedirectAfterSignOut = true;
Now, start the IdentityServer application and the Client application and log in. Once we click the Logout button, we are going to be redirected to the Login page.
About Additional User Info
If we look again at the Consent screen picture, we are going to see that we allowed for MVC Client to use the id and user profile information. But if we inspect the content in the Privacy page, we are going to see that we are missing the given_name and the family_name claims – from the Profile scope.
We can include these claims in the id_token but with too much information in the id_token, it can become quite large and cause the URI length restrictions. So, we are going to get these claims in another way.
Let’s modify the Client configuration a bit:
.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; });
With this, we allow our middleware to communicate with the /userinfo endpoint to retrieve additional user data.
At this point, we can log in again and inspect the Privacy page:
And, we can see additional claims.
We can inspect the console logs one more time:
We can see the call to the /connect/userinfo
endpoint and the additional claim types returned from that endpoint.
Conclusion
In this article, we have learned:
- About the Hybrid Flow and how it works
- How to configure IdentityServer to support this flow
- The way to create Login and Logout functionalities
- How to retrieve additional claims from the /userinfo endpoint
In the next article, we are going to learn about Authorization and advanced actions with Claims.
When I used authentication_code grant type it also worked. why did we prefer hybrid grant type? Why didn’t you want to use PKCE? Is Hybrid type a recommended type for MVC? Could you please share a reference for that?
Hi Helena. This article is a bit older – when Hybrid flow was a go-to option. But, feel free to use PKCE for your new projects. We used it in our Security book where we implemented both IS4 and Duende to secure our API with MVC Client.
H Marinko,
Even though I have included the “opt.GetClaimsFromUserInfoEndpoint = true;” line in the OpenIDConnect section of the client, I cannot get the given and family-name claims in the Claims page. Any idea why? Thank you very much for your help in advance.
Hi. Well, it is not easy for me to help you like this, just based on your message, but: Have you checked the console logs, can you see the request to the /connect/userinfo endpoint? Next, do you have these two claims in the configuration file? I just tested projects from this series and from our book as well, both works without any problem. In our book it works with .NET 5, so if you are using that version, you shouldn’t be having problems at all.
Thank you for your reply. Sorry for keeping you busy with my stupid mistake. I had made the mistake of using a “dash” such as “family-name” and “given-name instead of an underscore in TestUser claims definition.
I have another question though. I purchased your security book but it is not explained there either. It is about authorizing the api controller via a role as well. The role we use in the mvc client controller, it is supposed to be passed over to the Api. How can we place the same Role name with Authorize attribute on Api Controllers and do not get 403 Forbidden error. Thank you.
Please do not appologize, this is quite normal. Also thank you very much for the second question. To include roles for the web api as well, just change the IS4 configuration like this:
public static IEnumerable Apis =>() { “role”}
new ApiResource[]
{
new ApiResource(“companyemployeeapi”, “CompanyEmployee API”)
{
Scopes = { “companyemployeeapi.scope” },
UserClaims = new List
}
};
So, as you can see, all you have to add is the UserClaims property. Without it, MVC won’t send the role claim inside the access token, and that is the reason why you getting 403 when place [Authorize(Roles = “YourRole”)] in your API controller.
Hi Marinko, Thank you very much. It works.
I will keep you busy again as I have another question. Say, our webapi is contacting another webapi which is authorized as well? How would one transfer the access token to the other webapi. What would be the methodology or methodologies?.
In the MVC project we extract an access token with HttpContext and then attach it to the request, you can do the same from one API to another. As long as your http request has a Bearer header with the token, it should be a valid request. You can use HttpContext to extract the access token (as you did with the MVC app).
Ok. I had the same thought. I will try that.
Hi, I tried with the Start project (following the instructions) and then with the End project, starting all three projects, in the order you mentioned. It doesn’t work.
When I arrive to the section “Excellent. We have seen the Hybrid flow in action, not just in theory.”
Exception: Problem with fetching data from the API: Internal Server Error
Hi Zoltan. Have you ran the Update-Database command in the API project? We have prepared migrations, but you need to manyally trigger it.
Thanks, that was it. I could test it now.