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 download the source code for the client application, you can visit the Hybrid Flow repository (Start folder). You can download the source code for the ending projects by visiting the Hybrid Flow repository (End folder).

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

So, let’s start.

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

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:

Client app solution explorer - Hybrid Flow

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:

HttpClient service

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:

OpenId Connect package - Hybrid Flow

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:

Authorization page - Hybrid Flow

Now, let’s inspect our URI:

URI Inspection - Hybrid Flow

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:

URI console authorize endpoint - Hybrid Flow

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:

Back channel token - Hybrid Flow

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:

Privacy page with claims details

We can see all the different information about the Claims and Properties. There are also the access_tokenid_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:

Logout screen - Hybrid Flow

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:

Logout better UX

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:

Additional user data - Hybrid flow

And, we can see additional claims.

We can inspect the console logs one more time:

Additional user data console

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.

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