Scott Hanselman

Integrating Mozilla Persona with ASP.NET

January 24, '13 Comments [13] Posted in ASP.NET | ASP.NET MVC | ASP.NET Web API | Open Source
Sponsored By

imageASP.NET and Web Tools 2012.2 is coming soon, but one of the features of ASP.NET that you can use TODAY is support for Google, Twitter, Microsoft and Facebook logins out of the box with ASP.NET templates. I show how OAuth in ASP.NET works in this 5 minute video. We are also continuing to look at ways to make membership easier in coming versions of ASP.NET like including things like this out of the box.

Mozilla has a new identity system called Mozilla Persona that uses browserid technology to let users log in to your site without creating a custom login or username.

I wanted to see how Persona would work in ASP.NET and hacked up a prototype (with some sanity checking from my buddy Hao Kung). There's some comments and some TODOs, but it's a decent proof of concept.

First, I read the Mozilla Persona Developer docs and got their fancy button CSS, then added it all to the ExternalLoginsListPartial view.

The Magic Persona button is very blue

The ProviderName check is there just because all the buttons look the same except the Persona one. A better way, perhaps, would be partial views for each button, or a custom helper.

@foreach (AuthenticationClientData p in Model)
{
if(p.AuthenticationClient.ProviderName == "Persona") //ya, ya, I know.
{
if (!Request.IsAuthenticated) {
<p><a href="#" class="persona-button" id="personasignin"><span>Sign in with your Email</span></a></p>
}
<!-- The CSS for this is in persona-buttons.css and is bundled in in BundleConfig.cs -->
}else{
<button type="submit" name="provider" value="@p.AuthenticationClient.ProviderName" title="Log in using your @p.DisplayName account">@p.DisplayName</button>
}
}

After the login dialog, an AJAX call to do the login locally posts data to my new PersonaController. except it doesn't POST its assert as JSON, but rather as a simple (standard) POST value. That is, just "assertion: longvalue."

function onAssertion(assertion) {
if (assertion) {
$.ajax({ /* <-- This example uses jQuery, but you can use whatever you'd like */
type: 'POST',
url: '/api/persona/login', // This is a URL on your website.
data: { assertion: assertion, },
success: function (res, status, xhr) { window.location.reload(); },
error: function (res, status, xhr) { alert("login failure" + res); }
});
}
else {
alert('Error while performing Browser ID authentication!');
}
}

ASP.NET Web API doesn't grab simple POSTs cleanly by default, preferring more formal payloads. No worries, Rick Strahl solved this problem with this clever SimplePostVariableParameterBinding attribute which allows me to just have string assertion in my method.

Armed with this useful attribute (thanks Rick!) my PersonaController login is then basically:

  • Get the assertion we were given from Persona on the client side
  • Load up a payload with that assertion so we can POST it back to Persona from the Server Side.
  • Cool? We're in, make a local UserProfile if we need to, otherwise use the existing one.
  • Set the FormsAuth cookie, we're good.

Here is the work:

[SimplePostVariableParameterBinding]
public class PersonaController : ApiController
{
// POST api/persona
[HttpPost][ActionName("login")]
public async Task<HttpResponseMessage> Login(string assertion) {
if (assertion == null) {
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
var cookies = Request.Headers.GetCookies();
string token = cookies[0]["__RequestVerificationToken"].Value;
//TODO What is the right thing to do with this?

using (var client = new HttpClient()) {
var content = new FormUrlEncodedContent(
new Dictionary<string, string> {
{ "assertion", assertion },
{ "audience", HttpContext.Current.Request.Url.Host }
//TODO: Can I get this without digging in HttpContext.Current?
}
);
var result = await client.PostAsync("https://verifier.login.persona.org/verify", content);
var stringresult = await result.Content.ReadAsStringAsync();
dynamic jsonresult = JsonConvert.DeserializeObject<dynamic>(stringresult);
if (jsonresult.status == "okay") {
string email = jsonresult.email;

string userName = null;
if (User.Identity.IsAuthenticated) {
userName = User.Identity.Name;
}
else {
userName = OAuthWebSecurity.GetUserName("Persona", email);
if (userName == null) {
userName = email; // TODO: prompt for custom user name?
using (UsersContext db = new UsersContext()) {
//TODO: Should likely be ToLowerInvariant
UserProfile user = db.UserProfiles.FirstOrDefault(u => u.UserName.ToLower() == userName.ToLower());
// Check if user already exists
if (user == null) {
// Insert name into the profile table
db.UserProfiles.Add(new UserProfile { UserName = userName });
db.SaveChanges();
}
}
}
}

OAuthWebSecurity.CreateOrUpdateAccount("Persona", email, userName);

FormsAuthentication.SetAuthCookie(email, false);
return new HttpResponseMessage(HttpStatusCode.OK);
}
}
return new HttpResponseMessage(HttpStatusCode.Forbidden);
}

[HttpPost][ActionName("logout")]
public void Logout() {
WebSecurity.Logout();
}
}

You click Sign in and get the Persona login dialog:

Animation of the Persona login

At this point, you're logged into the site with a real UserProfile, and things with ASP.NET Membership work as always. You can add more external logins (Twitter, Google, etc) or even add a local login after the fact.

You are logged in!

As I said, this isn't ready to go, but feel free to poke around, do pull requests, fork it, or comment on how my code sucks (in a kind and constructive way, because that's how we do things here.)

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. I am a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Friday, January 25, 2013 2:55:57 AM UTC
It amazes me sometimes how easy it is to do this stuff in ASP.NET. But that's the way it should be. Why over complicate things?

Thanks for being one of those people who helps make my life easier.
Richard Rout
Friday, January 25, 2013 12:05:30 PM UTC
Hi, I've used Persona for two little projects using Nancy and NBrowserId

Here it is the module in Nancy

and if anybody want to see it in action
DPath
Rpsls

Juan J. Chiw
Friday, January 25, 2013 3:34:40 PM UTC
Hi Scott,

Thanks for the interesting post.

With respect to TODO # 1, I would

a) Send the hidden field in a header in the ajax call

$.ajax({ headers: { '__RequestVerificationToken': $("input[name='__RequestVerificationToken']").val() },...

b) In the server side validate the header against the cookie with

AntiForgery.Validate(cookieToken, headerToken);

Btw, the name of the cookie is in

System.Web.Helpers.AntiForgeryConfig.CookieName

Of course it would be better to encapsulate this validation in a AuthorizationFilterAttribute (or in two of them, one for MVC and one for WebApi).


Regards,


R. Martinez-Brunet

Roberto Martinez-Brunet
Friday, January 25, 2013 4:04:02 PM UTC
Persona is really cool. I've used it on one of my MVC websites, PrairieAsunder.com, for admin access.

Ultimately, the idea behind BrowserID/Persona is eliminate site-specific passwords and eventually have direct integration in the browsers themselves. In some ways, it's a step towards Jeff Atwood's proposal of making browsers understand identity on the web.

Nifty stuff!
Friday, January 25, 2013 6:07:28 PM UTC
I have a website with asp.net 3.5 and use the out-of-the-box sql server "membership roles and profile." My question is how much work will it be to convert to this new social login on the new framework? Will I be able to easily convert all my existing users?
johndoe
Friday, January 25, 2013 6:18:17 PM UTC
Having programmed mostly in PHP, I have found the ease of OAuth and Persona-like login services absolutely fantastic. Thanks for sharing this!
William Wingler
Friday, January 25, 2013 8:15:43 PM UTC
Roberto - Awesome! thanks for your help. Care to do a pull request? If not, can you add this to the issues here: https://github.com/shanselman/AspNetPersonaId/issues so you get credit?

Johndoe - With 3.5? You'll need to roll your own. If you move to 4.5, you should be able to keep using the same tables, add the two new ones for external logins, then users would have the ability to login with their local logins and then associate an optional external. Good idea for a blog post!
Friday, January 25, 2013 9:07:20 PM UTC
but it desont seem to be browser compatible , I opened one of integrated websites in ID and didnt ask me for persona login , so it should be one of the options not the only option
Sam
Friday, January 25, 2013 9:07:43 PM UTC
** IE
Sam
Thursday, March 07, 2013 1:52:56 AM UTC
Scott,

Do you have an ETA on when this project will be completed?
Victor
Thursday, March 07, 2013 3:25:18 AM UTC
Victor, it's just a Proof of Concept right now. Feel free to do with it as you like.
Monday, March 18, 2013 7:47:01 AM UTC
Is there any ASP.NET (only ASP.NET website) SPA templates to develop applications without MVC, could you please provide more information on this.
Gov
Wednesday, May 08, 2013 7:44:36 AM UTC
Persona is almost like Facebook connect, it removes the additional needs of username and password to which we are generally using. Never thought of Integration of Persona with ASP.NET but looking to what you have tried, result seems much closer.
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.