Integrate IdentityServer with ASP.NET Core (Part 2 - Web API)

In the previous part of this series you created IdentityServerDemo.Server
project and also added IdentityServer configuration. In this part we will first
create the Employees Web API. Then we will configure the Web API project to use
JWT issued by the authorization server. Finally, we will invoke the Web API
using Postman and HttpClient.
Begin by adding a Web API project to the existing solution. Name the project
as IdentityServerDemo.WebApi

Then add a new API Controller class named EmployeesController to the newly
created Web API project.

Next, modify the Get() action of the EmployeesController class as shown
below:
[ApiController]
[Route("[controller]")]
[Authorize(Roles ="Admin")]
public class EmployeesController : ControllerBase
{
[HttpGet]
public List<string> Get()
{
return new List<string>() {
"Nancy Davolio",
"Andrew Fuller",
"Janet Leverling"
};
}
}
The Get() action simply returns a List of employee names. Notice the code
shown in bold letters. The EmployeesController is decorated with [Authorize]
attribute. Moreover, the Roles property of [Authorize] is set to Admin. This
means the Employees Web API can be invoked only by users belonging to Admin
role. Recollect from the
previous part of this series that there are two users - user1 and user2 - in
the server configuration. The user1 has Admin role and user2 has Operator role.
Although we used [Authorize] attribute to protect the Employees Web API, we
haven't enabled JWT authentication for our Web API project yet. To do so, add
NuGet package for Microsoft.AspNetCore.Authentication.JwtBearer to the project.

Then open the Startup class and modify the ConfigureServices() method as
shown below:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap
.Clear();
services.AddAuthentication("Bearer")
.AddJwtBearer(o =>
{
o.Authority = "http://localhost:5000";
o.RequireHttpsMetadata = false;
o.Audience = "employeesWebApiResource";
o.TokenValidationParameters =
new TokenValidationParameters
{
RoleClaimType = "role"
};
});
}
Notice the code marked in bold letters.
The code clears the standard JWT claim type mappings using the Clear() method
of DefaultInboundClaimTypeMap. Then the code proceeds to calling
AddAuthentication() followed by AddJwtBearer() methods. These methods set the
authentication scheme to JWT bearer and configure the necessary options that
integrate IdentityServer into the process.
The Authority configuration property points to the URL of
IdentityServerDemo.Server project we created in the previous part. Note that I
am using http based URLs for my testing and hence RequireHttpsMetadata is set to
false. In most of the real-world cases you will use https URLs. The Audience
configuration property is set to the ApiResource we created in the previous part
- employeesWebApiResource. The TokenValidationParameters indicate that the token
must include role information of the user.
Then modify the Configure() method as shown below:
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Make sure to call AddAuthentication() and AddAuthorization() at the specified
location in the code. If you are using http (and not https) then you might also
want to remove the call to UseHttpsRedirection() from the Configure() method.
Now open project's property page and modify the app URL as shown below:

As you can see, the port number is changed to some different value. This is
required because our server project is already listening to port 5000. I am
using http for my testing but you can use https URLs also. Just make sure to
give some different port number.
This completes the Web API project (for now) and you can try invoking the
Employees Web API.
We will invoke the Employees Web API using two ways -
Postman and HttpClient.
If you haven't installed Postman yet, consider installing it by going
here.
Run the IdentityServerDemo.Server project followed by
IdentityServerDemo.WebApi project. You can also configure how projects are
started using Visual Studio solution's properties dialog.

Once both the projects are started successfully, open the Postman tool and
get ready to hit the authorization server to retrieve a JTW.
In order to request a JWT from the authorization server you need to hit the
token endpoint (/connect/token) with a POST request. As a part of the request
body you need to supply the client details such as client id, client secret
code, user name, and password. Take a look at the following figure:

As you can see, we are making a POST request to IdentityServerDemo.Server
project's URL. The /connect/token is the token endpoint of the server. We also
include client_id, client_secret, grant_type, username, password, and scope
parameters in the request body. Notice that all these parameter values have been
configured in the previous part of this article series. Especially notice that
the scope is set to a space delimited values - employeesWebApi and roles. These
values come from the employeesWebApi ApiScope and roles IdentityResource
respectively. The client_id, client_secret values come from Client
configuration. And username and password come from TestUser configuration. Here,
we use user1 because it has Admin role assigned.
If all goes well you will be given a JWT as shown below:

Notice the access_token key of the returned JSON data. It holds the JWT that
can be passed to the Employees Web API in an attempt to invoke the Get() action.
Now let's use this JWT to invoke the Employees Web API.
Open another tab of Postman tool and initiate a GET request to the Employees
Web API.

Here, we make a GET request to /employees. We also set the HTTP Authorization
header to the JWT received earlier. Notice that the Authorization header value
is Bearer followed by a white space and then the JWT value.
If all goes well you will see the employee names in the response of the Web
API.

So far so good.
Now let's try to invoke the Employees Web API using user2 account. Recollect
that user2 doesn't belong to Admin role and therefore the request should fail.
Retrieve another JWT from the authorization server as described earlier but
use use2 credentials. Then try invoking the Web API using the newly retrieved
JWT. This time it will give
403 - Forbidden error.

This confirms that the Employees Web API can be invoked only by Admin users.
Next, we will invoke the Employees Web API using HttpClient.
Add a new MVC project to the same solution and name it
IdentityServerDemo.Client. This MVC project will be used for two purposes.
First, we will use it to invoke Employees Web API directly using HttpClient
(later in this part). Secondly, we will use this project to demonstrate OpenID
Connect configuration (next part).

Once the project gets created, add NuGet packages for IdentityModel and
Microsoft.AspNetCore.Authentication.OpenIdConnect to the client project.

Then add a new controller class named WebApiClientController to the
Controllers folder. And write the following code in the Index() action.
public IActionResult Index()
{
// get access token
var serverClient = new HttpClient();
serverClient.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue("application/text"));
Dictionary<string, string> param =
new Dictionary<string, string>();
param.Add("client_id", "client1");
param.Add("client_secret", "client1_secret_code");
param.Add("grant_type", "password");
param.Add("username", "user1");
param.Add("password", "password1");
param.Add("scope", "employeesWebApi roles");
var content = new FormUrlEncodedContent(param);
var serverResponse = serverClient.PostAsync
("http://localhost:5000/connect/token", content).Result;
string jsonData = serverResponse.Content.
ReadAsStringAsync().Result;
var accessToken = JsonSerializer.Deserialize<Token>
(jsonData);
// call web api
var apiClient = new HttpClient();
apiClient.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue("application/json"));
apiClient.SetBearerToken(accessToken.access_token);
var apiResponse = apiClient.GetAsync
("http://localhost:5020/Employees").Result;
var jsonApiData = apiResponse.Content.
ReadAsStringAsync().Result;
List<string> apiData = JsonSerializer.Deserialize
<List<string>>(jsonApiData);
return View(apiData);
}
This code is divided into two parts. The first part calls the /connect/token
end-point of the authorization server and retrieves a JWT. And the second part
invokes the Employees Web API by passing that JWT in the authorization header.
The first part mentioned above mimics the request made through the Postman
tool. It creates a Dictionary with the required parameters such as client_id,
client_secret, username, password, and scope. Since these parameters are to be
sent as a request body, we create a FormUrlEncodedContent object from the
Dictionary.
It then makes a POST request to the /connect/token end-point using HttpClient
and also passes the FormUrlEncodedContent object. The return JSON contains the
access_token and a few more keys (see Postman figure discussed earlier). We are
interested only in the access_token value. To de-serialize this value and use it
further in the code we use Deserialize() method of JsonSerializer class. The
Token used by the Deserialize() method looks like this:
public class Token
{
public string access_token{get;set;}
}
Now that we have the JWT access token we can proceed to calling the Employees
Web API.
The second part of the code does just that. It creates another HttpClient and
sets the Authorization header using SetBearerToken() method. It then calls the
GetAsync() method and retrieves the employee names in a List<string>. The list
of names is passed to the Index view for the sake of displaying in the browser.
The Index view that renders the list of employee names is shown below:
@model List<string>
@foreach (var item in Model)
{
<h2>@item</h2>
}
This completes the client code. In order to test this code, run the Server
project followed by the Web API project. And then run the Client project.
If all goes well you should get this result in the browser:

In the Index() action discussed above we made a raw POST request to the token
end-point using Dictionary, FormUrlEncodedContent, and HttpClient. There is an
alternate and bit simplified way to accomplish the same task. You can use the
RequestPasswordTokenAsync() extension method provided by IdentityModel NuGet
package. Let's see how that can be donw.
Modify the Index() action as shown below:
public IActionResult Index()
{
// get access token from server
var serverClient = new HttpClient();
serverClient.DefaultRequestHeaders.Accept.
Add(new MediaTypeWithQualityHeaderValue("application/text"));
var tokenResponse = serverClient.RequestPasswordTokenAsync
(new PasswordTokenRequest
{
Address = "http://localhost:5000/connect/token",
ClientId = "client1",
ClientSecret = "client1_secret_code",
GrantType = "password",
UserName = "user1",
Password = "password1",
Scope = "employeesWebApi roles"
}).Result;
// call web api
var apiClient = new HttpClient();
apiClient.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue("application/json"));
apiClient.SetBearerToken(tokenResponse.AccessToken);
var apiResponse = apiClient.GetAsync
("http://localhost:5020/Employees").Result;
var jsonApiData = apiResponse.Content.ReadAsStringAsync()
.Result;
List<string> apiData = JsonSerializer.Deserialize
<List<string>>(jsonApiData);
return View(apiData);
}
This time we grab an access token using the RequestPasswordTokenAsync()
extension method of HttpClient. We pass a PasswordTokenRequest object containing
the same details that we pass using the Dictionary earlier. Once we get the
access token we can call SetBearerToken() method by passing the AccessToken
value. The remaining code is identical to the previous example.
That's it for now! Keep coding!!