I am currently working on an MVC4 project that allows users to authenticate through OpenID. I don’t think I need to convince anyone about the benefits for both parties that come with that. Users don’t have to register at your site, and you have less of those tedious account maintance tasks.
Although it’s apparently coming later on as a built-in feature into the Visual Studio templates (Damien Edwards showed that stuff for Web Forms during aspConf), let me show how you can very quickly add simple OpenID support to your MVC4 application.
More after the jump.
Setting the satge π
Let’s start with the basic MVC4 project, Internet template.
First of all you will need the excellent DotNetOpenAuth library, and that’s, obviously, something you should get from Nuget. After installing, you’d see a number of libraries added to your application.
Next thing to do is to grab openid-selector JS library. This can be downloaded from http://code.google.com/p/openid-selector/. Once downloaded, unzip the contents and copy all the images and CSS folders to your app’s “Content” folder. Additionally, copy openid-en.js and openid-jquery.js from the JS folder to your app’s “Scripts.” We only need that, because we will be relying on JQuery only.
This JS library will give our login page a familiar look you know for example from Stackoverflow, and would act as a ready plug and play interface for OpenID authentication.
Adding the CS and JS to the views π
Since this is MVC4 we can leverage on bundles. Go to App_Start/BundleConfig.cs and add the following there:
bundles.Add(new ScriptBundle("~/bundles/openid").Include(
"~/Scripts/openid-jquery.js",
"~/Scripts/openid-en.js"));
bundles.Add(new StyleBundle("~/Content/css/openid").Include(
"~/Content/openid-shadow.css",
"~/Content/openid.css"));
This would allow us to easily embed the openid specific JS/CSS whereever we need them. We need a couple of more minor tweaks before we proceed. In the Scripts/openid-jquery.js change
img_path : 'images/',
to
img_path : '/Content/images/'
so that it corresponds to our MVC4 project structure. If you are using the standard MVc4 project, go to site.css and remove padding-left and padding-right from the a tag definition, as that would ruin our OpenID layout.
Now let’s add the bundles to our view. Go to Views/Account/Login.cshtml. You can delete the existing FormsAuthentication form entirely, as we will be switching to OpenId authentication. Add our CSS bundle and in the scripts Razor section add our script bundle:
@Styles.Render("~/Content/css/openid")
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
@Scripts.Render("~/bundles/openid")
}
Finally, add the HTML form that will act as our OpenID gateway for our application’s users.
Our front end work is done for now, and we should be getting a nice Login page by now (which, of course, does nothing for the time being).
Implementing OpenID authentication service π
Before we start the C# implementation part of this, let me briefly explain how we are going to approach the problem:
- We will be calling the OpenID provider to authenticate our user
- The response claim will get wrapped into a custom OpenIdUser object
- When the user is successfully authenticated we will issue a FormsAuthentication ticket based on that OpenIdUser, this way we will be able to leverage on ASP.NET IPrincipal implementation (HttpContext.Current.User).
- We will actually have a custom IIdentity implementation as well. This way, you could access the information stored in OpenIdUser from anywhere in the application by just typing User.Identity.OpenIdUser
The model π
We will start by creating the OpenIdUser.
public class OpenIdUser
{
public string Email { get; set; }
public string Nickname { get; set; }
public string FullName { get; set; }
public bool IsSignedByProvider { get; set; }
public string ClaimedIdentifier { get; set; }
public OpenIdUser(string data)
{
populateFromDelimitedString(data);
}
public OpenIdUser(ClaimsResponse claim, string identifier)
{
addClaimInfo(claim, identifier);
}
private void addClaimInfo(ClaimsResponse claim, string identifier)
{
Email = claim.Email;
FullName = claim.FullName;
Nickname = claim.Nickname ?? claim.Email;
IsSignedByProvider = claim.IsSignedByProvider;
ClaimedIdentifier = identifier;
}
private void populateFromDelimitedString(string data)
{
if (data.Contains(";"))
{
var stringParts = data.Split(';');
if (stringParts.Length > 0) Email = stringParts[0];
if (stringParts.Length > 1) FullName = stringParts[1];
if (stringParts.Length > 2) Nickname = stringParts[2];
if (stringParts.Length > 3) ClaimedIdentifier = stringParts[3];
}
}
public override string ToString()
{
return String.Format("{0};{1};{2};{3}", Email, FullName, Nickname, ClaimedIdentifier);
}
}
The model is rather simple, and has the properties you’d normally expect from the User type of model, except Password of course, which is not needed. We have two constructors, because there are two ways in which we will create this object.
First way is to takee in a string that’s been decrypted from Forms authentication ticket UserData part. This is how we will get the info of the user from the cookie and inject into HttpContext as our IIdentity (will show this later).
The second constructor takes in claims response from OpenId provider, and populates the properties based on that.
We also have a simple ToString method which we’ll use to “serialize” the object into a string and embed this data in the Forms ticket.
The service π
Now let’s add the service, which will do all the heavy lifting for us.
We will start off with an interface - although it’s not necessary, it could come in handy if you wish to provide an alternative implementation later on. For example, in this particular tutorial, I will not be saving user information into our database at all - we simply use OpenID to authenticate a user. If you wish to store those OpenIDs in your user repository you might very easily do that.
Moreover, since we have an interface in place, you might want to change the implementation to not piggyback on FormsAuthentication tickets as we will do in this example, but use a completely different IPrincipal implementation.
namespace Mvc4.OpenId.Sample.Security
{
internal interface IOpenIdMembershipService
{
IAuthenticationRequest ValidateAtOpenIdProvider(string openIdIdentifier);
OpenIdUser GetUser();
}
}
The interface defines two methods - one to generate a request that is validatable against some openID provider and the other to get user.
Our implementation is as follows:
public class OpenIdMembershipService : IOpenIdMembershipService
{
private readonly OpenIdRelyingParty openId;
public OpenIdMembershipService()
{
openId = new OpenIdRelyingParty();
}
public IAuthenticationRequest ValidateAtOpenIdProvider(string openIdIdentifier)
{
IAuthenticationRequest openIdRequest = openId.CreateRequest(Identifier.Parse(openIdIdentifier));
var fields = new ClaimsRequest()
{
Email = DemandLevel.Require,
FullName = DemandLevel.Require,
Nickname = DemandLevel.Require
};
openIdRequest.AddExtension(fields);
return openIdRequest;
}
public OpenIdUser GetUser()
{
OpenIdUser user = null;
IAuthenticationResponse openIdResponse = openId.GetResponse();
if (openIdResponse.IsSuccessful())
{
user = ResponseIntoUser(openIdResponse);
}
return user;
}
private OpenIdUser ResponseIntoUser(IAuthenticationResponse response)
{
OpenIdUser user = null;
var claimResponseUntrusted = response.GetUntrustedExtension<ClaimsResponse>();
var claimResponse = response.GetExtension<ClaimsResponse>();
if (claimResponse != null)
{
user = new OpenIdUser(claimResponse, response.ClaimedIdentifier);
}
else if (claimResponseUntrusted != null)
{
user = new OpenIdUser(claimResponseUntrusted, response.ClaimedIdentifier);
}
return user;
}
public HttpCookie CreateFormsAuthenticationCookie(OpenIdUser user)
{
var ticket = new FormsAuthenticationTicket(1, user.Nickname, DateTime.Now, DateTime.Now.AddDays(7), true, user.ToString());
var encrypted = FormsAuthentication.Encrypt(ticket).ToString();
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encrypted);
return cookie;
}
}
It’s not very complicated at all. In the constructor we instantiate an OpenIdRelyingParty, which will be our OpenID authentication mediator. When the client calls ValidateAtOpenIdProvider and passes the username to be authenticated, we set up a set of request fields (Claim, in OpenID lingo) which we’d like the OpenID provider to return - in our case Email, FullName and NickName.
Once client has validated (authenticated itself), it can request the user object. This is done by invoking the GetUser method. This method uses a couple of helpers, but the gist is that we obtain the claim through the use of OpenIdRelyingParty. This claim can the be converted into our custom user object.
Finally, the service API exposes one more method (not defined in the interface), and that’s CreateFormsAuthenticationCookie. This creates a FormsAuthentication ticket which can be sent down to the browser.
To make this all work we need one more small extension method for the IAuthenticateRequest.
public static class Extensions
{
public static bool IsSuccessful(this IAuthenticationResponse response)
{
return response != null && response.Status == AuthenticationStatus.Authenticated;
}
}
Let’s see how we can now utilize this service from the MVC4 controller.
The authentication controller π
Let’s modify the existing AccountController to suit our needs. We won’t need change password and create user methods at all, so you can safely delete them.
We need to make sure we have our OpenID service available, so we add an OpenIdMembershipService field and a initizalize it in the constructor.
private readonly OpenIdMembershipService openidemembership;
public AccountController()
{
openidemembership = new OpenIdMembershipService();
}
Let’s look at the action which will be used for authentication.
[AllowAnonymous]
[HttpPost]
public ActionResult Login(string openid_identifier)
{
var response = openidemembership.ValidateAtOpenIdProvider(openid_identifier);
if (response != null)
{
return response.RedirectingResponse.AsActionResult();
}
return View();
}
If you recall, ValidateAtOpenIdProvider will return IAuthenticationRequest, which in turn exposes a property RedirectingResponse. This lets us send the user to the external page, at the OpenID provider server (i.e. Google login), where he will need to authenticate himself. Then the view re-renders, so that automatically means we have to put the rest of the login in the default GET version of the public ActionResult Login() action.
[AllowAnonymous]
public ActionResult Login()
{
var user = openidemembership.GetUser();
if (user != null)
{
var cookie = openidemembership.CreateFormsAuthenticationCookie(user);
HttpContext.Response.Cookies.Add(cookie);
return new RedirectResult(Request.Params["ReturnUrl"] ?? "/");
}
return View();
}
So when the view re-renders, we try to get user from our OpenID membership service. If the user is successfully returned - meaning the user has authenticated without any problems, we proceed by issuing a FormsAuthentication ticket and redirecting the user back to the original page on which his Authentication challenged was raised - that’s levering on the standard ASP.NET ReturnUrl query string.
The user should at this point see the following:
We might as well end this tutorial here. If you need very basic authentication mechanism, this should already be OK. I will take it a step further and let’s add a customized IIdentity. This would allow us to have access to our OpenIdUser anywhere within the application by calling User.Identity.
Custom IIdentity π
We will implement System.Security.Principal.IIdentity interafce, and add an OpenIdUser property there.
public class OpenIdIdentity : IIdentity
{
private readonly OpenIdUser _user;
public OpenIdIdentity(OpenIdUser user)
{
_user = user;
}
public OpenIdUser OpenIdUser
{
get
{
return _user;
}
}
public string AuthenticationType
{
get
{
return "OpenID Identity";
}
}
public bool IsAuthenticated
{
get
{
return true;
}
}
public string Name
{
get
{
return _user.Nickname ?? string.Empty;
}
}
}
We will inject this custom implementation of Identity into the Principal object using a custom AuthorizeAttribute. You could do that also by registering a handler for PostAuthenticateRequest in Global.asax but that’s a bit of an overkill, as it would execute numerous times, whereas the logic in the attribute will only execute once per request.
public class OpenIdAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var isAuthorized = base.AuthorizeCore(httpContext);
if (isAuthorized)
{
var authenticatedCookie = httpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authenticatedCookie != null)
{
var authenticatedCookieValue = authenticatedCookie.Value.ToString();
if (!string.IsNullOrWhiteSpace(authenticatedCookieValue))
{
var decryptedTicket = FormsAuthentication.Decrypt(authenticatedCookieValue);
var user = new OpenIdUser(decryptedTicket.UserData);
var openIdIdentity = new OpenIdIdentity(user);
httpContext.User = new GenericPrincipal(openIdIdentity, null);
}
}
}
return isAuthorized;
}
}
This grabs the OpenIdUser object from the FormsAuthentication ticket (or rather its UserData section) and inject it into the HTTP context. As a result, we now can go to our partial view for the login, _LoginPartial.cshtml, and replace the login section with
Hello, @User.Identity.Name
@Html.ActionLink("Log off", "LogOff", "Account")
Now this isn’t anything spectacular yet. But we could also use our custom Identity
Hello, @( (User.Identity as Mvc4.OpenId.Sample.Models.OpenIdIdentity).OpenIdUser.Email)
@Html.ActionLink("Log off", "LogOff", "Account")
As you see, this way we get access to the OpenID properties anywhere we want. Remember to decorate whichever action or controller you wish to be protected by OpenID with OpenIDAuthorize i.e.
public class HomeController : Controller
{
[OpenIdAuthorize]
public ActionResult Index()
{
ViewBag.Message = "Modify this template to kick-start your ASP.NET MVC application.";
return View();
}
[OpenIdAuthorize]
public ActionResult About()
{
ViewBag.Message = "Your app description page.";
return View();
}
}
As a side note, we don’t need any implementation of logout, since the standard one included in the out of the box AccountController does that for us (it deletes the FormsAuthentication ticket).
Trying it out π
Now that we have the application’s pieces in place, let’s try it. If you used the attributes like I did above, you should be challenged right away.
If you try Google, you should see this:
And then login successfully
Same with Yahoo:
Summary π
In a few simple steps we have implemented OpenID authentication thanks to the very robust DotNetOpenAuth library. You could obviously take it further, as I mentioned before, perhaps store user’s accounts in some repository of yours. Either way, this should be a decent start and I hope someone someone finds it useful. Source code is below (github).