# Token-based authentication and authorization (JWT Bearer) with ASP.NET Core

Español | English

Token-based authentication is an HTTP authentication scheme in which security relies on the use of encrypted text strings, usually generated by the server, which identify the bearer of the message by including these strings (token) in all resource requests made to the server.

# JSON Web Tokens (JWT)

It is a standard that defines how to compactly and securely transmit information between parties using a JSON object and is typically used in user access authorization scenarios and also as a means of securely transmitting information between parties.

Structurally it is a string encoded in base64 and formed by three blocks separated by a dot (.):

  • Header: Used to identify the type of token and the signature algorithm.
  • Payload: It is the block that contains the claims (key-value) related to an entity (usually the user), and which we can differentiate between registered (predefined and recommended by the standard), public (defined at will and without restrictions although with recommendations) and private (completely customized to share information agreed between the parties in a specific way).
  • Signature: Includes a secret key to validate the token.

# About authentication

In order to determine and control what a user (identity) can do in the system, we must have previously determined who the user is. This is called the authentication mechanism and in ASP.NET Core it is achieved through the authentication service used by the authentication middleware.

The set of authentication handlers and their configurations is what we call authentication schemas. Authentication schemes are responsible for defining the behaviors of the authentication process by which to authenticate users and recover their identity.

For each scheme we want to use, we must register the corresponding authentication services from Program.cs after calling AddAuthentication:

builder.Services.AddAuthentication()
  .AddJwtBearer()
  .AddJwtBearer("OtherAuthServer");

Example configuration based on the following configuration:

{
  "Authentication": {
    "DefaultScheme":  "OtherAuthServer",
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "customer:a",
          "customer:b"
        ],
        "ValidIssuer": "https://localhost:7202"
      },
      "OtherAuthServer": {
        "ValidAudiences": [
          "customer:c"
        ],
        "ValidIssuer": "https://localhost:4447"
      }
    }
  }
}

Bearer is the name of the default scheme when we register a service based on JWT Bearer (.AddJwtBearer()), but we can see how to add others like the one I have called OtherAuthServer and even set it as default using the DefaultScheme property.

Next we will have to add the authentication middleware from Program.cs and it will be used to use the previously registered schemes:

  app.UseAuthentication();

The user authentication action is performed by the authentication handler, which implements the necessary behavior according to the schema and is responsible for constructing and returning the authentication result, as well as the necessary user identity objects if the authentication is successful. In addition to authentication, the authentication handler also provides methods to know the authentication mechanism when trying to access a resource (challenge) and methods to know if a user is authenticated and allowed to access the requested resource (forbid).

In addition, there are scenarios where a remote authentication step is necessary, such as OAuth 2.0 (opens new window) and OIDC (opens new window), in which case the remote provider is responsible for authentication. This is the case for example of the use of Azure AD, Auth0, Identity Server, Okta, Facebook, Twitter, Google, Microsoft among others.

If you want to know more about Oauth 2.0 and OIDC, I recommend you to take a look at this other article I created about Authorization flows with OAuth 2.0 and OpenID Connect.

It is worth noting the importance of the ClaimsPrincipal class in ASP.NET Core as it is used to represent a security entity on which we will make decisions regarding permissions. In an HTTP request for example, it is the class from which derives the user that we can find in the HttpContext class:

ClaimsPrincipal principal = HttpContext.Current.User as ClaimsPrincipal;
if (null != principal)
{
  foreach (Claim claim in principal.Claims)
  {
      Response.Write("CLAIM TYPE: " + claim.Type + "; CLAIM VALUE: " + claim.Value + "</br>");
  }
}

Overview of ASP.NET Core authentication at learn.microsoft.com. (opens new window)

# Types of authorization

Once the user has been identified, .NET offers multiple ways to validate its permissions, among which we can highlight:

# Role-based authorization

When an entity is created, it can belong to one or more roles, which are used to validate the user's access to services. This is the classic example of belonging to the Administrator and User roles.

Registration of role-based authorization services from Program.cs and that will take care of using the previously registered schemes:

builder.Services.AddDefaultIdentity<IdentityUser>( ... )
    .AddRoles<IdentityRole>()

Role-based access checks:

[Authorize(Roles = "Administrator, User")]
public class FileManagerController : Controller
{
    public IActionResult Read() =>
        Content("Administrator or User");

    [Authorize(Roles = "Administrator")]
    public IActionResult Delete() =>
        Content("Administrator only");
}

Registration of role-based authorization services using the directive syntax from Program.cs and that it will be removed from using the previously registered schemes:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("IsAdministratorRole",
        policy => policy.RequireRole("Administrator"));
});

Role-based access checks:

[Authorize(Policy = "IsAdministratorRole")]
public IActionResult Delete() =>
        Content("Administrator only");

Official documentation at learn.microsoft.com. (opens new window)

# Claims-based authorization

When an entity is created, new claims (key-value) issued by a trusted entity are added to it. In this case, to validate user access to services, validation is performed by checking the presence of a claim and optionally its value.

Registration of authorization services based on claims from Program.cs and that will take care of using the previously registered schemes:

builder.Services.AddAuthorization(options =>
{
  options.AddPolicy("IdentifiedUser", policy =>
  {
    policy.RequireClaim("UserId");
  });

  options.AddPolicy("HasAPIScope", policy =>
  {
      policy.RequireAuthenticatedUser();
      policy.RequireClaim("scope", "api");
  });
});
//...
app.UseAuthorization();

Claim-based access checks:

[Authorize(Policy = "IdentifiedUser")]
public class UserDataController : Controller
{
    public IActionResult Profile() =>
        Content("IdentifiedUser only");

    [AllowAnonymous]
    public IActionResult Index() =>
        Content("Any");
}

[Authorize(Policy = "HasAPIScope")]
public class SomeAPIController : Controller
{
    public IActionResult DoSomething() =>
        Content("HasAPIScope only");
}

Official documentation at learn.microsoft.com. (opens new window)

# Policy-based authorization

In this case, validation is performed by checking the requirements registered during the configuration of the authorization service. Given its flexibility, in addition to being used internally by role-based authorization and claims-based authorization (from preconfigured settings), it also gives them more flexibility by allowing the registration of custom policies for such scenarios.

Registration of policy-based authorization services from Program.cs and that will take care of using the previously registered schemes:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("IsRestrictedIP", policy =>
        policy.Requirements.Add(new IPAddressRequirement(new List<string>(){"224.0.0.0", "224.0.0.1"})));
});

IPRequirement is a class we have created and implements the IAuthorizationRequirement interface and is used as a parameter in the creation of the policy requirements.

using Microsoft.AspNetCore.Authorization;
public class IPAddressRequirement : IAuthorizationRequirement
{
    public IPAddressRequirement(List<string> ips) =>
        Ips = ips;

    public List<string> Ips { get; }
}

In addition to the requirement, it is also necessary to define the handler responsible for evaluating its properties. To do this we will create the handler by implementing the AuthorizationHandler<TRequirement> interface, where TRequirement is the requirement to be handled.

public class IPAddressHandler : AuthorizationHandler<IPAddressRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IPAddressRequirement requirement)
    {
        // Requirement validation based on context and requirement.Ips
        var isAuthorizedIP = {...};

        if (isAuthorizedIP)
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

And finally, let's not forget the need to register our new controller in the service collection so that it is available throughout our application:

builder.Services.AddSingleton<IAuthorizationHandler, IPAddressHandler>();

Policy-based access checks:

[AllowAnonymous]
public class UserDataController : Controller
{
    [Authorize(Policy = "IsRestrictedIP")]
    public IActionResult Detail() =>
        Content("IsRestrictedIP only");

    public IActionResult Index() =>
        Content("Any");
}

Official documentation at learn.microsoft.com. (opens new window)

# Resource-based authorization

This approach allows to apply a completely customized authorization method and is used when the authorization process depends on the resource in question, which implies that the evaluation must be performed just before the requested operation, so we must invoke a customized authorization method.

First of all we must know that thanks to dependency injection, the authorization service is available from the controllers:

public class MediaServerController : Controller
{
    private readonly IAuthorizationService authorizationService;
    private readonly IMediaServerRepository mediaServerRepository;

    public DocumentController(IAuthorizationService authorizationService,
                              IMediaServerRepository mediaServerRepository)
    {
        this.authorizationService = authorizationService;
        this.mediaServerRepository = mediaServerRepository;
    }
    //...
}

We will use the authorization service to perform a custom authorization validation during resource recovery.

public class MediaServerController : Controller
{
  //...
  public async Task<IActionResult> OnGetConfigurationAsync(Guid mediaServerId)
  {
      MediaServer mediaServer = mediaServerRepository.Find(mediaServerId);

      if (mediaServer == null)
      {
          return new NotFoundResult();
      }

      var authorizationResult = await authorizationService
              .AuthorizeAsync(User, mediaServer, "GetConfigurationPolicy");

      if (authorizationResult.Succeeded)
      {
          return Page();
      }
      else if (User.Identity.IsAuthenticated)
      {
          return new ForbidResult();
      }
      else
      {
          return new ChallengeResult();
      }
  }
}

Definition of the requirement and handler responsible for evaluating its properties.

public class SameCountryRequirement : IAuthorizationRequirement { }

public class MediaServerAuthorizationHandler :
    AuthorizationHandler<SameCountryRequirement, MediaServer>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
      SameCountryRequirement requirement,
      MediaServer resource)
    {
        if (context.User.Claims.FirstOrDefault(c => c.Type == "CountryId") == resource.CountryId)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

And finally the registration of the requirement and the handler in Program.cs:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("GetConfigurationPolicy", policy =>
        policy.Requirements.Add(new SameCountryRequirement()));
});

builder.Services.AddSingleton<IAuthorizationHandler, MediaServerAuthorizationHandler>();

Official documentation at learn.microsoft.com. (opens new window)