ASP.NET MVC: How to implement invitation codes support
Last Christmas I blogged about how to make ASP.NET MVC users authorized only if they have profiles created. This works well on public sites where everybody can be user. Sometimes we don’t want to let all users to our system even when they were correctly authenticated by some authentication service. In this blog post I will show you how to create authorization attribute that you can use to make authenticated users insert their access code if it is their first visit to site.
About my solution
In my application users are authenticated through AppFabric Access Control Service using Windows Live ID. This means that for every authenticated user I will get only authentication token back from Windows Live ID.
Okay, I can show it to users and tell them to e-mail it to me and then insert their data manually but this makes my solution weird. What I can do is add support for invitation codes to my site. Then I can add users I expect to join and send them invitation codes that I also add to users table. When user logs in I will check if he or she has account in system and if there’s no account then system asks for invitation code.
User class
Here is the example of my User class. It is simple and small because I don’t need any advanced profiles stuff in my system. There are two important fields:
- AccessCode – GUID that is used as invitation code. This attribute is unique and it is required for all users who access the system. That’s why it was also very good candidate for primary key.
- UserToken – this field holds unique token for user. This token is coming from Live ID service and it is assigned to user when user inserts correct access code.
So, as a first thing I will insert new accounts to users table and then I will send out invitation codes to users. After successful authentication user is asked for invitation code and if user inserts correct invitation code then his or her Live ID token is inserted to users table row that is identified by given access code.
AccessCodeAuthorizationAttribute
I will use controller attribute on admin interface controllers to restrict access to administrative features. Here is the code for this attribute class.
public class AccessCodeAuthorizationAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
var httpContext = filterContext.HttpContext;
var identity = httpContext.User.Identity;
using (var ctx = new EventsEntities())
{
var user = ctx.GetUserByToken(identity.GetUserToken());
if (user != null)
{
httpContext.Session["UserId"] = user.AccessCode;
return;
}
}
if (!identity.IsAuthenticated ||
httpContext.Session["UserId"] == null)
if (filterContext.Result == null)
httpContext.Response.Redirect("~/getaccess");
}
}
EventsEntities is my Entity Framework model class. GetUserByToken() method looks for user with given security token (actually it is the value of name identifier claim).
GetUserToken() is extension method to IIdentity and it returns us unique code that Live ID service gives to user in our system context (you can use other authentication providers through AppFabric Access Control Services too). As I am using claims based authentication I need something to find the value of name identifier claim.
public static string GetUserToken(this IIdentity identity)
{
var claimsIdentity = (ClaimsIdentity)identity;
var nameClaimQuery = from c in claimsIdentity.Claims
where c.ClaimType == ClaimTypes.NameIdentifier
select c;
var nameClaim = nameClaimQuery.FirstOrDefault();
if (nameClaim == null)
return string.Empty;
return nameClaim.Value;
}
And here is how this attribute is used with administrator area home controller.
[AccessCodeAuthorization]
public class AdminHomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
In this point you can try to run the code and request admin interface through browser. If there is no errors you will be redirected to /getaccess page.
Asking invitation code
Now let’s create new view – GetAccess – that is used to ask invitation code from user. Here is the markup of this view.
@{
ViewBag.Title = "Insert invitation code";
}
<h2>Insert invitation code</h2>
@Html.ValidationMessage("accessCode")
@using (Html.BeginForm()) { <span>
Invitation code:
@Html.TextBox("accessCode")
<input type="submit" value="Ok" />
</span>
}
The page looks something like this.
And here are controller methods for displaying and saving invitation code. I use GUID-s as invitation codes. This code needs optimization and refactoring but it works also like it is right now.
[HttpGet]
public ActionResult GetAccess()
{
return View();
}
[HttpPost]
public ActionResult GetAccess(string accessCode)
{
if (string.IsNullOrEmpty(accessCode.Trim()))
{
ModelState.AddModelError("accessCode", "Insert invitation code!");
return View();
}
Guid accessGuid;
try
{
accessGuid = Guid.Parse(accessCode);
}
catch
{
ModelState.AddModelError("accessCode", "Incorrect format of invitation code!");
return View();
}
using (var ctx = new EventsEntities())
{
var user = ctx.GetNewUserByAccessCode(accessGuid);
if (user == null)
{
ModelState.AddModelError("accessCode", "Cannot find account with given invitation code!");
return View();
}
user.UserToken = User.Identity.GetUserToken();
ctx.SaveChanges();
}
Session["UserId"] = accessGuid;
return Redirect("~/admin");
}
When user inserts invitation code and clicks OK the invitation code is validated. If the code is valid then we ask User with given invitation code from database. Of course, we have one additional check needed – if code is already taken we have to return null from GetNewUserByAccessCode() method. If user inserts free invitation code then this user account is bound to current user and next time when he or she logs in the invitation code is not asked anymore.
Conclusion
As you saw it was not complex task to add support for invitation codes to system. We created new authorization attribute that checks if authenticated user has account or not and if there is no account for user in our system we will ask invitation code from user. To avoid keeping user names and passwords in our system and therefore open it to different kind of nasty attacks we used Windows Azure AppFabric ACS services to authenticate users through Windows Live ID.
Nice and very informative article.
Thanks for posting.
Great Article, but be careful with:
string.IsNullOrEmpty(accessCode.Trim())
if accessCode is null you get an exception, use instead:
string.IsNullOrWhiteSpace(accessCode)
Very useful article. Thanks for sharing!
I never thought it can be such easily implemented.
Thanks. You are great.
Pingback:ASP.NET MVC: Moving code from controller action to service layer | Gunnar Peipman - Programming Blog