In this article, let’s dig into securing an API by means of Authorizing the incoming requests based on an access token. And also let’s see how we can write our own token generation providers using the asp.net core library.

Setting up the Context:

Let’s assume we have a Readers WebAPI which holds the details about all the Users present in a system. And we have an API Endpoint which returns all the Readers present in the system. Now such an API when distributed can’t be accessed by everyone, and so we are looking in ways to secure this API so that only authorized users can be allowed to fetch the data. In our scenario, we are looking at Authenticating users and then issuing them a key to access the resource. Hence we go with JWT Bearer authentication mechanism, in which on successful authentication of users we issue access tokens for a shorter period of time via JWT. It can be understood as “give access to the bearer of the passed token”. And in our case, we would like to use our own authentication mechanism, while we can have the provision to use any third party Token Providers such as Azure Ad etc.

The Validating Attributes:

A basic JWT token should consist of an Audience, Issuer, an Expiration Time, a SecretKey and Claims. Let’s look at each of them:

Audience: The recepient of this token or the receiver for whom the token was generated. When accessed an API via a token, we look at the audience attribute present in the token and validate against our set of valid audience so as to decide whether the owner of this token is allowed or not.

Issuer: The producer or the owner of the token who has issued this token for use. When accessed the API would look at the issuer present in the token and validates against the valid issuer from which the token was intended to be made.

Expiry: One of the most important features of tokens are their short life-spans. An access token must be as short as possible so that in case if a token is stolen, it would become unusable after a short period of time. The API would check if a passed token has already expired or still is alive.

SecretKey: While signing a JWT key before issuing, we encrypt the token using a secret key which is only known to the issuer token server. When a token is received in a request, we use the same signing key against the same encrypting algorithm to decrypt the token for its integrity. This makes the token sealed to any tampering.

Claims: This is an optional part, and we pass-in any required information about the user for whom the token represents. This can be helpful for any second level of validation, or for any business logic. In general, the access tokens avoid possessing sensitive user information, apart from non-application-specific information such as email, userid etc.

Once a token passes all of the validating parameters above, it is said to be authorized and is good to access.

The Reader Example:

Let’s look at the ReaderController code, which has two API endpoints, one which returns all the users and another endpoint we added which authenticates a user for his credentials and issues access token for use.

[Route("api/[controller]")]
[ApiController]
public class ReaderController : ControllerBase
{
    IReaderRepo _repo;
    
    public ReaderController(IReaderRepo repo)
    {
        _repo = repo;
    }

    [Authorize]
    [Route("all")]
    [HttpGet]
    public List<User> Get()
    {
        return _repo.Users;
    }

    [Route("token")]
    [HttpPost]
    public AuthResult Token([FromBody]LoginModel credentials)
    {
        return _repo.Authenticate(credentials);
    }
}

We have encapsulated the business logic for Reader in an abstraction IReaderRepo which is implemented by ReaderRepo as follows:

public interface IReaderRepo
{
    List<User> Users { get; }
    AuthResult Authenticate(LoginModel credentials);
}

public class ReaderRepo : IReaderRepo
{
    private static List<User> users;
    private ITokenManager _tokenManager;

    public ReaderRepo(ITokenManager tokenManager)
    {
        _tokenManager = tokenManager;
        SeedUsers();
    }

    private void SeedUsers()
    {
        users = new List<User>() {
            new User {
                Id = 1,
                Name = "Reader",
                Email = "reader1@me.com"
            }
        };
    }

    public List<User> Users => users;
    
    public AuthResult Authenticate(LoginModel credentials)
    {
        var user = users.FirstOrDefault(x => x.Email == credentials.Email);

        if (user != null)
        {
            return new AuthResult
            {
                IsSuccess = true,
                Token = _tokenManager.Generate(user)
            };
        }

        return new AuthResult { IsSuccess = false };
    }
}

For simplicity sake, we use a static user store (a list of hardcoded users) to authenticate incoming user credentials. And the Authenticate() method checks for matching user for the credentials passed and then issue a Token by calling the TokenManager.Generate() method within. The method also takes the fetched user object as parameter to pass suitable information of the user as claims.

The Token Manager Class:

Its all simple till here, now we look at how we can create a jwt bearer token for use. We use the namespace System.IdentityModel.Tokens.Jwt for our purpose, which is a library of Jwt based token generation methods.

public interface ITokenManager
{
    AuthToken Generate(User user);
}

public class TokenManager : ITokenManager
{
    public AuthToken Generate(User user)
    {
        List<Claim> claims = new List<Claim>() {
            new Claim (JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim (JwtRegisteredClaimNames.Email, user.Email),
            new Claim (JwtRegisteredClaimNames.Sub, user.Id.ToString())
        };

        JwtSecurityToken token = new TokenBuilder()
        .AddAudience(TokenConstants.Audience)
        .AddIssuer(TokenConstants.Issuer)
        .AddExpiry(TokenConstants.ExpiryInMinutes)
        .AddKey(TokenConstants.key)
        .AddClaims(claims)
        .Build();

        string accessToken = new JwtSecurityTokenHandler().WriteToken(token);

        return new AuthToken() {
            AccessToken = accessToken,
            ExpiresIn = TokenConstants.ExpiryInMinutes
        };
    }
}

The above code uses an object of type TokenBuilder which is then chained in series of methods which add Audience, Issuer, Expiry, Key and Claims to the token. Finally we generate the token by creating an instance of JwtSecurityTokenHandler class and invoking WriteToken() method with the created SecurityToken instance. The Tokens are handled by a class as shown below:

public class TokenConstants
{
    public static string Issuer = "thisismeyouknow";
    public static string Audience = "thisismeyouknow";
    public static int ExpiryInMinutes = 10;
    public static string key = "thiskeyisverylargetobreak";
}

In reality, we have these constants placed in the appsettings and read them by using the IConfiguration instance. (link to Typed class).

The TokenBuilder class used here is developed as shown below:

public class TokenBuilder
{
    private string _issuer;
    private string _audience;
    private DateTime _expires;
    private SigningCredentials _credentials;
    private SymmetricSecurityKey _key;
    private List<Claim> _claims;

    public TokenBuilder AddClaims(List<Claim> claims)
    {
        if (_claims == null)
            _claims = claims;
        else
            _claims.AddRange(claims);

        return this;
    }

    public TokenBuilder AddClaim(Claim claim)
    {
        if (_claims == null)
            _claims = new List<Claim>() { claim };
        else
            _claims.Add(claim);

        return this;
    }

    public TokenBuilder AddIssuer(string issuer)
    {
        _issuer = issuer;
        return this;
    }

    public TokenBuilder AddAudience(string audience)
    {
        _audience = audience;
        return this;
    }

    public TokenBuilder AddExpiry(int minutes)
    {
        _expires = DateTime.Now.AddMinutes(minutes);
        return this;
    }

    public TokenBuilder AddKey(string key)
    {
        _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
        _credentials = new SigningCredentials(_key, 
        SecurityAlgorithms.HmacSha256);
        return this;
    }

    public JwtSecurityToken Build()
    {
        return new JwtSecurityToken(
            issuer: _issuer,
            audience: _audience,
            claims: _claims,
            expires: _expires,
            signingCredentials: _credentials
        );
    }
}

This is a custom Utility class which uses method chaining to create an instance of JwtSecurityToken returned when Build() method is called. When all of these components are grouped together we get an API which issues tokens on successful authentication of user credentials.

The Authorization:

Now that we have obtained a token, we need to still have a mechanism to validate the passed bearer token inorder to decide whether to allow the request to pass through or be blocked from access. For that purpose we use the Authorization Middlewares provided for various types of authorization schemes in dotnet core. We have JwtBearer(), OpenIdConnect(), Google(), Facebook(), Microsoft(), Twitter() and OAuth() among others for use. Ussing these we can implement user authentication and authorization. In our case, we go by JwtBearer() middleware since we are an API for which we authorize issued tokens.

Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. 
    // Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<ITokenManager, TokenManager>();
        services.AddSingleton<IReaderRepo, ReaderRepo>();
        
    // Custom extension method added that contains the actual logic
    services.AddBearerAuthentication();
        services.AddMvc();
    }

    // This method gets called by the runtime. 
    // Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseAuthentication();
        app.UseMvc();
    }
}

// The Extension class for ServiceCollections
static class AuthorizationExtension
{
// Extension method for Adding JwtBearer Middleware to the Pipeline
    public static IServiceCollection AddBearerAuthentication(
    this IServiceCollection services)
    {
        services.AddAuthentication(options =>
        {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters()
            {
                    ValidateAudience = true,
                    ValidateIssuer = true,
                    ValidateLifetime = true,
                    IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(TokenConstants.key)),
                    ValidIssuer = TokenConstants.Issuer,
                    ValidAudience = TokenConstants.Audience
            };

            options.Events = new JwtBearerEvents()
            {
                   OnAuthenticationFailed = (context) =>
                   {
                        Console.WriteLine(context.Exception);
                        return Task.CompletedTask;
                   },

                   OnMessageReceived = (context) =>
                   {
                         return Task.CompletedTask;
                   },

                   OnTokenValidated = (context) => 
                   {
                         return Task.CompletedTask;
                   }
              };
       });

        return services;
    }
}

In ConfigureService() method we add Authentication middleware to the pipeline and then attach JwtBearer() to specify what kind of authentication we’re using here. And we ask dotnet core to use the Authentication scheme we defined in the pipeline by adding the UseAuthentication() in Configure method. More about Startup class here

Observe the AddJwtBearer() method in this case, we specify what Audience, Issuer and Key we have used in generating token here so that the Authentication middleware validates the input token using the exact same attributes. And when something differs, the middleware returns a 401 UnAuthorized response. We have a provision to have any intermediatary logic when the Authentication middleware is invoked (OnMessageReceived), when the input token fails the validation (OnAuthenticationFailed) and when the token is successfully validated (OnTokenValidated). Once this is passed, the request goes to the Get() API and returns the data. We can force the request to go for authentication check by decorating the Endpoint with [Authorize] attribute. This kicks in the Authentication middleware and forces the request to be validated of its access token. (Also read: Middlewares in dotnet core)

A Sample request for this API can be as follows:

POST /api/reader/token HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{
	"email": "reader1@me.com"
}

Response:

{
    "isSuccess": true,
    "token": {
        "expiresIn": 10,
        "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxNGZhODkyMC00OGM3LTQ5NDctYjU0NC01Y2FlZTU4MzdhY2UiLCJlbWFpbCI6InJlYWRlcjFAbWUuY29tIiwic3ViIjoiMSIsImV4cCI6MTU3Mjk3Mjk4OSwiaXNzIjoidGhpc2lzbWV5b3Vrbm93IiwiYXVkIjoidGhpc2lzbWV5b3Vrbm93In0.SKQRYKG3sf5H8irXKI3xGg_pOKBexN1CNPXPT5winYQ"
    }
}

And using this Token to access the API:

GET /api/reader/all HTTP/1.1
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxNGZhODkyMC00OGM3LTQ5NDct
YjU0NC01Y2FlZTU4MzdhY2UiLCJlbWFpbCI6InJlYWRlcjFAbWUuY29tIiwic3ViIjoiMSIsImV4cCI6MTU3Mjk3Mjk
4OSwiaXNzIjoidGhpc2lzbWV5b3Vrbm93IiwiYXVkIjoidGhpc2lzbWV5b3Vrbm93In0.SKQRYKG3sf5H8irXKI3xG
g_pOKBexN1CNPXPT5winYQ

Response:

[
    {
        "id": 1,
        "name": "Reader",
        "email": "reader1@me.com"
    }
]

In this way, we can secure the APIs using JWT Bearer authentication.

via Referbruv

#asp.net #aspnet #aspnetcore #jwt

Securing Web APIs using JWT Bearer Authentication in ASP.NET Core
1 Likes78.10 GEEK