= 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. For an overview of the rationale or the operational details behind Clavis, see [http://higherlogics.blogspot.ca/search/label/Clavis the Clavis blog posts]. You need only understand a few concepts to use Clavis: 1. IContinuation for specifying page parameter types (up to 8 parameters at the moment) 1. Unsafe for specifying unsafe/insecure page parameters 1. {{{Continuation.ToUrl}}} overloads for generating URLs from continuations + arguments 1. {{{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 { ... } }}} 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 { ... } }}} Clavis can also handle lists of values by specifying IEnumerable as a parameter type: {{{ public class SomePage : System.Web.Page, IContinuation, Customer> { ... } }}} == Unsafe Parameters == If you wish to declare that a certain page parameter is unprotected, then you need only wrap it with Unsafe. For instance, suppose the integer argument from the first example should be unprotected: {{{ public class SomePage : System.Web.Page, IContinuation, string> { ... } }}} IEnumerable and Unsafe can also be nested, so you can have an unsafe list of objects as a parameter: {{{ public class SomePage : System.Web.Page, IContinuation>, Customer> { ... } }}} == Generating URLs == You can easily generate a URL from a continuation with its arguments like so: {{{ var url = Continuation.ToUrl( 3.AsParam(), "hello world!".AsParam()); }}} The first type argument, {{{SomePage}}}, is the continuation type. The subsequent type arguments are the type arguments to IContinuation<...>. Another method of generating a URL that requires fewer type annotations: {{{ var url = Continuation.Params(3.AsParam(), "hello world!".AsParam()); .ToUrl(); }}} The {{{Param.AsParam()}}} extension methods are fully defined over all [http://msdn.microsoft.com/en-us/library/system.iconvertible.aspx IConvertible] types. Types that aren't IConvertible require that you provide an ICovertible type as a key: {{{ public class SomePage : System.Web.Page, IContinuation { ... } ... var url = Continuation.Params(project.AsParam(project.Id), customer.AsParam(customer.Id)); .ToUrl(); }}} == 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 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 Id: {{{ public class SomePage : System.Web.Page, IContinuation { Project arg0; Customer arg1; override protected void OnInit(EventArgs e) { Id id0; if (this.TryParse0(out id0)) arg0 = SomeDb.Projects.Single(x => x.Id == id0.Key); Id 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, Customer> { IEnumerable arg0; Customer arg1; override protected void OnInit(EventArgs e) { IEnumerable> id0; if (this.TryParse0(out id0)) { var projectIds = id0.Select(x => x.Key).ToList(); arg0 = SomeDb.Projects.Where(x => projectIds.Contains(x.Id)); } Id 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. 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 was 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.