© Les Jackson 2020
L. JacksonThe Complete ASP.NET Core 3 API Tutorialhttps://doi.org/10.1007/978-1-4842-6255-9_14

14. Securing Our API

Les Jackson1 
(1)
Melbourne, VIC, Australia
 

Chapter Summary

In this chapter we discuss how we can secure our API; specifically, we’ll add the “Bearer” authentication scheme into the mix that will allow only authorized clients to access our API resource through the use of Tokens.

When Done, You Will

  • Understand the Bearer authentication scheme.

  • Use Azure Active Directory to secure our API.

  • Create a simple client that is authorized to use the API.

  • Deploy to Azure.

We have a lot to cover – so let’s get going!

What We’re Building

Our Authentication Use Case

Before delving into the technicalities of our chosen authentication scheme, I just wanted to cover our authentication use case. For this example, we are going to “secure” our API by using Azure Active Directory (AAD), and then create and configure a client (or daemon) app with the necessary privileges to authenticate through and use the API. We are not going to leverage “interactive” user-entered User Ids and passwords. This use case is depicted in Figure 14-1.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig1_HTML.jpg
Figure 14-1

Authentication use case

Overview of Bearer Authentication

There are a number of authentication schemes that we could have used, a non-exhaustive list is provided in the table.

Scheme

Description

Basic

A common, relatively simple authentication scheme. Requires the supply of a user name and password that’s then encoded as a Base64 string; this is then added to the authorization header of a http request. Natively, this is not encrypted, so it’s not that secure, unless you opt so make requests over https, in which case the transport is encrypted

Digest

Follows on from Basic Authentication but is more secure as it applies a hash function to any sensitive data (e.g. username and password) before sending

Bearer

Token-based authentication scheme where anyone in possession of a valid “token” can gain access to the associated secured resources, in this case our API. Considered secure, it is widely adopted in industry and is the scheme (specified in RFC 6750); we’ll use to secure our API

NTLM

Microsoft-specific authentication scheme, using Windows credentials to authenticate. Perfectly decent, secure scheme but as it’s somewhat “proprietary” (and I’m trying to avoid that), we’ll leave our discussion there for now

Bearer Token vs. JWT

The use of “tokens” in Bearer authentication is a central concept. A token is issued to a requestor (in this case a daemon client) and the client (or “bearer of the token”) then presents it to a secure resource in order to gain access.

So, what’s JWT?

JWT (or JSON Web Tokens) is an encoding standard (specified in RFC 7519) for tokens that contain a JSON payload. JWTs can be used across a number of applications; however, in this instance, we’re going to use JWT as our encoded token through our use of Bearer authentication.

In short
  • Bearer authentication is the authentication scheme that makes use of (bearer) “tokens.”

  • JWT is a specific implementation of bearer tokens, in particular those with a JSON payload.

Again, rather than dwelling on copious amounts of theory, the concepts will make more sense as we build them below.

Build Steps

As I’ve mentioned before, I like a bit of 50,000ft view of what we’re going to build before we start building it as it helps contextualize what we need to do, and it also allows us to understand the progress we’re making. Therefore, in terms of the configuration and coding we need to perform, I’ve detailed the steps we’ll follow here.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig2_HTML.jpg
Figure 14-2

API build steps

../images/501438_1_En_14_Chapter/501438_1_En_14_Fig3_HTML.jpg
Figure 14-3

Client build steps

Steps for Our API Project

Steps for Our Daemon Client

You can see there is actually a lot to do – so let’s get on it!

Registering Our API in Azure AD

The first thing we need to do is register our API with Azure Active Directory (AAD), as we’re using AAD as our Identity and Access Management directory service.

Les’ Personal Anecdote

../images/501438_1_En_14_Chapter/501438_1_En_14_Figa_HTML.jpgOne of my first jobs out of university was as part of a team supporting a large (I believe at the time the second largest in the world) deployment of Novell NetWare Directory Services (NDS), which was weird as I had neither the background nor inclination to learn NDS.

Anyhow, this product was considered relatively leading-edge at the time as it took the approach of storing user accounts (as well as other “organizational objects”) in a hierarchical directory tree structure that was both distributed and replicated (in this case) nationwide. In short it was hugely scalable and could cater for 10,000s (we had well over 100,000) of user accounts.

At the time Microsoft only used Windows NT Domains which were arguably more basic (they were “flat”), less scalable, distributable, and reliable than their NetWare counterparts. Blue screen of death anyone?

Microsoft was obviously, cough, “inspired,” cough again, by NDS (and Banyan Vines – see next section) to such an extent that they brought out a rival product, Active Directory, which bore a remarkable resemblance to, drum roll, NDS. You could argue this was poetic justice as Novell had been “inspired” by an earlier product called Banyan VINES.1 Interestingly, Jim Allchin, engineering supremo at Banyan, joined Microsoft due to creative and strategic differences with the Banyan leadership.

The rest is history.

Banyan and Novell’s products withered and died due to a number of different strategic missteps, as well as the fact that Microsoft had a compelling value proposition.

So, if you use a Windows PC at work and have to “log-in,” then you’re most likely logging into an Active Directory. Now with the emergence of Azure, you don’t even need to host your AD on premise and can opt to use Azure Active Directory, which is what we’ll be using for this chapter.

Create a New AD?

Now this step is optional, but I have created a “test” AAD in addition to the AAD that gets created when you sign up for Azure. This is really just to ring-fence what is in essence my “production AAD” (the one that holds my login for Azure) from any development activities I undertake.

You can create a new AAD in exactly the same way as you create any other resources in Azure, so I won’t detail the steps here. If you do opt for this approach though (remember it is optional), the only thing you need to be aware of is that when you want to create objects in your “Development AAD,” you’ll need to switch to it in the Azure Portal.

Switching Between AADs

To switch between your AADs, click the person icon at the top right hand of the Azure Portal.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig4_HTML.jpg
Figure 14-4

Switching Active Directory

On the resulting pop-up, you can then click Switch Directory (see circled section on Figure 14-4); you should then get the option to select and switch between the AADs you have (I have two as you can see in Figure 14-5).
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig5_HTML.jpg
Figure 14-5

I created a second AD for test purposes

Register Our API

Select the AAD you’re using for this exercise; the click Azure Active Directory from your portal landing page.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig6_HTML.jpg
Figure 14-6

Select the AD you want to work with

This should then take you into the Azure Active Directory overview screen.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig7_HTML.jpg
Figure 14-7

Select App registrations

s

Select “App registrations” as shown in Figure 14-7. You can see from the next example that I already have an existing app registered on my AAD, but we’re going to create a new one for our CommandAPI running on our “development” environment (i.e., the one running locally on our PC). We’ll come on to our Azure-deployed API later.

../images/501438_1_En_14_Chapter/501438_1_En_14_Figb_HTML.jpg Even though we are running our development API on our local machine, we can still make use of AAD as our Identity management service (assuming our development PC has connectivity to the Internet!).

The point I’m making here is that we can use AAD no matter where our APIs (and client for that matter) are located.

../images/501438_1_En_14_Chapter/501438_1_En_14_Fig8_HTML.jpg
Figure 14-8

Create a new registration

Select “New registration,” and you’ll see the following.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig9_HTML.jpg
Figure 14-9

Configure the registration

Enter a name for the app registration; it can be anything, but make it meaningful, (I’ve appended “_DEV” to this registration to differentiate it from any Production Registrations we subsequently create). Also, ensure that “Accounts in this organization directory only” ([Your AAD Name] only – Single tenant) is selected.

We don’t need a Redirect URI, so click “Register” to complete the initial registration, after which you’ll be taken to the overview screen.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig10_HTML.jpg
Figure 14-10

We'll use client Id and tenant id

Here, we are introduced to the first two important bits of information that we need to be aware of:
  1. 1.

    Application (client) ID

     
  2. 2.

    Directory (tenant) ID

     

Going forward I’m going to use the terms Client ID and Tenant ID, but what are they?

Client ID

The client ID is essentially just a unique identifier that we can refer to the Command API in reference to our AAD.

Tenant ID

A unique id relating to the AAD we’re using, remembering that we can have multiple (i.e. multi-tenant) AADs at our disposal.

We’ll come back to these items later when we come to configuring things at the application end; for now we need to move on as we’re not quite finished.

Expose Our API

So far, we’ve merely registered our API; we now need to expose it for use, so click “Expose an API” from our left-hand menu options on our Registrations page.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig11_HTML.jpg
Figure 14-11

Exposing our API

What we need to do here is create an “Application ID URI” (sometimes referred to as a “Resource ID”), so click “Set” as shown in Figure 14-12.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig12_HTML.jpg
Figure 14-12

Set the Resource ID

Azure will provide a default suggestion for this; go with it (it’s the Client ID with “api://” prepended).
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig13_HTML.jpg
Figure 14-13

Auto-generated Resource ID (Application ID URI)

Click Save and you’re done. Clicking back into the overview of the app registration, you should see this reflected here too.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig14_HTML.jpg
Figure 14-14

Resource ID is created

We’re almost finished with our API configuration in AAD but have one more bit of configuration to complete.

Update Our Manifest

Here, we update the appRoles section of our application manifest which specifies the type of application role(s) that can access the API. In our case, we need to specify a noninteractive “daemon” app that will act as our API client. More information on the Application Manifest can be found in Microsoft Docs.2

Anyway, back to the task at hand, we need to insert the following JSON snippet at the appRoles section of our manifest:
.
.
.
"appRoles": [
  {
    "allowedMemberTypes": [
      "Application"
    ],
    "description": "Daemon apps in this role can consume the web api.",
    "displayName": "DaemonAppRole",
    "id": "6543b78e-0f43-4fe9-bf84-0ce8b74c06a3",
    "isEnabled": true,
    "lang": null,
    "origin": "Application",
    "value": "DaemonAppRole"
  }
],
.
.
.
So, click “Manifest” in the left-hand window of our App Registration config page.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig15_HTML.jpg
Figure 14-15

Update the manifest

And insert the json given earlier into the correct spot (essentially updating the existing empty appRoles section).
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig16_HTML.jpg
Figure 14-16

Ensure you update the manifest correctly

Make sure you keep the integrity of the json, and don’t omit or introduce any additional commas. You can always use something like https://jsoneditoronline.org/ to check.

You can add multiple appRoles to this section; we need only one, although if you do decide to add some additional roles, you’ll need to ensure that the “id” attribute is a unique GUID. You can use the example GUID I’ve supplied with the JSON here, or you can create your own (you can use the same GUID’s across different AADs – you just can’t duplicate them in the same AAD).

When completed, don’t forget to save the file.

That’s it for our API registration in Azure; we need to move over to our API now and make some config and code changes so it can make use of AAD for authorization.

Add Configuration Elements

We need to make our API “aware” of the AAD settings we’ve just set up so that it can use AAD for authenticating clients. We need to configure
  • The log-in “Instance”

  • Our AAD Domain

  • The Tenant ID

  • The Client ID

  • The Application ID URL (or Resource ID)

../images/501438_1_En_14_Chapter/501438_1_En_14_Figc_HTML.jpg Remember we’re currently working with our API in our Development Environment, before we move on to configuring our API on Azure.

As we’ve already discussed, you can store your application config in a number of places (e.g., appsettings.json, appsettings.Development.json, etc.); in this section, I’m going to make use of User Secrets once again (refer to Chapter 8 for a refresher).

The primary reason I’m taking this approach is that I’ll be pushing my code up to a public GitHub repository and I don’t want those items visible in something like appsettings.json.

The table details the name of the user secret variables I’m going to use for each of the config elements.

Config element

User secret variable

The log-in “instance”

Instance

Our AAD Domain

Domain

The Tenant ID

TenantId

The Client ID

ClientId

The Application UD URL (Or Resource ID)

ResourceId

As a quick refresher to add the “Instance” User Secret, at a command prompt “inside” the API Project root folder (CommandAPI), type:
dotnet user-secrets set "Instance" "https://login.microsoftonline.com/"

This will add a value for our Login Instance (you should use the same value I’ve used here). The other User Secrets I’ll leave for you to add yourself, as the values you need to supply will be unique to your own App Registration (refer to these values on the App Registration overview screen for your API).

After adding all my User Secrets, the contents of my secrets.json file now looks like this.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig17_HTML.jpg
Figure 14-17

Example contents of my secrets.json file

Some points to note

  • The value you have for Instance should be exactly the same as I’ve used earlier.

  • The values you have for UserID and Password may be the same as what I’ve just shown if you’ve been following the tutorial exactly as I’ve described (they may of course be different if you’ve chosen your own values!).

  • The values you have for TenantId, Domain, ClientId, and ResourceId will be different to mine.3

Update Our Project Packages

Before we start coding, we need to add a new package that will be required to support the code we’re going to introduce, so at a command prompt “inside” the API project, type
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
This should successfully add the following package reference to the .csproj file.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig18_HTML.jpg
Figure 14-18

Add Reference to allow JWT Bearer Authentication

Updating our Startup Class

Over in the startup class of our API project, we need to update both our ConfigureServices and Configure methods. First though, add the following using directive to the top of the startup class file:
using Microsoft.AspNetCore.Authentication.JwtBearer;

Update Configure Services

We need to set up bearer authentication in the ConfigureServices method ; to do so, add the following code (new code is highlighted):
.
.
.
services.AddDbContext<CommandContext>(opt => opt.UseNpgsql(builder.ConnectionString));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(opt =>
  {
    opt.Audience = Configuration["ResourceId"];
    opt.Authority = $"{Configuration["Instance"]}{Configuration["TenantId"]}";
  });
services.AddControllers();
.
.
.
To put the changes in context, it should look like this.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig19_HTML.jpg
Figure 14-19

register Authentication service in Startup

The preceding code adds authentication to our API, specifically Bearer authentication using JWT Tokens. We then configure two options:
  • Audience: We set this to the ResourceID of our App Registration in Azure.

  • Authority: Our AAD Instance that is the token issuing authority (a combination of Instance and TenantId).

Update Configure

All we need to do now is add authentication and authorization to our request pipeline via the Configure method :
app.UseAuthentication();
app.UseAuthorization();
as shown in Figure 14-20.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig20_HTML.jpg
Figure 14-20

Update the configure method in Startup

Authentication vs. Authorization

As we’ve added both Authentication and Authorization to our request pipeline, I just want to quickly outline the difference between these two concepts before we move on:
  • Authentication (The “Who”): Verifies who you are, essentially it checks your identity is valid.

  • Authorization (The “What”): Grants the permissions/level of access that you have.

So in our example, our client app will be authenticated via AAD; once it has, we can then determine what endpoints it can call on our API (authorization).

Warning!

../images/501438_1_En_14_Chapter/501438_1_En_14_Figd_HTML.jpgAs authentication happens first (we need to identify you before we can authorize you to do anything), the order in which you add these components to the Request Pipeline (via the Configure method) is critically important. So please make sure you add them in the order specified earlier.

Refer back to Chapter 4 on our brief discussion on the Request Pipeline if you’ve forgotten (it was a while ago!); for a more in-depth conversation, refer to the Microsoft Docs.4

Update Our Controller

We have added the foundations of Bearer authentication using JWT tokens to our Startup class to enable it to be used throughout our API, but now we want to use it to protect one of our endpoints. We can of course protect the entire API, but let’s just start small for now. We can pick any of our API endpoints, but let’s just go with one of our simple GET methods, specifically our ability to retrieve a single Command.

Before we update our controller action, just make sure you add the following using directive at the top of our CommandsController class :
using Microsoft.AspNetCore.Authorization;
The new code for our controller action is simple; we just decorate it with the [Authorize]attribute as shown here:
[Authorize]
[HttpGet("{id}", Name = "GetCommandById")]
public ActionResult<CommandReadDto> GetCommandById(int id)
{
  var commandItem = _repository.GetCommandById(id);
  if (commandItem == null)
  {
    return NotFound();
  }
    return Ok(_mapper.Map<CommandReadDto>(commandItem));
 }
Save all the new code, build, then run the API locally. Once running, make a call to our newly protected endpoint in Postman.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig21_HTML.jpg
Figure 14-21

Our endpoint is secured

Here, you will see
  1. 1.

    We get a 401 Unauthorized response

     
  2. 2.

    Selecting the return headers, we see

     
  3. 3.

    That the authentication type is “Bearer” (and we have a token error back from AAD)

     
To double-check we have only protected this endpoint, make a call to our other GET action, and you’ll see we still get a list of commands back.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig22_HTML.jpg
Figure 14-22

This endpoint is not secured and can still be accessed

Learning Opportunity

../images/501438_1_En_14_Chapter/501438_1_En_14_Fige_HTML.jpgWhat happens if we run our Unit Test suite? Will some of our tests break because we require authorization on one of our API endpoint methods? If not, why not?

Register Our Client App

In the next section, we’re going to write a simple .NET Core Console application that will act as an authorized “client” of the API. As this is a “daemon app,” it needs to run without user authentication interaction, so we need to configure it as such.

../images/501438_1_En_14_Chapter/501438_1_En_14_Figf_HTML.jpg There are a number of different authentication use cases we could explore when it comes to consuming an API, for example, a user authenticating against AAD (username/password combo), to grant access to the API.

The use case I’ve decided to go with in this example (a “daemon app”) resonated with me more in terms of a real-world use case.

Back over in Azure, select the same AAD that you registered the API in, and select App Registrations once again.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig23_HTML.jpg
Figure 14-23

Create an App Registration for our client app

Then select “+ New registration,” and on the resulting screen enter a suitable name for our client app as shown next.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig24_HTML.jpg
Figure 14-24

Name the registration

Again, select the Single tenant supported account type option, and click “Register”; this will take you to the overview screen of your new app registration.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig25_HTML.jpg
Figure 14-25

Client registration overview

As before it will prepopulate some of the config elements for you, for example, Client ID, Tenant ID, etc.

Learning Opportunity

../images/501438_1_En_14_Chapter/501438_1_En_14_Figg_HTML.jpgWhat do you notice about the Tenant ID for our client registration when compared to the Tenant ID of API registration?

Create a Client Secret

Next click “Certificates & secrets” in the left-hand menu.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig26_HTML.jpg
Figure 14-26

Create a client secret

Here we are going to configure a “Client Secret.” This is a unique ID that we will use in combination with our other app registration attributes to identify and authenticate our client to our API. Click “+ New client secret.”
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig27_HTML.jpg
Figure 14-27

Select New client Secret

And on the resulting screen, give it
  • A description (can be anything but make it meaningful)

  • An expiry (you have a choice of 3 options)

../images/501438_1_En_14_Chapter/501438_1_En_14_Fig28_HTML.jpg
Figure 14-28

Name the secret and set expiry

When you’re happy, click “Add.”

Warning!

../images/501438_1_En_14_Chapter/501438_1_En_14_Figh_HTML.jpgMake sure you take a copy of the client secret now; shortly after creation it will not be displayed in full again – you’ll only see a redacted version, and you won’t be able to retrieve it unlike our other registration attributes.

This is a by design security feature to help stop the unauthorized propagation of the client secret (which is effectively a password).

Configure API Permissions

Now click “API Permissions”; here we are going to (drum roll please) configure access to our Command API.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig29_HTML.jpg
Figure 14-29

Setup Permissions to our API

Click “+ Add a permission.”
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig30_HTML.jpg
Figure 14-30

Add a permission

In the “Request API permissions” window that appears, select the “My APIs” tab.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig31_HTML.jpg
Figure 14-31

select "My APIs"

And find the Command API, and select it.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig32_HTML.jpg
Figure 14-32

Select the CommandAPI_DEV instance

On the resulting screen, ensure that
  1. 1.

    Application permissions is selected.

     
  2. 2.

    You “check” the DaemonAppRole Permission.

     
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig33_HTML.jpg
Figure 14-33

Configure permissions accordingly

When you’re happy, click “Add permission,” and your permission will be added to the list.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig34_HTML.jpg
Figure 14-34

Grant consent

You’ll notice
  1. 1.

    The permission has been “created” but not yet “granted.”

     
  2. 2.

    You’ll need to click the “Grant admin consent for <Name of Your AAD Here5>” button – do so now.

     
You may get a Microsoft authentication pop-up; authenticate and accept any permissions requests you get (don’t worry if this does not appear – it looks like this may be one of those ever-changing UI updates).
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig35_HTML.jpg
Figure 14-35

You may be asked to accept permission request

Either way, you’ll be returned to the Configure permissions window, where after a short time, your newly created API Permission will have been granted access.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig36_HTML.jpg
Figure 14-36

Permissions fully granted

And with that, the registration of our (yet to be created) client app is complete.

Create Our Client App

The final part of this chapter is to create a simple client that we can use to call our protected API, so we’re going to create new console project to do just that.

../images/501438_1_En_14_Chapter/501438_1_En_14_Figi_HTML.jpg I don’t consider this app part of our “solution” (containing our API and Test Projects), so I’m going to create it in a totally separate working project directory outside of CommandAPISolution folder.

Note

As we’ll only be creating 1 project, I’m not going to make use of a “solution” structure.

You can find the code to this project here on GitHub:

https://github.com/binarythistle/Secure-Daemon-Client/

At a command prompt in a new working directory “outside” of our CommandAPISolution folder, type
dotnet new console -n CommandAPIClient
Once the project has been created, open the project folder CommandAPIClient in your development environment, so if you’re using VS Code, you could type
code -r CommandAPIClient

This will open the project folder CommandAPIClient in VS Code.

Our Client Configuration

As I’m making this code available on GitHub for you to pull down and use, I’m deliberately going to store the config in an appsettings.json file as opposed to using User Secrets, as it will be easier for you to get going with it quickly if you choose to work with the code from the repo.6 We will, therefore, be storing sensitive config elements in here; therefore, for production systems you would not do this!

Learning Opportunity

../images/501438_1_En_14_Chapter/501438_1_En_14_Figj_HTML.jpgFollowing the approach we took for our API; “convert” the Client App example here to use user secrets.

Create an appsettings.json file in the root of your project folder; once done it should look like this if you’re using VS Code.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig37_HTML.jpg
Figure 14-37

Create an Appsettings.json file

Into that file, add the following JSON; making sure to populate the correct values for your client application registration (TenantId, ClientId, and ClientSecret), and in the case of the ResourceId, make sure it’s the ResourceId for the API:
{
  "Instance": "https://login.microsoftonline.com/{0}",
  "TenantId": "[YOUR TENANT ID]",
  "ClientId": "[YOUR CLIENT ID]",
  "ClientSecret": "[YOUR CLIENT SECRET]",
  "BaseAddress": "https://localhost:5001/api/Commands/1",
  "ResourceId": "api://[YOUR API CLIENT ID]/.default"
}
So, for example, my file looks like this.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig38_HTML.jpg
Figure 14-38

Client configuration

A couple of points to just double-check on:
  • BaseAddress: This is just the local address of the command API (we’ll update to our production URL later). Note that I’m deliberately specifying the API Controller Action that requires authorization.

  • ResourceId: This is the ResourceId of our API App Registration.

The other attributes are straightforward and can be retrieved from Azure, except the ClientSecret which you should have made a copy of when you created it.

Warning!

../images/501438_1_En_14_Chapter/501438_1_En_14_Figk_HTML.jpgAll the attributes given are enough to get access to our restricted API without the need for any additional passwords, etc. So, you should not store it like this in production; you should make use of user secrets or something similar.

Again, I’ve chose to provide it in an appsettings.json file to allow you to get up and running quickly with the code and have left it as a learning exercise for you to implement the user secrets approach.

Add Our Package References

Before we start coding, we need to add some package references to our project to support some of the features we’re going to use, so we’ll add
  • Microsoft.Extensions.Configuration

  • Microsoft.Extensions.Configuration.Binder

  • Microsoft.Extensions.Configuration.Json

  • Microsoft.Identity.Client

I prefer to do this by using the dotnet CLI , so as we’ve done previously, ensure your “in” the correct project folder (if you’re following the tutorial exactly you should be “in” the CommandAPIClient folder), and issue the following command to add the first of our packages:
dotnet add package Microsoft.Extensions.Configuration
Repeat so you add all four packages; your project .csproj file should look like this when done.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig39_HTML.jpg
Figure 14-39

Package References for our client

Client Configuration Class

For ease of use, we’re going to create a custom class that will allow us to read in our appsettings.json file and then access those config elements as class attributes. In the client project, create a new class file in the root of the project, and call it AuthConfig.cs as shown in Figure 14-40.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig40_HTML.jpg
Figure 14-40

AuthConfig class to read in and manage client configuration

Then enter the following code:
using System;
using System.IO;
using System.Globalization;
using Microsoft.Extensions.Configuration;
namespace CommandAPIClient
{
  public class AuthConfig
  {
    public string Instance {get; set;} =
      "https://login.microsoftonline.com/{0}";
    public string TenantId {get; set;}
    public string ClientId {get; set;}
    public string Authority
    {
      get
      {
        return String.Format(CultureInfo.InvariantCulture,
                             Instance, TenantId);
      }
    }
    public string ClientSecret {get; set;}
    public string BaseAddress {get; set;}
    public string ResourceID {get; set;}
    public static AuthConfig ReadFromJsonFile(string path)
    {
      IConfiguration Configuration;
      var builder = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile(path);
      Configuration = builder.Build();
      return Configuration.Get<AuthConfig>();
    }
  }
}
When complete your AuthConfig class should look like this.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig41_HTML.jpg
Figure 14-41

Walk-through of Authconfig class

Notable code listed here
  1. 1.

    We combine the Instance and our AAD Tenant to create something called the “Authority”; this is required when we come to attempting to connect our client later.

     
  2. 2.

    Our class has one static method that allows us to specify the name of our JSON config file.

     
  3. 3.

    We create an instance of the .NET Core Configuration subsystem.

     
  4. 4.

    Using ConfigurationBuilder, we read the contents of our json config file.

     
  5. 5.

    We pass back our read-in config bound to our AuthConfig class.

     
To quickly test that this all works, perform a build, and assuming we have no errors, move over to our Program class, and edit the Main method so it looks like this:
static void Main(string[] args)
{
  AuthConfig config = AuthConfig.ReadFromJsonFile("appsettings.json");
  Console.WriteLine($"Authority: {config.Authority}");
}
Build your code again then run it; assuming all is well, you should get output similar to this.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig42_HTML.jpg
Figure 14-42

Run the client

Finalize Our Program Class

As mentioned previously, the first thing our client will have to do is obtain a JWT token that it will then attach to all subsequent requests in order to get access to the resources it needs, so let’s focus in on that.

Still in our Program class, we’re going to create a new static asynchronous method called RunAsync; the code for our reworked Program class is shown next (noting new or changed code is bold and highlighted):
using System;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
namespace CommandAPIClient
{
  class Program
  {
    static void Main(string[] args)
    {
        Console.WriteLine("Making the call...");
        RunAsync().GetAwaiter().GetResult();
    }
    private static async Task RunAsync()
    {
      AuthConfig config = AuthConfig.ReadFromJsonFile("appsettings.json");
      IConfidentialClientApplication app;
      app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
          .WithClientSecret(config.ClientSecret)
          .WithAuthority(new Uri(config.Authority))
          .Build();
      string[] ResourceIds = new string[] {config.ResourceID};
      AuthenticationResult result = null;
      try
      {
        result = await app.AcquireTokenForClient(ResourceIds).ExecuteAsync();
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine("Token acquired ");
        Console.WriteLine(result.AccessToken);
        Console.ResetColor();
      }
      catch (MsalClientException ex)
      {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(ex.Message);
        Console.ResetColor();
      }
    }
  }
}
I’ve tagged the points of interest here.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig43_HTML.jpg
Figure 14-43

Progressing the client

  1. 1.

    Our RunAsync method is asynchronous and returns a result we’re interested in, so we chain the GetAwaiter and GetResult methods to ensure the console app does not quit before a result is processed and returned.

     
  2. 2.

    ConfidentialClientApplication is a specific class type for our use case; we use this in conjunction with the ConfidentialClientApplicationBuilder to construct a “client” with our config attributes.

     
  3. 3.

    We set up our app with the values derived from our AuthConfig class.

     
  4. 4.

    We can have more than one ResourceId (or scope) that we want to call; hence, we create a string array to cater for this.

     
  5. 5.

    The AuthenticationResult contains (drum roll) the result of a token acquisition.

     
  6. 6.

    Finally, we make an asynchronous AcquireTokenForClient call to (hopefully!) return a JWT Bearer token from AAD using our authentication config.

     
Save the file, build your code, and assuming all’s well, run it too; you should see the following.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig44_HTML.jpg
Figure 14-44

Successful token acquisition

Celebration Checkpoint

../images/501438_1_En_14_Chapter/501438_1_En_14_Figl_HTML.jpgGood job! There was a lot of config and coding to get us to this point, obtaining a JWT token, so the rest of this chapter is all too easy!

So well done!

We move onto the second and final part of our RunAsync method, and that is to call our protected API endpoint with the token we just obtained in the previous step, so directly after the catch statement in our RunAsync method, add the following code (take note of the three additional using statements too):
using System.Net.Http;
using System.Net.Http.Headers;
using System.Linq;
.
.
.
if (!string.IsNullOrEmpty(result.AccessToken))
{
  var httpClient = new HttpClient();
  var defaultRequestHeaders = httpClient.DefaultRequestHeaders;
  if(defaultRequestHeaders.Accept ==null ||
     !defaultRequestHeaders.Accept.Any(m => m.MediaType == "application/json"))
  {
    httpClient.DefaultRequestHeaders.Accept.Add(new
      MediaTypeWithQualityHeaderValue("application/json"));
  }
  defaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("bearer", result.AccessToken);
  HttpResponseMessage response = await httpClient.GetAsync(config.BaseAddress);
  if (response.IsSuccessStatusCode)
  {
    Console.ForegroundColor = ConsoleColor.Green;
    string json = await response.Content.ReadAsStringAsync();
    Console.WriteLine(json);
  }
  else
  {
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine($"Failed to call the Web Api: {response.StatusCode}");
    string content = await response.Content.ReadAsStringAsync();
    Console.WriteLine($"Content: {content}");
  }
  Console.ResetColor();
}
I’ve highlighted some interesting code sections here.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig45_HTML.jpg
Figure 14-45

Calling the API

  1. 1.

    We use a HttpClient object as the primary vehicle to make the request.

     
  2. 2.

    We ensure that we set the media type in our request headers appropriately.

     
  3. 3.

    We set out authorization header to “bearer” as well as attaching our token received in the last step.

     
  4. 4.

    Make an asynchronous request to our protected API address.

     
  5. 5.

    Check for success and display.

     
Save your code, build it, and run it (also ensure the Command API is running); you should see something like the following.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig46_HTML.jpg
Figure 14-46

Secure API Called

where we have the JSON for our protected API endpoint returned.

Note: If you get at error similar to the following:
System.Security.AuthenticationException, the remote certificate is invalid.
Just check that you took the steps in Chapter 2 to “trust” local SSL Certificates. If you’re too lazy to pop back, just type the following at a command line and rerun the client:
dotnet dev-certs https --trust

Updating for Azure

In order for our API code to continue to work when we deploy to Azure, we’re going to have to add the following Application Settings to our Command API on Azure (remember we currently have these stored as user secrets in our local development instance).

Config element

Application setting name

The log-in “instance”

Instance

Our AAD Domain

Domain

The Tenant ID

TenantId

The Client ID

ClientId

The Application UD URL (Or Resource ID)

ResourceId

Before we do that though, while we could reuse the existing API App Registration (CommandAPI_DEV) that we created for our “local” Command API, I think its good practice to set up a new “production” registration for our Command API.

Learning Opportunity

../images/501438_1_En_14_Chapter/501438_1_En_14_Figm_HTML.jpgRather than step through the exact same instructions to create a new “production” Command API registration, I’m going to leave you to do that now. As a suggestion, call this new app registration: CommandAPI_PROD.

Come back here when you’re done!

How did you go? Easy right? You should now have something similar to the following in your app registrations list.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig47_HTML.jpg
Figure 14-47

Production App Registrations

If like me you created your App Registrations in a different Azure Directory to your main one (i.e., where all your resources are), I’d take a note of all of the values for things like TenantId, ClientId, and ResourceId in the Production App Registration you just created before you switch back to your main AAD to add the new Application Settings for our API.

So, if needed, switch back to the AAD where you created the actual API App and Container instances.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig48_HTML.jpg
Figure 14-48

Switching active directories

Select your Command API service, then click “Configuration” to take you to the Application Settings screen.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig49_HTML.jpg
Figure 14-49

API app settings

Again, we’ve already added application settings before, so I’m going to leave it to you to add all the necessary application settings to allow our API to be correctly configured from an authentication perspective.

Warning!

../images/501438_1_En_14_Chapter/501438_1_En_14_Fign_HTML.jpgMake sure you give your application settings the exact same name as the User Secrets you set up before, with the relevant values from the Production API App registration (CommandAPI_PROD).

Here, you can see the new Application Settings I’ve added.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig50_HTML.jpg
Figure 14-50

Additional app settings to support authentication

Note

Remember to Save the new Application Settings you’ve just added.

Client Configurations

To ensure our client can authenticate to our Production API, we should:
  1. 1.

    Create a Production Client App Registration on Azure.

     
  2. 2.

    Update the necessary local settings in our Client App’s applicationsettings.json file.

     
Learning Opportunity

../images/501438_1_En_14_Chapter/501438_1_En_14_Figo_HTML.jpgYou have learned everything you need to know in order to complete this work, so again I’m going to leave it to you complete the two steps mentioned.

Take your time, and remember to copy down the new values that are generated as part of the new production client app registration.

When done, come back here.

Deploy Our API to Azure

Back in our Command API Solution, we just want to kick off a deploy to Azure, so if you don’t have any pending commits, make an arbitrary change to your code (insert a comment somewhere), and add/commit and push.

As before, our Build Pipeline should succeed as should our deployment. Using something like Postman to call an unsecured endpoint should still work as before.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig51_HTML.jpg
Figure 14-51

Unsecured endpoint continues to work

However, as expected when we attempt to call the secured endpoint (without a token), we should get a 401 Unauthorised response.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig52_HTML.jpg
Figure 14-52

Secured endpoint declines the request

Turning to our client app (with updated configuration to access Production), making a call-through to our secured endpoint will yield a successful result.
../images/501438_1_En_14_Chapter/501438_1_En_14_Fig53_HTML.jpg
Figure 14-53

Successful call of our secure endpoint on Azure

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.79.60