How OAuthSecurity to obtain emails for different oauth clients, but Microsoft Client doesn’t return email, it didn’t include scope “wl.emails”

I have been playing with MVC 4, SimpleMembership, WebSecurity and OAuthWebSecurity for a while now. I can see the idea of OAuthWebsecurity is a wrapper around DotNetOpenAuth. It registers the clients in AuthConfig.cs file. That really works and helps me reduce heaps lines of code.

But soon enough,

1. I find only Google client returns an email as a username. I mean email is still quite important for the newsletters or system emails etc.

2. For Twitter, they don’t provide email via OAuth or any API, which is a shame. But I don’t complain. (Maybe they have changed without my awareness). So we don’t do anything with it.

3. For Facebook, with DotNetOpenAuth and OAuth2, it actually includes the scope “email” and returns email in the “ExtraData” dictionary.

So in the ExternalLoginCallback() method, you can find this line:

            AuthenticationResult result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));

If you query result.ExtraData[“username”], that contains the user’s email in it.

4. For Microsoft, it is a nightmare, I find DotNetOpenAuth didn’t even include the scope “wl.emails” in their request at all. I am disappointed, but it is not the end of the world.

I am trying to create a Custom Authentication Client, to retrieve Microsoft emails.

First, create a class MicrosoftScopedClient and implement IAuthenticationClient interface. You must implement two methods of that interface.

    public class MicrosoftScopedClient : IAuthenticationClient
    {

        public void RequestAuthentication(HttpContextBase context, Uri returnUrl)
        {
  
        }

        public AuthenticationResult VerifyAuthentication(HttpContextBase context)
        {
        }
    }

The next step is to build the authentication url in method “RequestAuthentication()”,

public void RequestAuthentication(HttpContextBase context, Uri returnUrl)
        {
            string url = baseUrl + "?client_id=" + clientId + "&redirect_uri=" + HttpUtility.UrlEncode(returnUrl.ToString()) + "&scope=" + HttpUtility.UrlEncode(scope) + "&response_type=code";
            context.Response.Redirect(url);
        }

Then I build VerifyAuthentication() method to receive the authentication code and send requests to obtain the access_token, and then use the access_token to request for the profiles.

public AuthenticationResult VerifyAuthentication(HttpContextBase context)
        {
            string code = context.Request.QueryString["code"];

            string rawUrl = context.Request.Url.ToString();
            //From this we need to remove code portion
            rawUrl = Regex.Replace(rawUrl, "&code=[^&]*", "");

            IDictionary userData = GetUserData(code, rawUrl);

            if (userData == null)
                return new AuthenticationResult(false, ProviderName, null, null, null);

            string id = userData["id"];
            string username = userData["email"];
            userData.Remove("id");
            userData.Remove("email");

            AuthenticationResult result = new AuthenticationResult(true, ProviderName, id, username, userData);
            return result;
        }

After I have done the work of building the MicrosoftScopedClient, I need to register it in the AuthConfig.cs, now we can feel free to pass any scopes there =)

            OAuthWebSecurity.RegisterClient(new MicrosoftScopedClient(ConfigurationManager.AppSettings["Microsoft.ClientId"].ToString(),
                ConfigurationManager.AppSettings["Microsoft.Secret"].ToString(),
                "wl.basic wl.emails"
                )
                , "Microsoft", null);

Below is a full copy of the MicrosoftScopedClient,

using DotNetOpenAuth.AspNet;
using DotNetOpenAuth.AspNet.Clients;
using DotNetOpenAuth.Messaging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;

namespace MicrosoftClient.Filters
{
    public class MicrosoftScopedClient : IAuthenticationClient
    {
        private string clientId;
        private string clientSecret;
        private string scope;

        private const string baseUrl = "https://login.live.com/oauth20_authorize.srf";
        private const string tokenUrl = "https://login.live.com/oauth20_token.srf";

        public MicrosoftScopedClient(string clientId, string clientSecret, string scope)
        {
            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.scope = scope;
        }

        public string ProviderName
        {
            get { return "Microsoft"; }
        }

        public void RequestAuthentication(HttpContextBase context, Uri returnUrl)
        {
            string url = baseUrl + "?client_id=" + clientId + "&redirect_uri=" + HttpUtility.UrlEncode(returnUrl.ToString()) + "&scope=" + HttpUtility.UrlEncode(scope) + "&response_type=code";
            context.Response.Redirect(url);
        }

        public AuthenticationResult VerifyAuthentication(HttpContextBase context)
        {
            string code = context.Request.QueryString["code"];

            string rawUrl = context.Request.Url.ToString();
            //From this we need to remove code portion
            rawUrl = Regex.Replace(rawUrl, "&code=[^&]*", "");

            IDictionary userData = GetUserData(code, rawUrl);

            if (userData == null)
                return new AuthenticationResult(false, ProviderName, null, null, null);

            string id = userData["id"];
            string username = userData["email"];
            userData.Remove("id");
            userData.Remove("email");

            AuthenticationResult result = new AuthenticationResult(true, ProviderName, id, username, userData);
            return result;
        }

        private IDictionary GetUserData(string accessCode, string redirectURI)
        {
            string token = QueryAccessToken(redirectURI, accessCode);
            if (token == null || token == "")
            {
                return null;
            } 
            var userData = GetUserData(token);
            return userData;
        }

        private IDictionary GetUserData(string accessToken)
        {
            ExtendedMicrosoftClientUserData graph;
            var request =
                WebRequest.Create(
                    "https://apis.live.net/v5.0/me?access_token=" + EscapeUriDataStringRfc3986(accessToken));
            using (var response = request.GetResponse())
            {
                using (var responseStream = response.GetResponseStream())
                {
                    using (StreamReader sr = new StreamReader(responseStream))
                    {
                        string data = sr.ReadToEnd();
                        graph = JsonConvert.DeserializeObject(data);
                    }
                }
            }

            var userData = new Dictionary();
            userData.Add("id", graph.Id);
            userData.Add("username", graph.Name);
            userData.Add("name", graph.Name);
            userData.Add("link", graph.Link == null ? null : graph.Link.AbsoluteUri);
            userData.Add("gender", graph.Gender);
            userData.Add("firstname", graph.FirstName);
            userData.Add("lastname", graph.LastName);
            userData.Add("email", graph.Emails.Preferred);
            return userData;
        }

        private string QueryAccessToken(string returnUrl, string authorizationCode)
        {
            var entity =
                CreateQueryString(
                    new Dictionary {
						{ "client_id", this.clientId },
						{ "redirect_uri", returnUrl },
						{ "client_secret", this.clientSecret},
						{ "code", authorizationCode },
						{ "grant_type", "authorization_code" },
					});

            WebRequest tokenRequest = WebRequest.Create(tokenUrl);
            tokenRequest.ContentType = "application/x-www-form-urlencoded";
            tokenRequest.ContentLength = entity.Length;
            tokenRequest.Method = "POST";

            using (Stream requestStream = tokenRequest.GetRequestStream())
            {
                var writer = new StreamWriter(requestStream);
                writer.Write(entity);
                writer.Flush();
            }

            HttpWebResponse tokenResponse = (HttpWebResponse)tokenRequest.GetResponse();
            if (tokenResponse.StatusCode == HttpStatusCode.OK)
            {
                using (Stream responseStream = tokenResponse.GetResponseStream())
                {
                    using (StreamReader sr = new StreamReader(responseStream))
                    {
                        string data = sr.ReadToEnd();
                        var tokenData = JsonConvert.DeserializeObject(data);
                        if (tokenData != null)
                        {
                            return tokenData.AccessToken;
                        }
                    }
                }
            }

            return null;
        }

        private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" };
        private static string EscapeUriDataStringRfc3986(string value)
        {
            StringBuilder escaped = new StringBuilder(Uri.EscapeDataString(value));

            // Upgrade the escaping to RFC 3986, if necessary.
            for (int i = 0; i < UriRfc3986CharsToEscape.Length; i++)
            {
                escaped.Replace(UriRfc3986CharsToEscape[i], Uri.HexEscape(UriRfc3986CharsToEscape[i][0]));
            }

            // Return the fully-RFC3986-escaped string.
            return escaped.ToString();
        }

        private static string CreateQueryString(IEnumerable<KeyValuePair> args)
        {
            if (!args.Any())
            {
                return string.Empty;
            }
            StringBuilder sb = new StringBuilder(args.Count() * 10);

            foreach (var p in args)
            {
                sb.Append(EscapeUriDataStringRfc3986(p.Key));
                sb.Append('=');
                sb.Append(EscapeUriDataStringRfc3986(p.Value));
                sb.Append('&');
            }
            sb.Length--; // remove trailing &

            return sb.ToString();
        }

        protected class ExtendedMicrosoftClientUserData
        {
            public string FirstName { get; set; }
            public string Gender { get; set; }
            public string Id { get; set; }
            public string LastName { get; set; }
            public Uri Link { get; set; }
            public string Name { get; set; }
            public Emails Emails { get; set; }
        }

        protected class Emails
        {
            public string Preferred { get; set; }
            public string Account { get; set; }
            public string Personal { get; set; }
            public string Business { get; set; }
        }
    }
}
Advertisements

A JQuery plugin for OpenID selector & DotNetOpenAuth integration in MVC

JQuery OpenId selector can be downloaded here http://code.google.com/p/openid-selector/

DotNetOpenAuth can be downloaded here http://www.dotnetopenauth.net/ or install using Nuget  http://nuget.org/packages/DotNetOpenAuth/3.4.7.11121

PM> Install-Package DotNetOpenAuth -Version 3.4.7.11121

Here is what the JQuery openId selector looks, and there are two files you can configure: openId-en.js & openid-jquery.js

OpenID selector

To modify the UI, in openid-en.js, change the variable of “providers_large” and “providers_small” to make your own look and feel.

To modify the settings of this plugin, such as image path, cookie name, etc. Open openid-jquery.js, it is in the openid definition section.

openid = {
	version : '1.3', // version constant
	demo : false,
	demo_text : null,
	cookie_expires : 6 * 30, // 6 months.
	cookie_name : 'openid_provider',
	cookie_path : '/',

	img_path : '../openid/images/',
	locale : null, // is set in openid-.js
	sprite : null, // usually equals to locale, is set in
	// openid-.js
	signin_text : null, // text on submit button on the form
	all_small : false, // output large providers w/ small icons
	no_sprite : false, // don't use sprite image
	image_title : '{provider}', // for image title

	input_id : null,
	provider_url : null,
	provider_id : null,

In the MVC View, you need to have


    $(document).ready(function () {
        openid.init('openid_identifier');
    });

    function facebook_click() {
        alert("wwaaa");
    }


@using (Html.BeginForm("Login", "Account", new {ReturnUrl = Request.QueryString["ReturnUrl"] },FormMethod.Post, new { id = "openid_form" }))
{     <input type="hidden" name="action" value="verify" />
	<fieldset>
		<legend>Sign-in or Create New Account</legend>
		<div id="openid_choice">
			<p>Please click your account provider:</p>
			<div id="openid_btns"></div>
		</div>
		<div id="openid_input_area">
			<input id="openid_identifier" name="openid_identifier" type="text" value="http://" />
			<input id="openid_submit" type="submit" value="Sign-In"/>
		</div>
		<noscript>
			<p>OpenID is service that allows you to log-on to many different websites using a single indentity.
			Find out <a href="http://openid.net/what/">more about OpenID</a> and <a href="http://openid.net/get/">how to get an OpenID enabled account</a>.</p>
		</noscript>
	</fieldset>
}

In the controller, we need to have Login & Register action methods using DotNetOpenAuth to implement the authentication process.

[AcceptVerbs(HttpVerbs.Post | HttpVerbs.Get), ValidateInput(false)]
        public ActionResult Login(string openid_identifier, string returnUrl)
        {
            var response = _openId.GetResponse();
            if (response == null)
            {
                if (string.IsNullOrEmpty(openid_identifier))
                    return View();
                return SendRequestToOpenIdProvider(openid_identifier);
            }
            else 
            {
                switch (response.Status)
                {
                    case AuthenticationStatus.Authenticated:
                        string identifier = response.ClaimedIdentifier;

                        var user = _userRepo.GetUser(identifier);
                        if (user != null)
                        {
                            IssueFormsAuthenticationTicket(user);
                            if (string.IsNullOrEmpty(returnUrl))
                                return RedirectToAction("Index", "Home");
                            else
                                return Redirect(returnUrl);
                        }
                        else
                        {
                            var registrationModel = new RegistrationViewModel(identifier)
                            {
                                ReturnUrl = returnUrl
                            };
                            var simpleReg = response.GetExtension();
                            if (simpleReg != null)
                            {
                                if (!string.IsNullOrEmpty(simpleReg.Email))
                                    registrationModel.Email = simpleReg.Email;

                                if (!string.IsNullOrEmpty(simpleReg.FullName))
                                    registrationModel.FullName = simpleReg.FullName;

                                if (!string.IsNullOrEmpty(simpleReg.Nickname))
                                    registrationModel.Username = simpleReg.Nickname;
                            }
                            return View("Register", registrationModel);
                        }
                    case AuthenticationStatus.Canceled:
                        ModelState.AddModelError("openid_identifier", "Authetication canceled");
                        break;
                    case AuthenticationStatus.Failed:
                        ModelState.AddModelError("openid_identifier", "Authetication failed");
                        break;
                }
            }
            return View();
        }

        public ActionResult Register()
        {
            return RedirectToAction("Login");
        }

        [HttpPost]
        public ActionResult Register(User user, string identifier, string returnUrl)
        {
            //The registration form has been submitted
            try
            {
                if (ModelState.IsValid)
                {
                    var feed = _feedRepo.Create(new Feed(){
                        Id = Guid.NewGuid(),
                        Name = "Default"
                    });
                    user.OpenId = identifier;
                    user.ApiKey = Guid.NewGuid();
                    user.FeedUsers.Add(new FeedUser(){
                        IsOwner = true,
                        FeedId = feed.Id
                    });
                    _userRepo.Create(user);

                    // Now let's login out user to out application
                    IssueFormsAuthenticationTicket(user);

                    // We're done, let's get back to where we started from.
                    if (string.IsNullOrEmpty(returnUrl))
                        return RedirectToAction("Index", "Home");
                    else
                        return Redirect(returnUrl);
                }

                var registrationModel = new RegistrationViewModel(identifier)
                {
                    Username = user.Username,
                    Email = user.Email,
                    FullName = user.FullName,
                    ReturnUrl = returnUrl
                };

                return View(registrationModel);
            }
            catch
            {
                var registrationModel = new RegistrationViewModel(identifier)
                {
                    Username = user.Username,
                    Email = user.Email,
                    FullName = user.FullName,
                    ReturnUrl = returnUrl
                };

                return View(registrationModel);
            }
        }

A complete example can be downloaded at https://github.com/gligoran/mvcopenid

Integrate openID, oAuth to your site using JanRain

Integrate openID to your site using JanRain. It is definitely a time saver. My site is built in mvc3 and here are the steps how to do it.

1. You need to setup the widget and copy the generated js code to your site and update the “tokenUrl”.

<script type="text/javascript">(function() {    if (typeof window.janrain !== 'object') window.janrain = {};    window.janrain.settings = {};

    janrain.settings.tokenUrl = '__REPLACE_WITH_YOUR_TOKEN_URL__';

    function isReady() { janrain.ready = true; };    if (document.addEventListener) {      document.addEventListener("DOMContentLoaded", isReady, false);    } else {      window.attachEvent('onload', isReady);    }

    var e = document.createElement('script');    e.type = 'text/javascript';    e.id = 'janrainAuthWidget';

    if (document.location.protocol === 'https:') {      e.src = 'https://rpxnow.com/js/lib/itsharp/engage.js';    } else {      e.src = 'http://widget-cdn.rpxnow.com/js/lib/itsharp/engage.js';    }

    var s = document.getElementsByTagName('script')[0];    s.parentNode.insertBefore(e, s);})();</script>

2. Add your site url to the whitelist, here I put “itsharp.com.au”

itsharp.com.au JanRain Integration
itsharp.com.au JanRain Integration

3. I created a JanRainHelper to make the “auth_info” API call, and a class “JanRainData” for the de-serialization of the response data. (please forgive the naming conversions, I just copied all the return data fields from JanRain)

Don’t forget to copy your own “apiKey” and paste in the code below.

public class JanRainHelper{public static string AuthInfo(string token){string apiKey = "***************************";

WebRequest req = WebRequest.Create("https://rpxnow.com/api/v2/auth_info");

((HttpWebRequest)req).UserAgent = "JanRainProServ/1.0(Automated)";req.Method = "POST";

string postData = "token=";postData += token;postData += "&apiKey=";postData += apiKey;

byte[] byteArray = Encoding.UTF8.GetBytes(postData);// Set the ContentType property of the WebRequest.req.ContentType = "application/x-www-form-urlencoded";// Set the ContentLength property of the WebRequest.req.ContentLength = byteArray.Length;// Get the request stream.Stream dataStream = req.GetRequestStream();// Write the data to the request stream.dataStream.Write(byteArray, 0, byteArray.Length);// Close the Stream object.dataStream.Close();

// Send the data to the webserverHttpWebResponse rsp = (HttpWebResponse)req.GetResponse();string content = "";using (StreamReader sr = new StreamReader(rsp.GetResponseStream())){content = sr.ReadToEnd();}return content;}}

 

[Serializable]public class JanRainData{public Profile profile { get; set; }public string stat { get; set; }}

[Serializable]public class Profile{public string identifier { get; set; }

public string providerName { get; set; }

public string displayName { get; set; }

public Name name { get; set; }

public string gender { get; set; }

public bool? genderBool{get{if (string.IsNullOrEmpty(gender))return null;else{if (gender.Equals("male")){return true;}else if (gender.Equals("female")){return false;}else{return null;}}}}

public string birthday { get; set; }

public DateTime? birthdayDateTime{get{DateTime dt;if (!string.IsNullOrEmpty(birthday) && DateTime.TryParse(birthday, out dt)){return dt;}else{return null;}}}

public string email { get; set; }

public string verifiedEmail { get; set; }

public string phoneNumber { get; set; }

public string photo { get; set; }

public Address address { get; set; }}

[Serializable]public class Name{public string givenName { get; set; }public string familyName { get; set; }public string formatted { get; set; }}

[Serializable]public class Address{public string formatted { get; set; }public string streetAddress { get; set; }public string locality { get; set; }public string region { get; set; }public string postalCode { get; set; }public string country { get; set; }}

4. Create an Action method for the “tokenUrl”, JanRain post back with a token, and you will need to use that token to call the auth_info API to retrieve the user info.

I am using the JavascriptSerializer to deserialize the response to a JanRainData object. Then you can do whatever you need to do, such as create a user in your database and authentication cookie, etc.


public ActionResult JanRainPostBack(string token){if (!string.IsNullOrEmpty(token)){string content = JanRainHelper.AuthInfo(token);JavaScriptSerializer s = new JavaScriptSerializer();

var data = s.Deserialize<JanRainData>(content);if (data.stat.Equals("ok") && !string.IsNullOrEmpty(data.profile.providerName) &&       !string.IsNullOrEmpty(data.profile.identifier)){var user = new User(){Email = data.profile.email};_repository.Save(user);return View();}elsethrow new Exception("response failed");}elsethrow new Exception("token is invalid");}

5. It is all done now. Please be aware you might only retrieve limited user information from different openID providers. Eg, twitter will not return an email address which I think it’s pretty lame. If email is required in your dbo.User table, you will need to enforce users to complete the registration.