ASP.NET Core 5.0 : MVC, Razor Pages, Web API, EF Core, Blazor, Design Patterns, and more. Private online coaching for software developers. Click here for more details.

Integrate IdentityServer with ASP.NET Core (Part 3 - MVC Application)

In Part 2 of this series you protected the Employees Web API using IdentityServer. You also invoked the Web API using Postman tool and HttpClient. In this part of the series you will create an MVC application that is protected using IdentityServer and requires valid sign-in credentials from the end users. The MVC application then invokes the same Web API by passing a security token to it.

In the previous part you have already created the IdentityServerDemo.Client MVC project. You also added two NuGet packages namely IdentityModel and Microsoft.AspNetCore.Authentication.OpenIdConnect. But the project isn't yet to be configured to use OpenId Connect. Let's begin by doing just that.

First of all, open the ServerConfiguration class we created in the IdentityServerDemo.Server project and modify its Clients property to include a new client definition intended for the MVC application.

public static List<Client> Clients
{
    get
    {
        Client client1 = new Client
        {
          ...
        };

        Client client2 = new Client
        {
            ClientName = "Client 2",
            ClientId = "client2",
            ClientSecrets = { 
                new Secret("client2_secret_code".Sha512()) 
            },
            AllowedGrantTypes = GrantTypes.Hybrid,
            AllowedScopes = {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                IdentityServerConstants.StandardScopes.Address,
                IdentityServerConstants.StandardScopes.Email,
                IdentityServerConstants.StandardScopes.Phone,
                "employeesWebApi",
                "roles"
            },
            RedirectUris = new List<string> { 
                "http://localhost:5010/signin-oidc" 
            },
            PostLogoutRedirectUris = new List<string> { 
                "http://localhost:5010/signout-callback-oidc" 
            },
            RequirePkce = false,
            RequireConsent = true
        };

        List<Client> clients = new List<Client>();
        clients.Add(client1);
        clients.Add(client2);

        return clients;
    }
}

In the client2 definition we set the AllowedGrantTypes property to Hybrid. The AllowedScopes property is configured as before. The RedirectUris property indicates the URL to return the security token or authorization codes. The PostLogoutRedirectUris property indicates the URL to redirect to after user signs out of the MVC application. Recollect that 5010 is the port number of the MVC client application project you created in the previous part of this series (IdentityServerDemo.Client).

The newly defined client is added to the List of Client objects that is returned to the caller.

This completes the IdentityServerDemo.Server configuration for now.

Go to the IdentityServerDemo.Client project and open its Startup class.

Then add this code to its ConfigureServices() method :

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    services.AddAuthentication(o => { 
        o.DefaultScheme = "Cookies";
        o.DefaultChallengeScheme = "oidc";
        })
        .AddCookie("Cookies", o =>
        {
            o.AccessDeniedPath = "/Account/AccessDenied";
        })
        .AddOpenIdConnect("oidc", o =>
        {
            o.ClientId = "client2";
            o.ClientSecret = "client2_secret_code";
            o.SignInScheme = "Cookies";
            o.Authority = "http://localhost:5000";
            o.RequireHttpsMetadata = false;
            o.ResponseType = "code id_token";
            o.SaveTokens = true;
            o.GetClaimsFromUserInfoEndpoint = true;
            o.Scope.Add("employeesWebApi");
            o.Scope.Add("roles");
            o.ClaimActions.MapUniqueJsonKey("role", "role");
            o.TokenValidationParameters = new 
TokenValidationParameters
            {
                RoleClaimType = "role"
            };
        });
}

There are three important methods that configure the authentication scheme of the MVC client application - AddAuthentication(), AddCooki(), and AddOpenIdConnect(). The AddAuthentication() method specifies the authentication scheme to be Cookie and challenge  scheme to be oidc (OpenId Connect). Due to these authentication scheme settings the MVC client application will use to the IdentityServer based sign-in and sign-out mechanism for its protected controllers and actions.

Then AddCookie() call configures the access denied path of the system to be /Account/AccessDenied. We will be creating the Account controller and AccessDenied action later in this article.

Next, AddOpenIdConnect() method configures many important properties of the oidc scheme. I would suggest that you read more about these properties in the official documentation. Configuration properties that are more important for our example are marked in bold letters. The Authority property points to the URL of the IdentityServerDemo.Server application. We also add employeesWebApi and roles scopes using Scope.Add() calls. Since we want to implement role based security we also add role using TokenValidationParameters.

Also note the ClientId and ClientSecret properties. They are set to the respective values of the client2 that we added in the Server project earlier in this article.

 public void Configure(IApplicationBuilder app, 
IWebHostEnvironment env)
    {
        ...
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/
                {action=Index}/{id?}");
        });
    }
}

Now go to the Configure() method and add calls to UseAuthentication() and UseAuthorization().

We just completed Startup configuration required to use IdentityServer. Now we can create application controllers - HomeController and AccountController.

First, add the HomeController and decorate it with [Authorize] attribute :

[Authorize(Roles = "Admin")]
public class HomeController : Controller
{
..
}

Then add Index() action and invoke the Employees Web API. This is shown below:

public IActionResult Index()
{
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue("application/json"));

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

    if (!string.IsNullOrWhiteSpace(accessToken))
    {
        client.SetBearerToken(accessToken);
    }
    var response = client.GetAsync
("http://localhost:5020/Employees").Result;
    var jsonData = response.Content.
ReadAsStringAsync().Result;
    List<string> data = JsonSerializer.
Deserialize<List<string>>(jsonData);
    return View(data);
}

This code should look familiar to you because you used similar code in the previous part of this article series. Notice the code marked in bold letters.

In the previous part of this series when we invoked the Web API, we fetched the required token directly from the authorization server. Here, the MVC application already has the token as a part of the authentication process. We simply need to grab it and forward it to the Employees Web API. This is done using the GetTokenAsync() method of HttpContext. Once you get the access token you set it on the HttpClient using SetBearerToken() method.

Then we invoke the Get() action of Employees Web API using GetAsync() method. The list of employee names is passed to the Index view for displaying in the browser.

The Index.cshtml is shown below:

@model List<string>

@if (User.IsInRole("Admin"))
{
    <h2>You are Admin</h2>
}

@if (User.IsInRole("Operator"))
{
    <h2>You are Operator</h2>
}

<h3>
<ul>
    @foreach (var item in Model)
    {
        <li>@item</li>
    }
</ul>
</h3>


@if (User.Identity.IsAuthenticated)
{
   <h3>
       <a asp-controller="Account" 
asp-action="SignOut">SignOut</a>
    </h3>
}

Although we have added Admin role to the [Authorize] attribute, you can also check for role(s) using code as shown here. The IsInRole() call does that for us. A foreach look iterates through all the employee names and outputs them in a bulleted list. At the bottom we render SignOut link that points to the SignOut() action of AccountController. We will create AccountController shortly.

Now add AccountController and write AccessDenied() action and SignOut() action.

public class AccountController : Controller
{
    public IActionResult AccessDenied()
    {
        return View();
    }

   public void SignOut()
   {
     HttpContext.SignOutAsync("Cookies").Wait();
     HttpContext.SignOutAsync("oidc").Wait();
   }
}

The AccessDenied() action simply returns AccessDenied view and displays an error message to the user. The SignOut() action calls SignOutAsync() and signs the user out of the specified authentication scheme.

The AccessDenied.cshtml is shown below:

<h1>Access Denied !!!</h1>

<h3>
    <a asp-controller="Account" 
asp-action="SignOut">Sign-Out</a>
</h3>

This completes the MVC client application. It's time to test what we developed in this part of the article.

Run the IdentityServerDemo.Server application first, then run IdentityServerDemo.WebApi, and finally run IdentityServerDemo.Client application.

If all goes well, you will be presented with a sign-in page as shown below: 

Notice browser's address bar. This sign-in page comes from the Server project (recollect that we added QuickStart and other folders to the Server project in previous parts).

Recollect that we have created two in-memory user accounts - user1 and user2. The user1 account has Admin role and user2 account has Operator role. So, let's use user1 account to sign-in.

Enter Username and Password and click on Login button.

As you can see, the Client2 requires consent from you (user1count) before going ahead. Also notice how profile, roles, and Employees Web API are listed there. Uncheck the Remember My Decision checkbox and hit Yes button.

The page should now display a list of employees and the Sign-Out link. Clicking on the Sign-Out link will take you back to the Sign-In page.

Now, sign out of the system and sign in with user2 credentials.

You will displayed the Access Denied page because user2 doesn't belong to Admin role.

 In the next part we will learn something more about IdentityServer integration.

That's it for now! Keep coding!!


Bipin Joshi is an independent software consultant, trainer, author, yoga mentor, and meditation teacher. He has been programming, meditating, and teaching for 24+ years. He conducts instructor-led online training courses in ASP.NET family of technologies for individuals and small groups. He is a published author and has authored or co-authored books for Apress and Wrox press. Having embraced the Yoga way of life he also teaches Ajapa Yoga to interested individuals. To know more about him click here.

Get connected : Facebook  Twitter  LinkedIn  YouTube

Posted On : 17 August 2020


Tags : ASP.NET ASP.NET Core Data Access MVC C# Visual Studio


Subscribe to our newsletter

Get monthly email updates about new articles, tutorials, code samples, and how-tos getting added to our knowledge base.

  

Receive Weekly Updates