A few Decorators is all it takes.

This article is part of a short series on fit URLs. In the overview article, I argued that you should be signing URLs in order to prevent your REST APIs from becoming victims of Hyrum's law.

In this article, you'll see how to do this with ASP.NET Core 3.1, and in the next article you'll see how to check URL integrity.

The code shown here is part of the sample code base that accompanies my book Code That Fits in Your Head.

SigningUrlHelper #

I wanted the URL-signing functionality to slot into the ASP.NET framework, which supplies the IUrlHelper interface for the purpose of creating URLs. (For example, the UrlBuilder I recently described relies on that interface.)

Since it's an interface, you can define a Decorator around it:

internal sealed class SigningUrlHelper : IUrlHelper
{
    private readonly IUrlHelper inner;
    private readonly byte[] urlSigningKey;
 
    public SigningUrlHelper(IUrlHelper inner, byte[] urlSigningKey)
    {
        this.inner = inner;
        this.urlSigningKey = urlSigningKey;
    }
 
    // More code comes here...

As you can tell, this Decorator requires an inner IUrlHelper and a urlSigningKey. Most of the members just delegate to the inner implementation:

public bool IsLocalUrl(string url)
{
    return inner.IsLocalUrl(url);
}

The Action method creates URLs, so this is the method to modify:

public string Action(UrlActionContext actionContext)
{
    var url = inner.Action(actionContext);
    if (IsLocalUrl(url))
    {
        var b = new UriBuilder(
            ActionContext.HttpContext.Request.Scheme,
            ActionContext.HttpContext.Request.Host.ToUriComponent());
        url = new Uri(b.Uri, url).AbsoluteUri;
    }
    var ub = new UriBuilder(url);
 
    using var hmac = new HMACSHA256(urlSigningKey);
    var sig = Convert.ToBase64String(
        hmac.ComputeHash(Encoding.ASCII.GetBytes(url)));
 
    ub.Query = new QueryString(ub.Query).Add("sig", sig).ToString();
    return ub.ToString();
}

The actionContext may sometimes indicate a local (relative) URL, in which case I wanted to convert it to an absolute URL. Once that's taken care of, the method calculates an HMAC and adds it as a query string variable.

SigningUrlHelperFactory #

While it's possible take ASP.NET's default IUrlHelper instance (e.g. from ControllerBase.Url) and manually decorate it with SigningUrlHelper, that doesn't slot seamlessly into the framework.

For example, to add the Location header that you saw in the previous article, the code is this:

private static ActionResult Reservation201Created(
    int restaurantId,
    Reservation r)
{
    return new CreatedAtActionResult(
        nameof(Get),
        null,
        new { restaurantId, id = r.Id.ToString("N") },
        r.ToDto());
}

The method just returns a new CreatedAtActionResult object, and the framework takes care of the rest. No explicit IUrlHelper object is used, so there's nothing to manually decorate. By default, then, the URLs created from such CreatedAtActionResult objects aren't signed.

It turns out that the ASP.NET framework uses an interface called IUrlHelperFactory to create IUrlHelper objects. Decorate that as well:

public sealed class SigningUrlHelperFactory : IUrlHelperFactory
{
    private readonly IUrlHelperFactory inner;
    private readonly byte[] urlSigningKey;
 
    public SigningUrlHelperFactory(IUrlHelperFactory inner, byte[] urlSigningKey)
    {
        this.inner = inner;
        this.urlSigningKey = urlSigningKey;
    }
 
    public IUrlHelper GetUrlHelper(ActionContext context)
    {
        var url = inner.GetUrlHelper(context);
        return new SigningUrlHelper(url, urlSigningKey);
    }
}

Straightforward: use the inner object to get an IUrlHelper object, and decorate it with SigningUrlHelper.

Configuration #

The final piece of the puzzle is to tell the framework about the SigningUrlHelperFactory. You can do this in the Startup class' ConfigureServices method.

First, read the signing key from the configuration system (e.g. a configuration file):

var urlSigningKey = Encoding.ASCII.GetBytes(
    Configuration.GetValue<string>("UrlSigningKey"));

Then use the signing key to configure the SigningUrlHelperFactory service. Here, I wrapped that in a little helper method:

private static void ConfigureUrSigning(
    IServiceCollection services,
    byte[] urlSigningKey)
{
    services.RemoveAll<IUrlHelperFactory>();
    services.AddSingleton<IUrlHelperFactory>(
        new SigningUrlHelperFactory(
            new UrlHelperFactory(),
            urlSigningKey));
}

This method first removes the default IUrlHelperFactory service and then adds the SigningUrlHelperFactory instead. It decorates UrlHelperFactory, which is the default built-in implementation of the interface.

Conclusion #

You can extend the ASP.NET framework to add a signature to all the URLs it generates. All it takes is two simple Decorators.

Next: Checking signed URLs with ASP.NET.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 02 November 2020 08:20:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 02 November 2020 08:20:00 UTC