If you are writing a web application, often, you’ll need the user to accept a license or user agreement. You can do this as part of the user registration process, however, what if your license agreement changes and you need the user to re-accept the agreement? You could hardcode the check into your /Home/Index action, however, if the user bookmarks some other action or goes there directly, they can circumvent the new license agreement.
I wanted to create a way to force the user to accept the license agreement in a similar way to how the user is forced to authenticate themselves: a page is displayed, regardless of where they try to go, and when they accept the agreement, they are only then redirected back to where they want to go in the first place.
In this article, I will show you how to add a click-through license agreement to your MVC3 web application. It will store the user’s acceptance in the standard ASP.NET Profile system. This data does not need to be stored in the profile system. I just used it here as an example. You can store it anywhere your business logic warrants with minor changes.
Add a Property to ASP.NET’s Profile
If you are not storing the user’s acceptance in the user profile, then you can skip this step.
Since we are using ASP.NET’s profile system to store the user’s acceptance, we need to add a property to the profile system. We do this by adding the following into our application’s web.config file.
<system.web>
<profile>
<properties>
<add name="AcceptedLicense" defaultValue="false"
type="System.Boolean" />
</properties>
</profile>
<system.web>
If your web.config file already has the <profile> section, then just add the <properties> section inside your existing <profile> section.
Create the Attribute Class
We are going to be using an attribute to indicate which controllers and actions require acceptance of the license agreement.
Create a new class and call it “RequiresLicenseAgreementAttribute”. Override the OnActionExecuting method as follows:
public class RequiresLicenseAgreementAttribute :
ActionFilterAttribute
{
public override void OnActionExecuting(
ActionExecutingContext filterContext)
{
ProfileBase profile = ProfileBase.
Create(filterContext.HttpContext.User.Identity.Name);
bool hasAcceptedLicense =
(bool)profile.GetPropertyValue("AcceptedLicense");
if (!hasAcceptedLicense)
{
filterContext.HttpContext.Response.StatusCode = 418;
filterContext.HttpContext.
ApplicationInstance.CompleteRequest();
return;
}
base.OnActionExecuting(filterContext);
}
}
This attribute performs the following:
- It checks our business logic to see if the user has accepted the license agreement. In this sample, I am using the user’s profile. However, this can be changes as per your own application’s business logic.
- If the user has not accepted the agreement, then we abandon the request and trigger a 418 status code. 418 is an unused code called “I’m a Teapot”. It was originally created as a joke. We will make use of it here since it is unlikely it will be used elsewhere.
Create the HttpModule to Intercept 418
Once we trigger the 418 code, we need to intercept it later on in the pipeline so that we can redirect the user. For this, we need to create an HttpModule which will have a handler for the “end request” event.
Create a new class and call it “LicenseAgreementModule”. In my sample, I put it in an HttpModules sub-namespace.
To this class’s Init() method, add the following:
context.EndRequest += new EventHandler(OnEndRequest);
Later on, when you’re running your app, you may get an exception about the following line requiring IIS integrated pipeline mode. If so, then you can comment out this line since we are not going to need it.
//context.LogRequest += new EventHandler(OnLogRequest);
In our event handler, we will watch for status code 418. If we see it, we will redirect the user to the license agreement action.
void OnEndRequest(object sender, EventArgs e)
{
HttpContext context = ((HttpApplication)sender).Context;
if (context.Response.StatusCode == 418)
{
string rawUrl = context.Request.RawUrl;
if ((rawUrl.IndexOf("?ReturnUrl=",
StringComparison.Ordinal) == -1)
&& (rawUrl.IndexOf("&ReturnUrl=",
StringComparison.Ordinal) == -1))
{
string strUrl = "/Account/LicenseAgreement";
string str3 = strUrl + "?ReturnUrl=" +
HttpUtility.UrlEncode(rawUrl,
context.Request.ContentEncoding);
context.Response.Redirect(str3, false);
}
}
}
Add Module to web.config
The next step is to add our HttpModule to the web application’s web.config file.
Add the following in the appropriate place:
<system.web>
<httpModules>
<add name="LicenseAgreement"
type="LicenseAgreementSample.HttpModules.LicenseAgreementModule"/>
</httpModules>
</system.web>
Create a LicenseAgreement Action
We need to create an action to display and handle the License Agreement acceptance. We will put it in our AccountController.
[Authorize]
[HttpGet]
public ActionResult LicenseAgreement()
{
return View();
}
[Authorize]
[HttpPost]
public ActionResult LicenseAgreement(string returnUrl)
{
if (ModelState.IsValid)
{
ProfileBase profile =
ProfileBase.Create(User.Identity.Name);
profile.SetPropertyValue("AcceptedLicense", true);
profile.Save();
if (Url.IsLocalUrl(returnUrl) &&
returnUrl.Length > 1 &&
returnUrl.StartsWith("/") &&
!returnUrl.StartsWith("//") &&
!returnUrl.StartsWith("/\\"))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
return View();
}
In the above, we have two handlers for our action: one for the initial display of the agreement (first one), and one for the acceptance (second one).
The first one is pretty basic: just show the view.
The second one does all the work:
- Save to our profile that the user accepted the license agreement
- If our URL has a return address stored in it, then redirect to that URL.
- If our model is not valid, then re-show our page.
Create the License Agreement View
Next, create the actual license agreement view that the user will see and accept.
Create a new view for your new action. My sample license agreement is pretty bleak. You will want to populate it with whatever agreement you require. My view only contains a single button.
@{
ViewBag.Title = "LicenseAgreement";
}
<h2>LicenseAgreement</h2>
@using (Html.BeginForm())
{
<button type="submit">Accept</button>
}
Note that I am using the Razor engine above.
Apply our Attribute to our Controllers and Actions
Now that everything is in place, our final step is to apply our custom attribute to our controllers. In this sample, I will force all actions in the HomeController to require acceptance of the license agreement. Here, I can apply it to the controller itself.
[Authorize]
[RequiresLicenseAgreement]
public class HomeController : Controller
Note, that I have added the [Authorize] attribute as well. Since we are storing the agreement in the user’s profile, a user needs to be logged in.
Once this is in place, when you run your application, you should see:
- The log in page, then
- The license agreement page, then
- The desired page that the user originally navigated to
Some Additional Notes
- I originally thought that I would have to deal with attribute ordering to ensure the user logs in before they need to accept the agreement. I have been told that authentication will always happen before any action filters. So we do not need to worry about this.
- If you want to add a “Do not accept” button to your license agreement view, you will need to do one of two things:
- Have that button go to a different action, or
- Add a check in your controller to see which button the user clicked
Final Word
As I write this, I am asking myself whether the HttpModule is really required. Would it be possible to simply redirect right from the RequiresLicenseAgreementAttribute class instead of using the 418 status code? Maybe. I will have to try it one day.
References
I want to thank the guys in the MVC Forum. They helped me out and gave me the info I needed to create this.
The following were used in the research to this solution:
Sample Application
Here is a link to the sample project using all of the above.
Mvc3LicenseAgreementSample.zip (1.98 MB)