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


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 : 10 August 2020


Tags : ASP.NET ASP.NET Core MVC .NET Framework 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