Friday, December 24, 2021

ASP.NET Core WebAPI secured using OAuth2 Client Credentials

Authentication means verifying the user who is accessing the system. We have available different types of authentication in .NET programming like Windows Authentication, Forms Authentication, Claim Based Authentication, Token-Based Authentication, etc.

 

Token-based Authentication

 
In token-based authentication, you pass your credentials [user name and password], to the server, which verifies your credentials and if it is a valid user, then it will return a signed token to the client system, which has an expiration time. The client can store this token locally using any mechanism like local storage, session storage, etc. and if the client makes any other call to the server for data, then it does not need to pass its credentials every time. The client can directly pass the token to the server, which will be validated by the server and if the token is valid, then you will able to access your data.
 
OAuth
 
(Open Authorization) is an open standard for token-based authentication and authorization on the Internet.
 
OAuth versions
 
There are two versions of OAuth authorization OAuth 1 (using HMAC-SHA signature strings) and OAuth 2 (using tokens over HTTPS).
 

OAuth Tokens

 
There are two types of tokens involved in OAuth 2,
 
Access Token
 
The access token is used to for authentication and authorization to get access to the resources from the resource server.
 
Refresh Token
 
The refresh token normally is sent together with the access token.
 
The refresh token is used to get a new access token when the old one expires. Instead of the normal grant type, the client provides the refresh token and receives a new access token.
 
Token Types
 
Access tokens have a type, which defines how they are constructed.
  • Bearer Tokens
    The bearer tokens use HTTPS security, and the request is not signed or encrypted. Possession of the bearer token is considered authentication.

  • MAC Tokens
    More secure than bearer tokens, MAC tokens are similar to signatures, in that they provide a way to have (partial) cryptographic verification of the request.

which shows how to create an Authorization Server that can issue Tokens following the OAuth2 Authorization framework.

It  is possible, anyway, to use ANY Authorization server to obtain the token and validate it without substantial changes in the secured Web API. This extension is, at the moment, not covered in the article.

Step-by-step creation of the secured WebAPI service

Creation of the empty WebAPI project

For this WebAPI we are going to use Visual Studio 2017, together with .NET Core 1.1.
We start opening VS2017 and selecting File -> New -> New Project. We select then a .NET Core project as in the following:



Give to the project the name you prefer. I'm using here ProtectedWebAPI.

After pressing OK, in the next screen be sure that you are using .NET Core 1.1. Select the template WebAPI and press OK again.


Add an open (non-secured) welcome page to your WebAPI service

This is not a necessary step. But personally I like when I press "Play" in the debugger and something happens, it gives me the feeling that my code is working.

For this reason in this section I'm adding a "non secured" welcome page to our WebAPI. For this page we won't need any token and it will be the default page when accessing our WebAPI service using a browser.

In order to do so, I need to add a controller, a view, and the MVC libraries. I will detail the necessary steps, for those that are not familiar with the ASP.NET MVC framework.

Add a MVC controller

  • In the folder Controllers add a new controller named HomeController.
  • Adding the new controller, VS will ask what dependencies to add to the project. You can choose Minimal Dependencies for now.
  • At the time I'm writing, after adding the dependencies, I need to add the controller again. I think it is a little bug of the Visual Studio UI....
  • Now, adding the controller, VS asks which scaffold to use. Choose MVC Controller - Empty.
  • Finally, choose the name HomeController.  A basic controller that can manage an Index.cshtml page is now created.

Add a MVC View

  • Again, in VS2017, right click on the project and select Add new folder.
  • Name it Views (don't change it! It's a default option for Asp.net MVC!).
  • Now, inside this Views folder, add another folder named Home.
  • Right click now on the folder Home and select Add -> New item ->  MVC View Page (ASP.NET Core).
  • By default the name of the view is Index.cshtml which is what we want.  Press the button Add to add the new view.

Write the HTML content of the View

Change the code in the Index.cshtml with the code below, just to create a welcome message:

HTML
<html>
<head>
<title>Protected WebAPI. Welcome page.</title>
</head>
<body>
<h1>Protected WebAPI. Welcome page.</h1> 
</body> 
</html>

Configure the Startup class

Setup the propert route in the Startup.cs to be able to browse the website:

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

Now browse the file in ProtectedWebAPI -> Properties -> LaunchSettings.json and remove the line containing the property launchUrl from every section visible in the file.  

JavaScript
"profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/values",  //REMOVE THIS ONE
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "ProtectedWebAPI": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/values",  //REMOVE THIS ONE
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:56087"
    }

Without this property, Visual Studio will launch the home page following the default path, and will point automatically to the web page that we just created.

Launch the project to check that it works...

...and to enjoy the achievement of a first step!

Warning: some people reported some issues in VS2017 related to launching a WebAPI project using IISExpress. In case you are one of those, you can change the profile next to the button "Start project" switching from IISExpress to ProtectedWebAPI as shown in the following picture:

Image 3

Secure the WebAPI endpoint

Now that we have our project all set up and we also have a nice welcome page, we can create an endpoint and then secure it, so that it won't accept any request if a proper OAuth2 token is missing! 

First, create an open endpoint that returns some values

This is an easy step to achieve, the VS WebAPI template has already created a basic open endpoint for us! You can browse the project to  ProtectedWebAPI -> Controllers -> ValuesController.  Here the code that you will find:

C#
namespace ProtectedWebAPI.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}
</string>

This code provides an endpoint that

  • answer at the address http://localhost:56087/api/values (ok, the port actually depends from the applicationUrl property in LaunchSettings.json)
  • can be browsed with a browser and returns a list of string
  • can accept other requests, using different HTTP Verbs (POST, PUT, DELETE) but these are out of the scope of this article for now.

If we try to browse the address http://localhost:56087/api/values we will see that (depending on the browser) we can receive back a file called values.json containing a list of values.

This means that your endpoint is currently answering to every anonymous request. It's an open endpoint. 

Of course we can change the type of values that we are providing back to the user request, but this is not the point of this article. Our sample endpoint returns a list of strings, but it could be anything.

Add the Middleware needed to manage the validation of the token

Using the Nuget package manager (or whatever you like), add the packages

  • IdentityServer4.AccessTokenValidation 

to the project:



Now include the configuration call to UseIdentityServerAuthentication(..) needed in the Configure() method, in the Startup.cs file.
Remember to always ensure that the code app.UseMvc(...) is the last line in the Configure method:

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            //more code....

            //add this configuration for the middleware needed to validate the tokens
            app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
            {
                Authority = "http://localhost:50151",
                RequireHttpsMetadata = false,
                ApiName = "scope.readaccess"
            });

            //more code....and especially app.UseMvc(...)
        }   

Secure this endpoint to not accept  anonymous requests

Before securing the endpoint in this example, it is useful to configure our WebApi to show the error code when something goes wrong. It will help us to better understand the example.

Add the instruction app.UseStatusCodePages() in the Startup.Cs:

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    //more code...

    //add this line to show the ERROR STATUS CODES
    app.UseStatusCodePages();

    //more code...
}

And now add the [Authorize] attribute to the class ValuesController.cs
This is the key step to activate the security features embedded in ASP.NET MVC.

C#
namespace ProtectedWebAPI.Controllers
{
    [Route("api/[controller]")]
    [Authorize] // THIS IS THE KEY ATTRIBUTE TO ACTIVATE SECURITY FEATURES IN OUR WEBAPI
    public class ValuesController : Controller
    {
       //some code that I cut in this snippet
    }
}

If you want to know more, I found very useful this article that explaines clearly the basics of the JWT Authentication Middleware in .NET Core. 

Add a basic Scope validation in our secured WebAPI

In the previous article, when we created the Authorization server, we also created two different scopes: "scope.readaccess" and "scope.fullaccess". Now, in this article, while we are accessing our Protected API we are passing the scope "scope.readaccess". Validating the right scope is beyond this article. There are many techniques to inspect the token, analyze the scope and react properly in the API we are protecting.

As usual the guys from the IdentityServer project gave us already some good hints on how to do that. In this article, Dominick Bayer provides various techniques.

In this article, however, just for completeness, I'm providing one possible way to check the scope. The code snippet is the one that you will put  in the ValuesController class, inside the public IEnumerable<string> Get() method. The method will look like this:

 // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            //this is a basic code snippet to validate the scope inside the API
            bool userHasRightScope = User.HasClaim("scope", "scope.readaccess");
            if (userHasRightScope == false)
            {
                throw new Exception("Invalid scope");
            }
            return new string[] { "value1", "value2" };
        }

Test the secure endpoint

The test is really straightforward. Launch again your WebAPI project: the welcome page shows up.
But now try again to access the link http://localhost:56087/api/values. Before we were receiving back a JSON file containing the values  returned from the API Call.....but now the answer is a clear:



Our WebAPI is now secured. It can't be anonymously accessed anymore.

Create a client that can access our WebApi

Now that we have our secured WebAPI, how do we access it? In this section we are going to create a client that can

  • request an access token from an OAuth2 Authorization Server;
  • call our WebAPI endpoint using the token and being authorized;

Put together the Authorization Server credentials

Before we start writing our client, we need to know which authorization server are we going to use. In this article I'm creating a Web API secured against the authorization server that I built in a previous article.

Therefore I'm going to use the credentials parameters that were configured in that article for this specific client. These credentials are:

  • Urlhttp://localhost:50151/connect/token (The Authorization Server runs locally on my same machine answering at this specific port)
  • ClientIdClientIdThatCanOnlyRead
  • ClientSecretsecret1
  • Scopescope.readaccess

Keep in mind these parameters. You will find them hardcoded in the Client code, and now you know where they are coming from.

Add a Console project

As a test client we don't need anything special in this tutorial. Therefore I'm just adding a Console client which will request the token and then access our WebAPI showing the results in the console window.

So, let's take our solution containing the WebAPI project and add, next to it a new File-> Add ->New project-> Console App (.NET Core). Choose the name ConsoleTestClient and press OK to add it.



Add the IdentityModel Nuget packages

To allow our client to handle with the request and make our life easier, add the following packages to the Console project:

  • System.IdentityModel.Tokens.Jwt

When adding this package, a few others will be pulled in from Visual Studio. Accept everything as they are needed references.

Request the JWT Token

Now it's time to write some code. My console client is just a Program.cs file. The first part of the code makes a request to the Authorization Server previously built in this article. In this example I'm assuming that this server is running locally on the same machine!

Here the snippet of Program.cs to request the token. Don't forget to add the using that will be suggested from Visual Studio:

C#
        static void Main(string[] args)
        {
            //authorization server parameters owned from the client
            //this values are issued from the authorization server to the client through a separate process (registration, etc...)
            Uri authorizationServerTokenIssuerUri = new Uri("http://localhost:50151/connect/token");
            string clientId = "ClientIdThatCanOnlyRead";    
            string clientSecret = "secret1";
            string scope = "scope.readaccess";

            //access token request
            string rawJwtToken = RequestTokenToAuthorizationServer(
                 authorizationServerTokenIssuerUri,
                 clientId, 
                 scope, 
                 clientSecret)
                .GetAwaiter()
                .GetResult();

            //...some more code
        }

The important part is in the method RequestTokenToAuthorizationServer. As you can see, it contains a simple POST request submitting the client credentials assigned to us from the AuthorizationServer:

C#
        private static async Task<string> RequestTokenToAuthorizationServer(Uri uriAuthorizationServer, string clientId, string scope, string clientSecret)
        {
            HttpResponseMessage responseMessage;
            using (HttpClient client = new HttpClient())
            {
                HttpRequestMessage tokenRequest = new HttpRequestMessage(HttpMethod.Post, uriAuthorizationServer);
                HttpContent httpContent = new FormUrlEncodedContent(
                    new[]
                    {
                    new KeyValuePair<string, string>("grant_type", "client_credentials"),
                    new KeyValuePair<string, string>("client_id", clientId),
                    new KeyValuePair<string, string>("scope", scope),
                    new KeyValuePair<string, string>("client_secret", clientSecret)
                    });
                tokenRequest.Content = httpContent;
                responseMessage = await client.SendAsync(tokenRequest);
            }
            return await responseMessage.Content.ReadAsStringAsync();
        }
About the specified ClientId, ClientSecret and Scope

The ClientId, ClientSecret and Scope are normally known to the caller. You, as a user of the API, are supposed to know your ClientId, ClientSecret and which scope is associated to these credentials. If you try to get a token for a different scope than the one connected to these credentials, you will get an error as this is exactly what the Authorization Server is supposed to check before issuing the token, to avoid giving you a wrong authorization.

Call the secured WebAPI providing the JWT token

Now that we have the token, we can use it to perform a request to the WebAPI. Here the easy piece of code that does the work. This method is also called in the Program.Cs, in the main file:

C#
 private static async Task<string> RequestValuesToSecuredWebApi(AuthorizationServerAnswer authorizationServerToken)
        {
            HttpResponseMessage responseMessage;
            using (HttpClient httpClient = new HttpClient())
            {
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authorizationServerToken.access_token);
                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:56087/api/values");
                responseMessage = await httpClient.SendAsync(request);
            }

            return await responseMessage.Content.ReadAsStringAsync();
        }

The only important difference with a generic HttpRequest is that we added an AuthorizationHeader. This authorization header carries the token and is inspected from the authorization middleware (in our case IdentityServer4) configured in the Web API server. 

Run everything together

We are almost at the end. It's time to run all these pieces together. To make the example working you will have to download and launch also the Authorization Server that I built in this article.

If you have everything ready, here what to run in which order. While you are getting familiar with the various pieces, it could be easier if you launch every project from a separate instance of VS2017:

  • Launch the Authorization Server first.
  • Launch then the ProtectedWebAPI Server.
  • Finally launch the ConsoleTestClient. Check in the console output that you read the answer from the WebApi server.

Enjoy the result of all this hard work.

Conclusions

 This article is meant to be just an introductory tutorial to the client credentials authentication following the OAuth2 framework.  We touched many different aspects: HttpRequest/Responses and WebAPIs in .NET Core, the Identity Server middleware, JWT Tokens, etc.... Much more can be done, configured, explored.

Once again, my goal is just to share with other readers my understanding about these concepts and some possible approaches for an implementation, not necessarily the best one. As usual, any comment or feedback or question will be very much appreciated, in the spirit of a community where we can all learn from each other. 

Code on GITHub

As usual, attached to this article there is a downloadable version of the code, but if for some reason you prefer github, here is the public repository.

No comments:

Popular Posts