Showing posts with label WebApi. Show all posts
Showing posts with label WebApi. Show all posts

Thursday, October 10, 2024

JWT validation in Azure Web App

 JWT validation in Azure Web App

Why

First of all some background of why we are doing this. 
In a Sitecore JSS headless project we have an API which is hosted in a Web App. So no Azure Function (we have those as well) but for a number of reasons we choose a WebApi for this part. Some of these api's return sensitive data and need to be secured. And not just secured in a standard way (subscription keys, ...) but also on user level. For some requests you need to be an admin, others can only be requested for yourself and so on.

Also important to mention is that this is a B2B project where the customer decided that the login is done with an Entra App registration in order to provide SSO experience without them having to deal with granting or revoking access - except of course in the custom application itself. As a result, we have no claims in the tokens as at that moment the user is only authenticated but not yet authorized.

As we cannot fetch the role information about a user from the JWT, in our Azure API Management (apim) we can only verify if the token is valid but not the content. 

And so, we want to validate the content in the Web App itself. I found a lot of information on the subject, but nothing was really what I needed. But after a lot of trial and error and putting various docs together I finally have something that seems to work. 

As Sitecore is becoming more and more headless and distributed and custom api's are probably a part of that it seems like a good idea to share my info.

Goal

To summarize, our goal is to verify if the request came from a specific email - specific can be the same email as in the request data or the email of an admin user. Verification is done based on a JWT that will be created by MS Entra.


Code

Startup

Let's dive straight into it and start with a function that we add in our Program class:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;

void AddPolicies(WebApplicationBuilder builder)
{
  var tenant = builder.Configuration.GetValue<string>("TenantId");
  var client = builder.Configuration.GetValue<string>("ClientId");
  builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opt =>
    {
      opt.Authority = $"https://login.microsoftonline.com/{tenant}/v2.0"; //.well-known/openid-configuration"
      opt.TokenValidationParameters = new TokenValidationParameters
      {
        ValidateIssuerSigningKey = true,
        ValidIssuer = $"https://login.microsoftonline.com/{tenant}/v2.0",
        ValidateIssuer = true,
        ValidAudience = client,
        ValidateAudience = true,
        ValidateLifetime = true,
      };
    });

  builder.Services.AddAuthorization(options =>
  {
    options.AddPolicy("EmailPolicy", policy =>
    {
      policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
      policy.Requirements.Add(new EmailRequirement());
    });
    options.AddPolicy("AdminPolicy", policy =>
    {
      policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
      policy.Requirements.Add(new AdminRequirement());
    });
  });
}
We need a tenant and client id. With those secrets we can define the Authentication based on the JwtBearer schema and set the issuing authority along with the parameters that define what we want to validate.

Once we have the authentication in place, we can add the Authorization as that is what we are actually looking for.

For the authorization we can add a Policy. Or in our case two policies as we want to be able to verify the email and the admin status. We will add the code for those policies later.

In the main function we add a call to the AddPolicies and also mention that we want to use Authentication and Authorization.
AddPolicies(builder);

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

Policies

Creating the policies is no more than creating an empty class - as example the EmailRequirement:
using Microsoft.AspNetCore.Authorization;

public class EmailRequirement : IAuthorizationRequirement
{
}

Authorization Handler

The next step - and this is where the magic happens - is the authorization handler. You have a few options here as mentioned in the MS-docs article and we decided for one handler to handle all requirements.
using Microsoft.AspNetCore.Authorization;

public class PermissionHandler : IAuthorizationHandler
{
  ...

  public Task HandleAsync(AuthorizationHandlerContext context)
  {
    if (context.Resource is HttpContext httpContext)
    {
      var jwtMail = securityService.GetJwtMail(httpContext.Request);
      if (string.IsNullOrEmpty(jwtMail))
      {
        context.Fail();
        return Task.CompletedTask;
      }

      var pendingRequirements = context.PendingRequirements.ToList();
      foreach (var requirement in pendingRequirements)
      {
        if (requirement is EmailRequirement)
        {
          var email = GetEmail(httpContext.Request);
          if (!string.IsNullOrEmpty(email) && ...)
          {
            context.Succeed(requirement);
          }
          else
          {
            context.Fail();
          }
        }
        else if (requirement is AdminRequirement)
        {
          ...
        }
      }
    }

    return Task.CompletedTask;
  }

  private static string? GetEmail(HttpRequest request)
  {
    ...
  }
}

A PermissionHandler is a straight forward class that checks the requirements sets the context to success or failure and return the task completion. In our case we are first getting the email address from the JWT (details follow) and if we have an email we loop over the requirements. 

Those are pure business logic. As an example for the EmailRequirement we fetch the email from the request (how to do this is dependent on how that information is being send to your api's). If the email is valid (equal to the one in the JWT) we set the context to be succeeded - otherwise it failed.
We repeat this for every requirement needed.

Getting the email from the JWT

using System.IdentityModel.Tokens.Jwt;

public class SecurityService : ISecurityService
{
  ...

  internal static string? GetJwt(HttpRequest request)
  {
    try
    {
      var jwt = request.Headers.Authorization;
      if (jwt.Count == 1)
      {
        var jwtSplit = jwt.ToString().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
        return jwtSplit.Last();
      }
    }
    catch
    {
      return null;
    }

    return null;
  }

  public string? GetJwtMail(HttpRequest request)
  {
    try
    {
      var jwt = GetJwt(request);
      var handler = new JwtSecurityTokenHandler();
      var token = handler.ReadJwtToken(jwt);
      var claim = token.Claims.FirstOrDefault(c => c.Type.Equals("email", StringComparison.OrdinalIgnoreCase));
      if (claim != null)
      {
        return claim.Value;
      }
    }
    catch
    {
      return null;
    }

    return null;
  }
}

The JW token is in the Authorization header. We split that string as we don't want the "Bearer" part that precedes the token. We parse the token and get the email from the claims.

Don't forget to register Permissionhandler in your main class:
builder.Services.AddScoped<IAuthorizationHandler, PermissionHandler>();

That wraps up all the code that we need to write. Now we can start using the Authorization attribute on our api's. On all task in your controller you can add the attribute with the name of the requirement that is needed:
[Authorize(Policy = "EmailPolicy")]

And that's all folks. 

I had to get quite a few blogpost and documentation pages together to get this working and I learned that any change in the architecture or setup change the way you should do this, so I hope this explanation will be useful to someone. And if not completely what you need, it might get you in the right direction.

Thursday, June 2, 2016

Sitecore WebApi missing context

Sitecore & WebApi


A lot has already been written about Sitecore and WebApi the last years since your custom WebApi calls didn't work anymore without a little tweaking. We have used the solution by Patrick Delancy a few times now and it worked fine. Until today..
Well, the issue seemed to be in the WebApi but it turned out to be something else. My journey of the day:

WebApi call is missing all Sitecore context

Our starting point was indeed a web api request that had no Sitecore context. The request looked like this: 
"http://website/api/stores/nearby/50.860421/4.422365"

First thing to do was compare configs and code with other projects where it was working, but that didn't help. All was fine there.. 
When I tried to place a language in between the domain and the path (../en/api/...) I got a 404 error from IIS. Weird. Nothing from Sitecore, although this should work. So I had my first conclusion: Sitecore is rejecting the request. 

Inspect the httpRequestBegin pipeline

I started inspecting the httpRequestBegin pipeline and noticed that it was skipped in the first step, the CheckIgnoreFlag. A custom processor placed before this step got hit, the one right behind it didn't. So I had to continue my search in.. 

The preprocessRequest pipeline

The preprocessRequest performs several checks in order to determine whether the request is valid for Sitecore. After staring at it for a while my eye fell on the dot. A simple stupid ".". The web api action was expecting 2 doubles as parameters and as this all should work find, there is a filter in this Sitecore pipeline on extensions: the FilterUrlExtensions. And of course, Sitecore is thinking that our extension is 422365 :)

The fix

Fixing this seemed very simple: just a a trailing slash to the request. And there we had our context again!

So remember when using doubles in web api request: use a trailing slash if it is your last parameter...