OpenID is a centralized membership mechanism. You register at one site and use those credentials with all other sites that you want to be a member of. This means that you have only one username and password set to memorize and, more importantly, that password is stored at only one location. When you want to log into a site that is enabled with OpenID authentication, you're forwarded to your OpenID provider. When you login, you are redirected back to the original site with your unique authorization token.
There are a lot of OpenID providers out there, including Google, Yahoo!, and Flickr. If you don't want to use one of the available providers, you can always roll your own OpenID provider.
Setting up your website for OpenID authentication is typically super-easy.
OpenID frameworks for .NET
There are a couple of different OpenID frameworks for .NET, but the one with the most active development is DotNetOpenAuth. On top of providing OpenID support for both the 1.x and 2.0 versions, DotNetOpenAuth also implements OAuth, which is a protocol for secure authorization. DotNetOpenAuth supports OpenID development in ASP, classic ASP .NET web applications, as well as ASP .NET MVC websites.
In this article, we're going to talk about how to setup OpenID authentication in your ASP .NET MVC website. Our process will be as follows:
- When users want to login, we provide a way for the users to select the URL for their OpenID provider.
- We redirect the users to the provider for authentication.
- If login failed for whatever reason, we let the user know.
- Otherwise, we check to see if this is the first time user is logging into our site.
- If it is, we register the user and redirect the user to select his or her display name.
- We update last login date for the user -- after they selected their display name in case of registration.
Login ViewPage
Our login view page is fairly simple: it includes a textbox for the user to enter the OpenID provider url. There are quite a few javascript libraries out there that make this a one-click process. Check out ID Selector.
<h2>Login with OpenID</h2>
<% Html.BeginForm("Authenticate", "Login"); %>
<fieldset title="Login via OpenID">
<% Html.RenderPartial("ErrorControl"); %>
<p>
<label for="openIdIdentifier">
Manually enter your OpenID URL: </label>
<%= Html.TextBox("openIdIdentifier", null, new { @class = "openId" }) %>
<input type="submit" value="Login" />
</p>
</fieldset>
<% Html.EndForm(); %>
Authenticate via OpenID
Once the Login button is clicked, we get taken to the Authenticate action, which looks like this:
public ActionResult Authenticate(string openIdIdentifier)
{
var openId = new OpenIdRelyingParty();
var response = openId.GetResponse();
if (UserNeedsToLogin(response))
{
return AskUserToLogin(openIdIdentifier, openId);
}
return HandleAuthenticationResponse(response);
}
private bool UserNeedsToLogin(IAuthenticationResponse response)
{
return response == null;
}
private ActionResult AskUserToLogin(string openIdIdentifier, OpenIdRelyingParty openId)
{
var request = openId.CreateRequest(openIdIdentifier);
return request.RedirectingResponse.AsActionResult();
}
private ActionResult HandleAuthenticationResponse(IAuthenticationResponse response)
{
switch(response.Status)
{
case AuthenticationStatus.Authenticated:
return HandleSuccessfulLogin(response);
case AuthenticationStatus.Canceled:
_context.ErrorMessage = "Login was cancelled at the provider.";
break;
case AuthenticationStatus.Failed:
_context.ErrorMessage = "Login failed at the provider.";
break;
case AuthenticationStatus.SetupRequired:
_context.ErrorMessage = "The provider requires setup.";
break;
default:
_context.ErrorMessage = "Login failed.";
break;
}
return View("Index");
}
Successful login
The code for successful login looks as expected given our requirements:
private ActionResult HandleSuccessfulLogin(IAuthenticationResponse response)
{
var claimedIdentifier = response.ClaimedIdentifier;
registerIfUserIsNotRegisteredYet(claimedIdentifier);
currentUserIs(claimedIdentifier);
if (_context.CurrentUser.HasYetToLogin)
{
return RedirectToAction("FirstTimeLogin");
}
currentUserIsLoggedIn();
return View();
}
private void currentUserIsLoggedIn()
{
_accountRepository.Login(_context.CurrentUser);
}
If it's the first time the user is logging in, we register the user with the identifier returned by the OpenID provider.
private void registerIfUserIsNotRegisteredYet(Identifier claimedIdentifier)
{
if (_accountRepository.Fetch(claimedIdentifier) == null)
{
var regularRole = new string[] { "Regular" };
var userToRegister = new Account(claimedIdentifier, regularRole)
{
Id = 0,
LastLoginAt = DateTime.MinValue,
Profile = new Profile { DisplayName = claimedIdentifier }
};
_accountRepository.Insert(userToRegister);
}
}
Account implements IPrincipal, and Profile has only one property for now (DisplayName). (You can add any other properties that make sense to your application, like FirstName, LastName, Email, etc.)
Setting up the current user
We also need to let our application know who the current user is. I do that by setting a "CurrentUser" property on my "context" object:
private void currentUserIs(Identifier claimedIdentifier)
{
var registeredUser = _accountRepository.Fetch(claimedIdentifier);
_contextUser.CurrentUser = registeredUser;
}
If user is logging in for the first time, ask for required information
Normally, I use the username for both Identity.Name and DisplayName. But with OpenID, the identifiers can be long, bizarre, and useless, so we need the user to provide us a display name. When the user provides us with the display name, we update our repository with the information. Since we figure out whether or not the user has logged in before based on the last login date, we don't set it for first-time users until after the user has provided all the required information and we've updated our repository with the information. For all other users, we set the last login date as soon as the user has logged in.
[HttpGet]
public ActionResult FirstTimeLogin()
{
return View();
}
[HttpPost]
public ActionResult FirstTimeLogin(string displayName)
{
if (string.IsNullOrWhiteSpace(displayName))
{
ModelState.AddModelError("displayName", "Display name is required.");
return View();
}
var userName = _context.CurrentUser.Identity.Name;
_accountRepository.UpdateDisplayName(userName, displayName);
currentUserIsLoggedIn();
return RedirectToAction("Index", "Home");
}
Conclusion
Integrating OpenID with your ASP.NET MVC website feels more complicated than it really is. While we had to write a lot of code, none of it is particularly complicated or unintuitive. DotNetOpenAuth and ASP.NET MVC make it especially easy.