DEV Community

Bradley Wells
Bradley Wells

Posted on • Originally published at wellsb.com on

Protect Web API using IdentityServer4 and consume with Blazor

Original Article

In the previous tutorial, you created a public web API and learned the right way to access it from a server-side Blazor application. In this tutorial, you will make that web API private by securing it using IdentityServer4. Specifically, you will restrict access to the API to only select applications that are authenticated via a secret key. For example, you may want your API to be accessible from your company’s mobile application or website, but you do not want it to be public to the outside world.

In this Blazor tutorial series

Either follow the tutorial about using HttpClientFactory to access an external web API, or clone the previous tutorial’s GitHub repo to get caught up.

Create IdentityServer4 Authentication Server

To create the auth server, you will use IdentityServer4. Install the relevant Nuget packages by issuing the following commands in the Package Manager Console or in a PowerShell terminal.

Install-Package IdentityServer4
Install-Package IdentityServer4.Templates

Recall, in this series we are creating a contact management application using Blazor. Open the BlazorContacts solution in Visual Studio. In the Solution Explorer, right click the solution and select Open in File Explorer.

With the root solution folder open in your file explorer, you should see a BlazorContacts.API directory, a BlazorContacts.Shared directory, and a BlazorContacts.Web directory among other files and folders. Create a new folder called BlazorContacts.Auth.

Navigate to the new BlazorContacts.Auth folder in a PowerShell terminal and run the following command to scaffold a new IdentityServer4 empty template.

dotnet new is4empty

Once the template is created, you must add the new project to your existing BlazorContacts solution in Visual Studio. Right-click the solution and Add > Existing Project… Find the BlazorContacts/BlazorContacts.Auth folder you created and select the BlazorContacts.Auth.csproj file.

Your BlazorContacts.Auth project is templated with an empty Config.cs file that looks like the following:

using IdentityServer4.Models;
using System.Collections.Generic;

namespace BlazorContacts.Auth
{
    public static class Config
    {
        public static IEnumerable<IdentityResource> Ids =>
            new IdentityResource[]
            { 
                new IdentityResources.OpenId()
            };

        public static IEnumerable<ApiResource> Apis =>
            new ApiResource[] 
            { };

        public static IEnumerable<Client> Clients =>
            new Client[] 
            { };

    }
}

Configure IdentityServer4 Auth Server

In Config.cs , you will list the APIs you wish to secure, as well as any approved clients, such as a web front-end or a mobile application.

Start by adding a new API resource to the Apis enumerable, and give it a name.

public static IEnumerable<ApiResource> Apis =>
    new ApiResource[] 
    {
        new ApiResource("blazorcontacts-api")
    };

Next, list any approved clients by adding them to the Clients array.

public static IEnumerable<Client> Clients =>
    new Client[] 
    {
        new Client
        {
            AllowedGrantTypes = GrantTypes.ClientCredentials,
            ClientId = "blazorcontacts-web",
            ClientSecrets =
            {
                new Secret("thisismyclientspecificsecret".Sha256())
            },
            AllowedScopes = { "blazorcontacts-api" }
        }
    };

The AllowedGrantTypes line indicates that authentication is performed using client credentials. That is to say that the combination of client ID and a secret key are used to authenticate the client application itself to the auth server. Next, you defined the ClientID and Secret that will be used to authenticate your Blazor application. Finally, the AllowedScopes line indicates that this particular client should have access to the blazorcontacts-api API that was previously defined.

Note : It is important to use secure keys and to securely hash them. It is never a good idea to include secrets directly in your application’s source code. In another tutorial, you will learn a more secure technique for including sensitive data in your projects.

Configure API

With the authentication server set up, it will issue access tokens to the client, authorizing it to access interfaces within its allowed scopes. You now need to configure the web API and clients, themselves. The API should operate as blazorcontacts-api, as defined in Config.cs of BlazorContacts.Auth.

Install and reference the Microsoft.AspNetCore.Authentication.JwtBearer package for the BlazorContacts.API project. Then, open the Startup.cs file of BlazorContacts.API and locate the ConfigureServices() method. Here, you will configure the authentication service of the API.

services.AddAuthentication("Bearer")
  .AddJwtBearer("Bearer", options =>
  {
      options.Authority = "http://localhost:5000";
      options.RequireHttpsMetadata = false;

      options.Audience = "blazorcontacts-api";
  });

The above code connects a JWT bearer (###Define###) with issuing authority of http://localhost:5000. This should correspond to the address of BlazorContacts.Auth server. In this test environment, we are not using SSL certificates, but feel free to require them in your production application. Finally, the Audience should correspond to the name you assigned to the API resource when you added it to the Auth server’s Apis array. In this case, we are using blazorcontacts-api.

With the service configured, you must actually start the service by adding it to the ServiceCollection container. In the Configure() method of Startup.cs (still within BlazorContacts.API), add the following lines:

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

Now, any controller in the API can be forced to require authorization by simply adding a [Authorize] annotation to the appropriate class.

For example, the ContactsController.cs controller file contained a class with methods to GET, POST, PUT, and DELETE contacts. To require authorization to access these API methods, start by including the following namespace.

using Microsoft.AspNetCore.Authorization;

This enables you to use the [Authorize] annotation. Next, annotate parts of the API that you wish to protect. You could use tag for individual methods, or for the entire class. You could restrict access to the entire ContactsController class by adding the [Authorize] tag above the class definition:

[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ContactsController : ControllerBase
{
...
}

Remember, the authorization technique you have configured does not require individual user login to consume the API; instead, it ensures that the clients attempting to access the API are using a valid CliendID/Secret pair. Now, you simply need to configure your Blazor web app to use the client ID and secret key you allowed when setting up the Authentication server.

Configure Blazor Web App

When attempting to make an API call that requires authorization, the web app must include an access token in the header of the request. This access token, issued by the authentication server, will contain the unique client ID and secret key. When the API receives an access request, it will check that the access token exists, and confirm its authenticity with the authentication server before repsonding to the request.

To begin this handshaking procedure, modify the ApiService.cs file of the BlazorContacts.Web project that you created in the last tutorial. You will need to include in your BlazorContacts.Web project a package reference for the IdentityModel package. Then, add a method to fetch an access token from the auth server. The most basic form for this feature would be the following, though it would be wise to add some error handling. See the source code for a more robust version of this method.

private async Task<string> requestNewToken()
{
    var discovery = await HttpClientDiscoveryExtensions.GetDiscoveryDocumentAsync(
        _httpClient, "http://localhost:5000");

    var tokenResponse = await HttpClientTokenRequestExtensions.RequestClientCredentialsTokenAsync(_httpClient, new ClientCredentialsTokenRequest
    {
        Scope = "blazorcontacts-api",
        ClientSecret = "thisismyclientspecificsecret",
        Address = discovery.TokenEndpoint,
        ClientId = "blazorcontacts-web"
    });

    return tokenResponse.AccessToken;
}

The variable discovery uses the instance of HttpClient created by the HttpClientFactory and sets an endpoint for the authentication server. This endpoint should correspond to the base address of the authentication server. It is used to fetch an access token for the scoped API, using the client ID of the Blazor web app and the secret key set in Config.cs of the BlazorContacts.Auth project.

Note: In a production project, you should never include values such as ClientId and ClientSecret directly in the source code. In another tutorial, you will learn how to securely pull secret values into your project.

To make a call to an API requiring authorization, you must use the string returned by RequestNewToken() and then set that string as the bearer token of the instance of HttpClient that is attempting to consume the API.

var access_token = await requestNewToken();
_httpClient.SetBearerToken(access_token);

The GetContactsAsync() method, for example, might look like the following:

public async Task<List<Contact>> GetContactsAsync()
{
    var access_token = await requestNewToken();
    _httpClient.SetBearerToken(access_token);

    var response = await _httpClient.GetAsync("api/contacts");
    response.EnsureSuccessStatusCode();

    using var responseContent = await response.Content.ReadAsStreamAsync();
    return await JsonSerializer.DeserializeAsync<List<Contact>>(responseContent);
}

This technique works, but it requests a new access token for every API call. This puts an unnecessary load on the authentication server. In another tutorial, you will learn how to cache the access token for a defined period of time to maximize efficiency.

Putting it all together

To test your application, don’t forget the Debug > Set Startup Projects dialog should be configured for Multiple startup projects , and BlazorContacts.Auth, BlazorContacts.API, and BlazorContacts.Web should all be set to Start.

Once you run the program and navigate to the Contacts page, an access token will be fetched and your Blazor application will be authorized to consume the web API and list all contacts.

In this tutorial, you learned how to secure a Web API and access it from a Blazor server-side application using HttpClientFactory. You also learned how to grant API access to specific front-end applications, such as web or mobile apps. The techniques you learned in this tutorial can be extended to other ASP.NET web apps, including the MVC and Razor Pages framework. Finally, this authentication approach has the added benefit of still being able to use another authorization technique, such as Azure AD B2C, to provide individual user authorization on the front-end of your application.

In the coming tutorials, you will also learn how to add caching to this project to reduce unnecessary access token requests to the authentication server. You will also learn how to securely pull secret keys and other sensitive values into your project.

The source code for this project is available on GitHub.

Original Article

Top comments (0)