Clavis: Secure, Type-safe URLs for ASP.NET
Clavis is a simple class library for use with ASP.NET. It provides primitives for secure parameter passing between pages, with compiler-verified taint-checking for any insecure parameters. You can download the preview via Nuget.
You need only understand a few concepts to use Clavis:
- IContinuation for specifying page parameter types (up to 16 parameters at the moment)
- Unsafe<T> for specifying unsafe/insecure page parameters
- Continuation.ToUrl overloads for generating URLs from continuations + arguments
- Continuation.TryParseX for parsing page parameters
Typed Page Parameters
A typed page parameter list is specified via an IContinuation<...> declaration, like so:
public class SomePage : System.Web.Page, IContinuation<int, string> { ... }
This declares a page that accepts a protected Int32 as the first argument, and a protected string as the second argument. By default, all types specified in an IContinuation<...> declaration will be protected, which means they cannot be changed by clients.
Any type can appear as an argument to IContinuation<...>, not just primitive values. In fact, it's good practice in Clavis not to use primitive types since parameter names are generated from the class name by default:
public class SomePage : System.Web.Page, IContinuation<Project, Customer> { ... }
Clavis can also handle lists of values by specifying IEnumerable<T> as a parameter type:
public class SomePage : System.Web.Page, IContinuation<IEnumerable<Project>, Customer> { ... }
Unsafe Parameters
If you wish to declare that a certain page parameter is unprotected, then you need only wrap it with Unsafe<T>. For instance, suppose the integer argument from the first example should be unprotected:
public class SomePage : System.Web.Page, IContinuation<Unsafe<int>, string> { ... }
IEnumerable<T> and Unsafe<T> can also be nested, so you can have an unsafe list of objects as a parameter:
public class SomePage : System.Web.Page, IContinuation<Unsafe<IEnumerable<Project>>, Customer> { ... }
Generating URLs
You can easily generate a URL from a continuation with its arguments like so:
var url = Continuation.Params(3.AsParam(), "hello world!".AsParam()); .ToUrl<SomePage>();
AsParam is an extension method that converts values into parameters that can be passed to an IContinuation. Then the actual continuation type is provided to generate the URL. If the set of parameters don't match the resource's signature, then a compile error is generated.
The Param.AsParam() extension methods are fully defined over all IConvertible types. Types that aren't IConvertible require that you provide an ICovertible type as a key:
public class SomePage : System.Web.Page, IContinuation<Project, Customer> { ... } ... var url = Continuation.Params(project.AsParam(project.Id), customer.AsParam(customer.Id)); .ToUrl<SomePage>();
Parsing Page Parameters
Inside the continuation, you can obtain access to page parameter values via the Param.TryParseX overloads, where X is the index of the parameter in the IContinuation<...> specification:
public class SomePage : System.Web.Page, IContinuation<int, string> { int arg0; string arg1; override protected void OnInit(EventArgs e) { if (this.TryParse0(out arg0)) ; //do something with arg0 if (this.TryParse1(out arg1)) ; //do something with arg1 } }
The above will only work if the types are IConvertible. For non-IConvertible values, you instead need to parse an Clavis.Id<TKey, TType>:
public class SomePage : System.Web.Page, IContinuation<Project, Customer> { Project arg0; Customer arg1; override protected void OnInit(EventArgs e) { Id<int, Project> id0; if (this.TryParse0(out id0)) arg0 = SomeDb.Projects.Single(x => x.Id == id0.Key); Id<int, Customer> id1; if (this.TryParse1(out arg1)) arg1 = SomeDb.Customers.Single(x => x.Id == id1.Key); } }
Lists of non-IConvertible types work the same:
public class SomePage : System.Web.Page, IContinuation<IEnumerable<Project>, Customer> { IEnumerable<Project> arg0; Customer arg1; override protected void OnInit(EventArgs e) { IEnumerable<Id<int, Project>> id0; if (this.TryParse0(out id0)) { var projectIds = id0.Select(x => x.Key).ToList(); arg0 = SomeDb.Projects.Where(x => projectIds.Contains(x.Id)); } Id<int, Customer> id1; if (this.TryParse1(out arg1)) arg1 = SomeDb.Customers.Single(x => x.Id == id1.Key); } }
Clavis Setup
Global.asax.cs
Clavis requires a very simple setup. In Global.asax.cs, you simply need to call Continuation.Init to initialize the Clavis library with a private 64-byte key:
protected void Application_Start(object sender, EventArgs e) { var key = "ha6dMh+cymHn25ndckkQ9ajtzCu97frmpsmUzTLHEjwMp7nXMX/dqYfATANqNf5jy5Wvi1BFnz1293lc1D3KKw=="; Continuation.Init(key); }
This makes all URLs delegable, which is to say that no cookies or other type of user-specific context prevents a user from sharing this URL. His private URL carries his full credentials, assuming the URL contains credentials.
If you wish to make URLs non-delegable, then use the other Continuation.Init overload where you can specify a custom context that is hashed with the default HMAC:
protected void Application_Start(object sender, EventArgs e) { var key = "ha6dMh+cymHn25ndckkQ9ajtzCu97frmpsmUzTLHEjwMp7nXMX/dqYfATANqNf5jy5Wvi1BFnz1293lc1D3KKw=="; Continuation.Init(key, () => { var x = HttpContext.Current.Session; return x == null ? "" : x.SessionID; }); }
Here we use the default ASP.NET session id which is stored in cookies to ensure that users can't inadvertently leak their private URL.
Page.OnPreInit
The last step is to add continuation validation at some point in the page lifecycle. Clavis checks that all the protected parameters hash to the same HMAC value as specified in the URL. If they don't, an exception is thrown indicating that a protected parameter was incorrectly changed. I typically do this in Page.OnPreInit in a custom System.Web.Page base class:
protected override void OnPreInit(EventArgs e) { var k = this as IContinuationBase; if (k != null) k.Validate(); }
As you can see, Clavis is a drop-in library that doesn't affect existing code. You can incrementally deploy it by converting one page at a time to continuations, and these pages will validate automatically as soon as they implement some IContinuation<...> type.
You don't need to perform validation exactly as above of course, you just need to place a call to Continuation.Validate() somewhere in the page lifecycle. On init or pre-init is preferable so you don't waste time parsing parameters that won't validate anyway.
References
- An overview of the rationale or the operational details behind Clavis is available in the Clavis blog posts.
- Full API documentation is available online, or as a downloadable .CHM file
- The latest Clavis version can be downloaded via Nuget