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!!