I recently read a classic blog post from the RavenDB community recommending developers think twice about exposing primary identifiers in their URLs. There are several reasons for this, which I’ll summarize in this post, but I wanted to revisit the problem and see what the current ASP.NET Core development stack has to offer when it comes to solving this problem.

In this post, we’ll see some code that can both encrypt and decrypt sensitive identifiers in the URL path.

Why Obscure Identifiers?

Many people first think, “Why obscure identifiers in the first place?” I mean, it’s just an ID, right?

There are several arguments as to why it may be a “bad” practice.

1. URL Tampering

While we all do our best to secure our applications, there are times when we may forget to properly verify that a user has permission to access a particular resource.

https://example.com/?id=1

Over the years, we’ve become acutely aware that changing the value of the id in a URL might return a different resource. This may be intended behavior, or it could give “hackers” another avenue to exfiltrate data from your system.

Making identifiers more opaque can eliminate a user experience you never intended to offer.

2. Leaking System Information

Most identifiers can be tied back to their supporting system. For example, integers are widely used in relational databases, while document databases use string identifiers. This extra information may give users more information about your internal system and architectural choices than you intended.

3. Leaking Business Information

This is likely the most important for your business stakeholders. Since most identifiers are incremental, they could accidentally leak your business’s critical metrics to outside parties. You could track the current volume and growth velocity over time of a system type. Some hypothetical entities may include orders, issues, and products.

The reasons given here clearly rely on your context and your application security. So, while you may or may not ultimately decide to obscure identifiers, it’s still important to consider why you’re making that decision.

ASP.NET Core Route Constraints

In this code, we’ll use ASP.NET Core’s route constraints to provide an implicit way of encrypting identifiers in route templates. We’ll also use the DataProtection APIs, so we don’t have to worry about encryption keys and storage for this example. However, you could adapt this code to use any methodology you have in mind.

The first step is to create a new class called EncryptedParameter, and we’ll need to implement the IOutboundParameterTransformer interface.

using Microsoft.AspNetCore.DataProtection;

public class EncryptedParameter(IDataProtectionProvider dpp) : 
    IOutboundParameterTransformer
{
    private readonly IDataProtector protector 
        = dpp.CreateProtector("EncryptedParameter");
    
    public string? TransformOutbound(object? value)
    {
        var result = value?.ToString();
        return string.IsNullOrEmpty(result) 
            ? null 
            : protector.Protect(result);
    }
}

We need to register this code as a route constraint. Add the following code to the Program class of your ASP.NET Core application.

builder.Services.AddDataProtection();
builder.Services.Configure<RouteOptions>(opt =>  {
    opt.ConstraintMap.Add("encrypt", typeof(EncryptedParameter));
});

We have to register the constraint, and since our constraint is using the DataProtection API, we also need to remember to add the data protection services.

We can now use the constraint in our route templates on our target actions. Here’s an example being used in a Razor Page route template.

@page "{id:encrypt}"
@model RavenDbTodoApp.Pages.Agenda

<h1>@Model.Id</h1>

When we use ASP.NET Core’s LinkGenerator, it will use our constraint to process parameters.

<a href="@Url.Page("Agenda", new { id = "agenda/1" })">
    Link To Agenda
</a>

The previous code generates the following HTML link.

<a href="/Agenda/CfDJ8LTlmMRHw3JNmUOvTfhPRjsGI3dXCSP-7yuu-Hu05yjURdIZkalKYJc7-rbJbOXrJCkeLdywxW7m6A7XT3ylMY6ilrNC5DYdssyWTA1-QCHpqFFvRi6LokwktvGkcGs5BA">
    Link To Agenda
</a>

Neat, but as you may have noticed, this is only one part of the equation. Dealing with these identifiers may become cumbersome if we don’t use the ASP.NET Core machinery. Let’s implement the second part.

ASP.NET Core Model Binders

We’ll be using ASP.NET Core’s model binding machinery to transform the incoming parameter into something more usable. Let’s modify our EncryptedParameter class with a new interface, IModelBinder.

using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc.ModelBinding;

public class EncryptedParameter(IDataProtectionProvider dpp) : 
    IModelBinder,
    IOutboundParameterTransformer
{
    private readonly IDataProtector protector 
        = dpp.CreateProtector("EncryptedParameter");
    
    public string? TransformOutbound(object? value)
    {
        var result = value?.ToString();
        return string.IsNullOrEmpty(result) 
            ? null 
            : protector.Protect(result);
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var key = bindingContext.FieldName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(key);
        
        if (valueProviderResult.FirstValue is { } value)
        {
            var result = protector.Unprotect(value);
            bindingContext.Result = ModelBindingResult.Success(result);
        }

        return Task.CompletedTask;
    }
}

Now, let’s apply this model binder to a parameter on the receiving page.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class Agenda: PageModel
{
    [BindProperty(SupportsGet = true, BinderType = typeof(EncryptedParameter))]
    public string Id { get; set; }
    
    public void OnGet()
    {
        
    }
}

Now, you can access the parameter with its original value, which should make it easier to use against your existing data storage engine of choice.

Some thoughts on this approach

You may want to evaluate a few things when taking this approach and whether it’s worth the tradeoffs.

  1. Encryption and Decryption can be expensive, adding to the overhead of each link generation and request.
  2. Encryption creates some really long strings. I’m not an encryption expert, but there are likely encryption algorithms that produce more compact strings.
  3. A little more noise in code with constraints and attributes.
  4. Easy to miss a constraint or attribute as the system grows.

Conclusion

If you’ve been worried about URL tampering or exposing critical system information, the approach outlined in this post might be useful to you. That said, before slapping this onto your applications, please consider the trade-offs and costs of applying encryption to parameters.

As always, thanks for reading and I hope this article has helped. Cheers