Categories
javascript Uncategorized web

Clipping image edges at runtime with CSS clip-path

This is going to be a rare front-end post from me, but I thought I’d share a handy technique for making a web browser treat the non-transparent edge of an image as the actual edge, rather than using the bounding box as it does by default.

I spent some time this year building a collage editing tool for an artist friend of mine, who runs workshops where students create collages out of existing images; I created a digital app to help out in the time of COVID, which you can find at https://collageit.co.uk (it’s actually pretty fun to play with).

The thing is that, when editing a collage, images overlap with each other a lot, and users interact with images in a more physical manner than just with ‘layers’, as you might find in a lot of image editors; that’s the nature of collage art. So, why does the default browser behaviour make this so difficult to implement?

Let’s say I have this image below. The image has a transparent background. From a visual perspective, the edge of this image is the edge of the circle, right?

Just a red button.

But, as you may know, the browser (and mostly every other image rendering system) will treat the edge of the image as the bounding box.

The actual image box.

This means that any mouse-over events or other interactions will all be with the image’s bounding box edge. If I want things to happen only when a user clicks on the actual image, things get tricky.

CSS clip-path

I’m going to start off just showing the working solution, which I’ve set up in codepen. Try it out with some of your own images if you like.

Due to canvas security restrictions, any images you want to load into the codepen need an Access-Control-Allow-Origin header in order to work. Imgur is a pretty good source that works.

So, we can display an image, and have the browser interact with the ‘actual’ edge of the image, rather than the bounding box.

How does this work?

The actual clipping behaviour is done with a CSS clip-path property. This property allows you to define a custom polygon that clips both the actual displayed portion of an image, as well as the area where mouse interactions are detected.

/* A simple clip example */
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);

However, we can actually exploit this property to provide explicit x + y positions for the clip:

clip-path: polygon(55px 49px,55px 50px,54px 50px,54px 51px,53px 51px,53px 52px,52px 52px, ....);

There isn’t really a limit to the length of the clip-path, so we could actually define the entire edge of the image using this mechanism if we wanted.

This is how I’m clipping the image in my approach.

Marching Squares

You may have realised (especially if you load your own images in the codepen) that I haven’t actually pre-computed this path and loaded it in, or embedded it in my HTML ahead of time. This path is computed at runtime when we load the image!

We do this using something called the Marching Squares algorithm. Basically, it’s an algorithm that detects a contour (or edge) of something. In this case, I use it to detect the path around the image where the alpha component of the image transitions to opaque.

I’m using an implementation of Marching Squares by Mike Bostock, from the d3 graphics library. The license is included in the codepen source; if you use that code, please maintain the license!

So, what are the steps here?

  1. Load the image from whatever source.
let loadPromise = new Promise((resolve, reject) => {        
        const imageElement = new Image();
        imageElement.crossOrigin = "anonymous";
        imageElement.onload = ev => {
            resolve(imageElement);
        };
        imageElement.onerror = ev => {
            reject(`Could not load image from ${imageUrl}.`);
        }
        imageElement.src = imageUrl;
});

// Wait for the load to finish.
const loadedImage = await loadPromise;

  1. Resize an in-memory canvas and render the image into it.
// I've defined 'workingCanvas' elsewhere.
const canvasWidth = workingCanvas.width = loadedImage.width;
const canvasHeight = workingCanvas.height = loadedImage.height;

const drawContext = workingCanvas.getContext("2d");

// Wipe the canvas.
drawContext.clearRect(0, 0, workingCanvas.width, workingCanvas.height);

// Draw the image.
drawContext.drawImage(loadedImage, 0 ,0);

  1. Get the raw pixel data for the image so we can analyse it.
// Get the raw pixel data that we can analyse.
const pixelData = drawContext.getImageData(0, 0, workingCanvas.width, workingCanvas.height)
                             .data;
  1. Define a function that, for any given x,y coordinate, can assess whether the point is part of the image.
// This is used by the marching squares algorithm
// to determine the outline of the non-transparent
// pixels on the image.    
const defineNonTransparent = function (x, y)
{
    // Get the alpha value for a pixel.
    var a=pixelData[(y*canvasWidth+x)*4+3];
    
    // This determines the alpha tolerance; I'm extremely intolerant of transparency;
    // any transparency is basically counted as the edge of the image.
    return(a>99);
}
  1. Finally, get the edge points of the image.
// Get the edges.
var points = contour(defineNonTransparent);

From the edge points we can trivially construct a clip path to use in the css polygon construct.

function getClipPath(imgData) 
{
    let clipPathSet = [];

    imgData.boundingPoints.forEach(p => clipPathSet.push(`${p.x}px ${p.y}px`));

    return clipPathSet.join(',');
};

Limitations

There are a couple of limitations to this approach (which I can largely accept for my purposes):

  1. The algorithm can only detect one edge in an image; so if your image is made up of multiple disconnected parts, only one of them is going to be picked up and clipped.
  2. I can’t detect internal transparency with this algorithm right now, so if you have a blank space inside the image, that’ll be treated as part of the image rather than something you can click through.
  3. This isn’t going to work in old browsers (IE is out, basically).

It’s quite likely that there are better ways to do this, and I’d be interested to learn of better solutions if you have them!

Categories
c# Uncategorized

Detecting Init-Only Properties with Reflection in C# 9

This is a super-quick post to hopefully save someone the trouble of figuring out how to detect init-only properties (as I had to this morning).

Init-only properties are a new feature in C# 9, and let you define properties that can only be set in the constructor or in an object initialisation block:

public class MyObject
{
    public string InitOnlyProperty { get; init; }
}

static void Main(string[] args)
{
    var newInstance = new MyObject
    {
        InitOnlyProperty = "value"
    };

    // Compiler error!!
    newInstance.InitOnlyProperty = "something";
}

The init; used instead of set; marks the property as init-only.

If you want to check if any given property is init-only at runtime, it’s a little more complicated than just checking a single flag on PropertyInfo.

Looking at the generated IL for our code in dotPeek, we can see that the generated set method’s return value has an additional modreq option for System.Runtime.CompilerServices.IsExternalInit.

// .property instance string InitOnlyProperty()
public string InitOnlyProperty
{
    // .method public hidebysig specialname instance string
        // get_InitOnlyProperty() cil managed
    [CompilerGenerated] get
    // .maxstack 8
    // 
    // IL_0000: ldarg.0      // this
    // IL_0001: ldfld        string InitOnlyDetection.Program/MyObject::'<InitOnlyProperty>k__BackingField'
    // IL_0006: ret
    // 
    {
        return this.\u003CInitOnlyProperty\u003Ek__BackingField;
    }

    // .method public hidebysig specialname instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit)
        // set_InitOnlyProperty(
        // string 'value'
        // ) cil managed
    [CompilerGenerated] set
    // .maxstack 8
    // 
    // IL_0000: ldarg.0      // this
    // IL_0001: ldarg.1      // 'value'
    // IL_0002: stfld        string InitOnlyDetection.Program/MyObject::'<InitOnlyProperty>k__BackingField'
    // IL_0007: ret
    // 
    {
        this.\u003CInitOnlyProperty\u003Ek__BackingField = value;
    }
}

So, we can detect init-only properties with the following extension method:

using System.Linq;
using System.Reflection;

public static class PropertyExtensions
{
    /// <summary>
    /// Determines if this property is marked as init-only.
    /// </summary>
    /// <param name="property">The property.</param>
    /// <returns>True if the property is init-only, false otherwise.</returns>
    public static bool IsInitOnly(this PropertyInfo property)
    {
        if (!property.CanWrite)
        {
            return false;
        }

        var setMethod = property.SetMethod;

        // Get the modifiers applied to the return parameter.
        var setMethodReturnParameterModifiers = setMethod.ReturnParameter.GetRequiredCustomModifiers();

        // Init-only properties are marked with the IsExternalInit type.
        return setMethodReturnParameterModifiers.Contains(typeof(System.Runtime.CompilerServices.IsExternalInit));
    }
}

There you go, told you it would be super quick!

Categories
c# Uncategorized

Making Users Re-Enter their Password: ASP.NET Core & IdentityServer4

It’s often good security behaviour to require users to re-enter their password when they want to change some secure property of their account, like generate personal access tokens, or change their Multi-factor Authentication (MFA) settings.

You may have seen the Github ‘sudo’ mode, which asks you to re-enter your password when you try to change something sensitive.

Sudo Mode Dialog
The Github sudo mode prompt.

Most of the time a user’s session is long-lived, so when they want to do something sensitive, it’s best to check they still are who they say.

I’ve been working on the implementation of IdentityServer4 at Enclave for the past week or so, and had this requirement to require password confirmation before users can modify their MFA settings.

I thought I’d write up how I did this for posterity, because it took a little figuring out.

The Layout

In our application, we have two components, both running on ASP.NET Core 3.1

  • The Accounts app that holds all the user data; this is where Identity Server runs; we use ASP.NET Core Identity to do the actual user management.
  • The Portal app that holds the UI. This is a straightforward MVC app right now, no JS or SPA to worry about.

To make changes to a user’s account settings, the Profile Controller in the Portal app makes API calls to the Accounts app.

The Portal calls APIs in the Accounts app

All the API calls to the Accounts app are already secured using the Access Token from when the user logged in; we have an ASP.NET Core Policy in place for our additional API (as per the IdentityServer docs) to protect it.

The Goal

The desired outcome here is that specific sensitive API endpoints within the Accounts app require the calling user to have undergone a second verification, where they must have re-entered their password recently in order to use the API.

What we want to do is:

  • Allow the Portal app to request a ‘step-up’ access token from the Accounts app.
  • Limit the step-up access token to a short lifetime (say 15 minutes), with no refresh tokens.
  • Call a sensitive API on the Accounts App, and have the Accounts App validate the step-up token.

Issuing the Step-Up Token

First up, we need to generate a suitable access token when asked. I’m going to add a new controller, StepUpApiController, in the Accounts app.

This controller is going to have a single endpoint, which requires a regular access token before you can call it.

We’re going to use the provided IdentityServerTools class, that we can inject into our controller, to do the actual token generation.

Without further ado, let’s look at the code for the controller:

[Route("api/stepup")]
[ApiController]
[Authorize(ApiScopePolicy.WriteUser)]
public class StepUpApiController : ControllerBase
{
    private static readonly TimeSpan ValidPeriod = TimeSpan.FromMinutes(15);

    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IdentityServerTools _idTools;

    public StepUpApiController(UserManager<ApplicationUser> userManager,
                               IdentityServerTools idTools)
    {
        _userManager = userManager;
        _idTools = idTools;
    }

    [HttpPost]
    public async Task<StepUpApiResponse> StepUp(StepUpApiModel model)
    {
        var user = await _userManager.GetUserAsync(User);

        // Verify the provided password.
        if (await _userManager.CheckPasswordAsync(user, model.Password))
        {
            var clientId = User.FindFirstValue(JwtClaimTypes.ClientId);

            var claims = new Claim[]
            {
                new Claim(JwtClaimTypes.Subject, User.FindFirstValue(JwtClaimTypes.Subject)),
            };

            // Create a token that:
            //  - Is associated to the User's client.
            //  - Is only valid for our configured period (15 minutes)
            //  - Has a single scope, indicating that the token can only be used for stepping up.
            //  - Has the same subject as the user.
            var token = await _idTools.IssueClientJwtAsync(
                clientId,
                (int)ValidPeriod.TotalSeconds,
                new[] { "account-stepup" },
                additionalClaims: claims);

            return new StepUpApiResponse { Token = token, ValidUntil = DateTime.UtcNow.Add(ValidPeriod) };
        }

        Response.StatusCode = StatusCodes.Status401Unauthorized;

        return null;
    }
}

A couple of important points here:

  • In order to even access this API, the normal access token being passed in the requested must conform to our own WriteUser scope policy, which requires a particular scope be in the access token to get to this API.
  • This generated access token is really basic; it has a single scope, “account-stepup”, and only a single additional claim containing the subject.
  • We associate the step-up token to the same client ID as the normal access token, so only the requesting client can use that token.
  • We explicitly state a relatively short lifetime on the token (15 minutes here).

Sending the Token

This is the easy bit; once you have the token, you can store it somewhere in the client, and send it in a subsequent request.

Before sending the step-up token, you’ll want to check the expiry on it, and if you need a new one, then prompt the user for their credentials and start the process again.

For any request to the sensitive API, we need to include both the normal access token from the user’s session, plus the new step-up token.

I set this up when I create the HttpClient:

private async Task<HttpClient> GetClient(string? stepUpToken = null)
{
    var client = new HttpClient();

    // Set the base address to the URL of our Accounts app.
    client.BaseAddress = _accountUrl;

    // Get the regular user access token in the session and add that as the normal
    // Authorization Bearer token.
    // _contextAccessor is an instance of IHttpContextAccessor.
    var accessToken = await _contextAccessor.HttpContext.GetUserAccessTokenAsync();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

    if (stepUpToken is object)
    {
        // We have a step-up token; include it as an additional header (without the Bearer indicator).
        client.DefaultRequestHeaders.Add("X-Authorization-StepUp", stepUpToken);
    }

    return client;
}

That X-Authorization-StepUp header is where we’re going to look when checking for the token in the Accounts app.

Validating the Step-Up Token

To validate a provided step-up token in the Accounts app, I’m going to define a custom ASP.NET Core Policy that requires the API call to provide a step-up token.

If there are terms in here that don’t seem immediately obvious, check out the docs on Policy-based authorization in ASP.NET Core. It’s a complex topic, but the docs do a pretty good job of breaking it down.

Let’s take a look at an API call endpoint that requires step-up:

[ApiController]
[Route("api/user")]
[Authorize(ApiScopePolicy.WriteUser)]
public class UserApiController : Controller
{
    [HttpPost("totp-enable")]
    [Authorize("require-stepup")]
    public async Task<IActionResult> EnableTotp(TotpApiEnableModel model)
    {
        // ... do stuff ...
    }
}

That Authorize attribute I placed on the action method specifies that we want to enforce a require-stepup policy on this action. Authorize attributes are additive, so a request to EnableTotp requires both our normal WriteUser policy and our step-up policy.

Defining our Policy

To define our require-stepup policy, lets jump over to our Startup class; specifically, in ConfigureServices, where we set up Authorization using the AddAuthorization method:

services.AddAuthorization(options =>
{
    // Other policies omitted...

    options.AddPolicy("require-stepup", policy =>
    { 
        policy.AddAuthenticationSchemes("local-api-scheme");
        policy.RequireAuthenticatedUser();
        
        // Add a new requirement to the policy (for step-up).
        policy.AddRequirements(new StepUpRequirement());
    });
});

The ‘local-api-scheme’ is the built-in scheme provided by IdentityServer for protecting local API calls.

That requirement class, StepUpRequirement is just a simple marker class for indicating to the policy that we need step-up. It’s also how we wire up a handler to check that requirement:

public class StepUpRequirement : IAuthorizationRequirement
{
}

Defining our Authorization Handler

We now need an Authorization Handler that lets us check incoming requests meet our new step-up requirement.

So, let’s create one:

public class StepUpAuthorisationHandler : AuthorizationHandler<StepUpRequirement>
{
    private const string StepUpTokenHeader = "X-Authorization-StepUp";

    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ITokenValidator _tokenValidator;

    public StepUpAuthorisationHandler(
        IHttpContextAccessor httpContextAccessor,
        ITokenValidator tokenValidator)
    {
        _httpContextAccessor = httpContextAccessor;
        _tokenValidator = tokenValidator;
    }

    /// <summary>
    /// Called by the framework when we need to check a request.
    /// </summary>
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        StepUpRequirement requirement)
    {
        // Only interested in authenticated users.
        if (!context.User.IsAuthenticated())
        {
            return;
        }

        var httpContext = _httpContextAccessor.HttpContext;

        // Look for our special request header.
        if (httpContext.Request.Headers.TryGetValue(StepUpTokenHeader, out var stepUpHeader))
        {
            var headerValue = stepUpHeader.FirstOrDefault();

            if (!string.IsNullOrEmpty(headerValue))
            {
                // Call our method to check the token.
                var validated = await ValidateStepUp(context.User, headerValue);

                // Token was valid, so succeed.
                // We don't explicitly have to fail, because that is the default.
                if (validated)
                {
                    context.Succeed(requirement);
                }
            }
        }
    }

    private async Task<bool> ValidateStepUp(ClaimsPrincipal user, string header)
    {
        // Use the normal token validator to check the access token is valid, and contains our
        // special expected scope.
        var validated = await _tokenValidator.ValidateAccessTokenAsync(header, "account-stepup");

        if (validated.IsError)
        {
            // Bad token.
            return false;
        }

        // Validate that the step-up token is for the same client as the access token.
        var clientIdClaim = validated.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.ClientId);

        if (clientIdClaim is null || clientIdClaim.Value != user.FindFirstValue(JwtClaimTypes.ClientId))
        {
            return false;
        }

        // Confirm a subject is supplied.
        var subjectClaim = validated.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);

        if (subjectClaim is null)
        {
            return false;
        }

        // Confirm that the subject of the stepup and the current user are the same.
        return subjectClaim.Value == user.FindFirstValue(JwtClaimTypes.Subject);
    }
}

Again, let’s take a look at the important bits of the class:

  • The handler derives from AuthorizationHandler<StepUpRequirement>, indicating to ASP.NET that we are a handler for our custom requirement.
  • We stop early if there is no authenticated user; that’s because the step-up token is only valid for a user who is already logged in.
  • We inject and use IdentityServer’s ITokenValidator interface to let us validate the token using ValidateAccessTokenAsync; we specify the scope we require.
  • We check that the client ID of the step-up token is the same as the regular access token used to authenticate with the API.
  • We check that the subjects match (i.e. this step-up token is for the same user).

The final hurdle is to register our authorization handler in our Startup class:

services.AddSingleton<IAuthorizationHandler, StepUpAuthorisationHandler>();

Wrapping Up

There you go, we’ve now got a secondary access token being issued to indicate step-up has been done, and we’ve got a custom authorization handler to check our new token.