Signing URLs with ASP.NET by Mark Seemann
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.