Support For OpenID

From Open Source@Seneca

Jump to: navigation, search

Contents

Project Description

To create an extension that will manage multiple OP's and act as a liaison between the user and their OP(s).

Project Leader(s)

Amit Gundu aka Cozby on IRC

What is OpenID?

OpenID is a method of verifying who you are via URI. Its main objective is to eliminate the use of multiple usernames across different sites. Unifying identity on the Net.

How do I get an OpenID?

In-fact you might already have an OpenID, if you belong to wordpress.com, livejournal.com and few others you're already ahead of the game. For more information on obtaining an OpenID check out OpenID.net

Where can I use it?

You can use your OpenID at any site that supports OpenID authentication.

Project Contributors

Please add your name here if you would like to get involved.

Releases

v0.1

Skipped

v0.2

OK, where to start, v0.1 was all about gathering information and breaking down the process that occurs when logging into a OpenID supported site. This .2 release was to circumvent the current process of logging into your OP upon redirection (and have code to back it up!)

Login Flow

The following is the GET/POST trail when logging into a OpenID site. Ignore the 1 & 2, those are just debug outputs.

2 - Topic:http-on-modify-requestURI: Params: http://toodledo.com/openid.php
 Req.Method:POST
Status Code:0

1 -Topic:http-on-modify-requestURI: Params: http://www.myopenid.com/server?openid.return_to=http%3A%2F%2Fwww.toodledo.com%2Fopenid.php
openid.mode=checkid_setup
openid.identity=http%3A%2F%2Fwugunz.myopenid.com
openid.trust_root=http%3A%2F%2Fwww.toodledo.com%2F
openid.sreg.required=email,fullname
openid.sreg.optional=timezone

 Req.Method:GET
Status Code:0

2 - Topic:http-on-modify-requestURI: Params: https://www.myopenid.com/trust_submit
 Req.Method:POST
Status Code:0

1 -Topic:http-on-modify-requestURI: Params: http://www.toodledo.com/openid.php?openid.assoc_handle=%7BHMAC-SHA1%7D%7B474621b0%7D%7BlP7t3w%3D%3D%7D
openid.identity=http%3A%2F%2Fwugunz.myopenid.com
openid.mode=id_res
openid.op_endpoint=http%3A%2F%2Fwww.myopenid.com%2Fserver
openid.response_nonce=2007-11-23T00%3A41%3A20ZJJZFTI
openid.return_to=http%3A%2F%2Fwww.toodledo.com%2Fopenid.php
openid.sig=MleGfFdfSo5ijju4GzSb%2BT3hn0E%3D
openid.signed=assoc_handle%2Cidentity%2Cmode%2Cop_endpoint%2Cresponse_nonce%2Creturn_to%2Csigned%2Csreg.email%2Csreg.fullname
openid.sreg.email=cozbyx%40gmail.com
openid.sreg.fullname=Wu+Gunz
 Req.Method:GET
Status Code:0

From the above trail you can determine what the site requires from my OP upon login.
The line openid.sreg.required=email,fullname ask my OP for both my email and fullname.
At this point your browser would be redirected to your OP for permission to release such informatiton.
I can't for some reason link to my screenshot on blogger, so see the following
http://bp2.blogger.com/_kbJjIwwO7yU/RzM5EhY3zNI/AAAAAAAAA2w/4zDquVLrfEw/s1600-h/openidscreenshot.JPG to see what you'd be prompted with.

Upon accepting this request and I'm authenticated I'm sent to openid.return_to=http%3A%2F%2Fwww.toodledo.com%2Fopenid.php

Getting around the OP login

In order for you to bypass logging into your OP every time you're redirected I established a login session with your OP (in this case myopenid.com) on browser startup. This was done with a XMLHTTPRequest (Ajax POST) to my provider. I send both my username and password along with two token ID's the site generates on random. I believe the token ID's can be null but I haven't tested that yet. For More info on using AJAX calls in your code consult http://developer.mozilla.org/en/docs/nsIXMLHttpRequest and http://developer.mozilla.org/en/docs/AJAX:Getting_Started .

Login code to establish session

At the moment the username and password are kept in clear text if you were to use this code. I intend on tying the password manager in for the handling of your username password in future releases.


function makeRequest(url) {
    LOG("Login Request made");
    var httpRequest;

    if (window.XMLHttpRequest) { // Mozilla, Safari, ...
        httpRequest = new XMLHttpRequest();
        if (httpRequest.overrideMimeType) {
            httpRequest.overrideMimeType('text/xml');
            // See note below about this line
        }
    } 

    if (!httpRequest) {
        alert('Giving up :( Cannot create an XMLHTTP instance');
        return false;
    }
    httpRequest.onreadystatechange = function() { alertContents(httpRequest); };
    httpRequest.open('POST', url, true);
    httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    //POST the following values to the URL submitted
    httpRequest.send('user_name=SOMEUSERNAME&password=SOMEPASSWORD&tid=RANDOMTOKEN&token=RANDOMTOKEN');
    
    
    openid.init();
}

function alertContents(httpRequest) {
    //ready state values 1 = loading, 2 = loaded, 3 = interactive, 4 = complete
    if (httpRequest.readyState == 4) {
	//200 = OK response
        if (httpRequest.status == 200) {
	    window.dump(httpRequest.responseText);
        } else {
            alert('There was a problem with the request.');
        }
    }
}
Process observation code

This code is used to check the request/response trail. I also use this code to filter for OP login requests.

//For dumping msg's to console 
function LOG(msg) {
  var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
                                 .getService(Components.interfaces.nsIConsoleService);
  consoleService.logStringMessage(msg);
}

var openid = 
{
	init: function(){
            
		var httpRequestObserver =
		{
		  observe: function(subject, topic, data)
		  {
			var sreg
			var uri; 
			var reqMethod;
			var status = 0;
		   if (topic == "http-on-modify-request") {
		   	var httpChannel = subject.QueryInterface(Components.interfaces.nsIHttpChannel);
				uri = httpChannel.QueryInterface(Components.interfaces.nsIChannel).URI.spec;
				try{
					sreg = httpChannel.getRequestHeader("openid.sreg.required")
				}catch(e){
					sreg = "nope";
				}
			  	reqMethod = httpChannel.requestMethod;
            try{
	        		status = httpChannel.responseStatus;
            }catch(e){
            	status = 0;
            }
 		   }else if(topic == "http-on-examine-response"){
				var httpChannel = subject.QueryInterface(Components.interfaces.nsIHttpChannel);
				try{
					status = httpChannel.responseStatus;
				}catch(e){ window.dump('error retrieving status! '+ e + '\n')}
		    }
		
 		    var params = " Params: "
        	 var rawparams = uri.split(/&/);
          for (var i = 0; i < rawparams.length; i++) {
        		params += rawparams[i] + "\n";
          }
			//filter for openid req only. 
          if(uri.indexOf("openid.mode")!= -1){
          	LOG('1 -Topic:' + topic + 'URI:' + params + ' Req.Method:' + reqMethod + '\n' + 'Status Code:' + status + '\n');				
				LOG(sreg);
	
          }
          if(reqMethod == "POST"){
			 	LOG('2 - Topic:' + topic + 'URI:' + params + ' Req.Method:' + reqMethod + '\n' + 'Status Code:' + status + '\n');				
				LOG(sreg);
		    }
		
			},
		
		  get observerService(){
		    return Components.classes["@mozilla.org/observer-service;1"]
		                     .getService(Components.interfaces.nsIObserverService);
		  },

		  register: function()
		  {
		    this.observerService.addObserver(this, "http-on-modify-request", false);
		  },

		  unregister: function()
		  {
		    this.observerService.removeObserver(this, "http-on-modify-request");
		  }
		};
    
		httpRequestObserver.register();
	}
};


Pre Conditions (maybe this should be first)

In order for you to try this you'll need to have an account at myopenid.com, as that is the only OP site I'm currently supporting. In the future intend to have support for multiple OPs.

v0.3

So here we are now, .3 hot off my laptop. Not a whole lot has changed, its still very how do I say ugly in terms of code. However hurdles have been hopped and hills conquered, the main functionality has been achieved! With this version installed you can now log directly into your favourite OID supported site and have no ugly redirects and no more prompting for the release of persona information. Sweet. Its not all blue skies though there's much work to be done, its far from elegant and its riddled with bugs.

Avoiding The Redirect

One of my goals was to get around the redirect that takes place when you login to an OpenID supported site. I was able to get around this issue by filtering requests with openid.checkid_setup and then calling a request.cancel(Components.results.NS_BINDING_ABORTED); on that request. I then would copy the requests params and construct my own request which the browser would send.

var request = subject.QueryInterface(Components.interfaces.nsIRequest);
                         request.cancel(Components.results.NS_BINDING_ABORTED);
                         LOG("Request Aborted");
                         //Now that the req. has been aborted to avoid the redirect, lets copy the query string params
                         //and send them ourselves. Once thats been done, we'll need to redirect the browser to the
                         //return_url.
                         
                         //split postdata from host
                         var postData = uri.split(/\?/);
                         LOG(postData[1]);
                         //get the return to URL
                         LOG(rawparams[0]);
                         returnURL = rawparams[0].split(/=/);
                         LOG(returnURL[1]);
                         authRequest('http://www.myopenid.com/server?', postData[1]);

Notice the returnURL variable, that holds the openid.return_to URL that you are forwarded to once you've been authenticated. Its pretty ugly stuff, but works.

Parting with Persona Info

In the .2 release I show you a screen shot of what happens when you're redirected to your OP for authentication. You're prompted to release whatever information the consumer site requests. This could be your email, first name, last name, etc. I got around this persona prompting by hard coding 'Allow Once' anytime a request comes in. I know, this isn't exactly ideal I intend on prompting the user in chrome for this type of information in later releases. There is a lot of hard coded ugliness in the code, there are a few variables that I need to capture from myopenid.com in order to make this more dynamic. One such variable is PERSONA ID. PERSONA ID is attached the profile information you have setup on myopenid.com (you can have multiple personas/profiles).
The following submits the 'Allow Once' information aka OK's the release of my data to the consumer site.

httpRequest.send("request_id="+reqID+"&persona_id="+PERSONA_ID+"&allow_once=Allow%20Once&token="+token);

Once the request to submit data completes, I then redirect the user to the openid.return_to URL. So yes, I guess you could say there is still a redirect of sorts going on, but not to your OP for authentication/prompting. Instead its redirects you straight through to your consumer sites welcome page or whatever page they have setup after you login.

/************************
 *REDIRCT BROWSER CODE
 ***********************/
function redirectBrowser(){
    LOG("RE-DIRECTS FOR 500 Alex");
    var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
    var bw = wm.getMostRecentWindow("navigator:browser");
    //need to fix up the return_to parsing, right now its very basic and only works on a couple of sites
    LOG(Url.decode(returnURL[1]));
    bw.content.location.assign(Url.decode(returnURL[1]));    
}

I should also inform you that the return_to parsing functionality is very rudimentary and only works on a couple of tested sites. This shouldn't take long to fix, just some regex tweaking.

The Code

//For dumping msg's to console 
function LOG(msg) {
  var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
                                 .getService(Components.interfaces.nsIConsoleService);
  consoleService.logStringMessage(msg);
}
//this is just a test, I'll need to clean this up properly later
//request_id is generated when an authentication req. is made to my OP.
//my OP uses this request_id when submitting to trust_submit
var request_id;
var token; 

var xmldoc;
const PERSONA_ID = 154363;
var returnURL = null;
var openid = 
{
	init: function(){
                //this is pretty dirty, I'll have to remove this object outside later on
		var httpRequestObserver =
		{
		  observe: function(subject, topic, data)
		  {
			var uri; 
			var reqMethod;
			var status = 0;
                        var originalUri;
                        
		    if (topic == "http-on-modify-request") {
                        //obtain current channel here
		      var httpChannel = subject.QueryInterface(Components.interfaces.nsIHttpChannel);
			  uri = httpChannel.QueryInterface(Components.interfaces.nsIChannel).URI.spec;
                          originalUri = httpChannel.originalURI;
			  reqMethod = httpChannel.requestMethod;
                          try{
                            status = httpChannel.responseStatus;
                          }catch(e){
                            status = 0;
                          }
                        //lets try some redirect capturing action here
                        //*note this does infact capture redirects but upon closing the browser causes
                        //Firefox to crash - don't know why, and don't care ATM. Not sure if I'll even use this.
                        /*
                        try{
                            var listener = new StreamListener();  
                            httpChannel.notificationCallbacks = listener ;
                        }catch(e){
                            alert(e);
                        }*/

		    }else if(topic == "http-on-examine-response"){
				var httpChannel = subject.QueryInterface(Components.interfaces.nsIHttpChannel);
				try{
					status = httpChannel.responseStatus;
				}catch(e){ window.dump('error retrieving status! '+ e + '\n')}
		    }
                    
                    //The originalURI doesn't spec out as I thought it would. It did not catch the redirect URL with
                    //request_id in the query string. Ignore this for now, searching for request_id is the only way so far.
                    //LOG("Original URI:"+originalUri);
                    
                    //Too verbose
                    //LOG("URI"+uri);
 		    var params = " Params: "
                    var rawparams = uri.split(/&/);
                    for (var i = 0; i < rawparams.length; i++) {
        		params += rawparams[i] + "\n";
                    }
                    //try and get the request_id before redirect takes place
                    //this has changed - noticed this dec 07 2007, they no longer use request_id, but tid= 
                    if(uri.indexOf("tid") != -1){
                        request_id = params.split("=");
                        LOG(request_id[1]);
                        //trustRequest('http://www.myopenid.com/trust_submit?');
                    }
                    //filter for openid req only. 
                    if(uri.indexOf("openid.mode=checkid_setup")!= -1){
                        LOG('1 - Topic:' + topic + 'URI:' + params + ' Req.Method:' + reqMethod + '\n' + 'Status Code:' + status + '\n');
                        //enter code to stop request ,check for openid.sreg.required
                        var request = subject.QueryInterface(Components.interfaces.nsIRequest);
                         request.cancel(Components.results.NS_BINDING_ABORTED);
                         LOG("Request Aborted");
                         //Now that the req. has been aborted to avoid the redirect, lets copy the query string params
                         //and send them ourselves. Once thats been done, we'll need to redirect the browser to the
                         //return_url.
                         
                         //split postdata from host
                         var postData = uri.split(/\?/);
                         LOG(postData[1]);
                         //get the return to URL
                         LOG(rawparams[0]);
                         returnURL = rawparams[0].split(/=/);
                         LOG(returnURL[1]);
                         authRequest('http://www.myopenid.com/server?', postData[1]);                         
                    }
                    if(reqMethod == "POST"){
			 LOG('2 -Topic:' + topic + 'URI:' + params + ' Req.Method:' + reqMethod + '\n' + 'Status Code:' + status + '\n');				
		    }
		},
		
		  get observerService(){
		    return Components.classes["@mozilla.org/observer-service;1"]
		                     .getService(Components.interfaces.nsIObserverService);
		  },

		  register: function()
		  {
		    this.observerService.addObserver(this, "http-on-modify-request", false);
		  },

		  unregister: function()
		  {
		    this.observerService.removeObserver(this, "http-on-modify-request");
		  }
                  
		};
    
		httpRequestObserver.register();
	}
};


/************************************
 * Login request to OpenID Provider *
 ************************************/

function makeRequest(url) {
    LOG("Login Request made");
    var httpRequest;

    if (window.XMLHttpRequest) { // Mozilla, Safari, ...
        httpRequest = new XMLHttpRequest();
        if (httpRequest.overrideMimeType) {
            httpRequest.overrideMimeType('text/xml');
            // See note below about this line
        }
    } 

    if (!httpRequest) {
        alert('Giving up :( Cannot create an XMLHTTP instance');
        return false;
    }
    httpRequest.onreadystatechange = function() { alertContents(httpRequest, "login"); };
    httpRequest.open('POST', url, true);
    httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    //POST the following values to the URL submitted
    httpRequest.send('user_name=USERNAME&password=PASSWORD&tid=971c5fd1&token=d11880466933caba543bb0fbfbf9cff8000000000002f278');
    
    openid.init();
}

function alertContents(httpRequest, type) {
    //ready state values 1 = loading, 2 = loaded, 3 = interactive, 4 = complete
    if (httpRequest.readyState == 4) {
	//200 = OK response
        if (httpRequest.status == 200) {
	  //need to change the type from trust submit to check_id request
	  //trust_submit is done after check_id is authed
            if(type == "checkid_setup"){
                //this is the actual HTML page thats returned
                //LOG(httpRequest.responseText);
                //var dom = httpRequest.responseXML;
		//LOG(xmldoc);
                //use some XPATH magic to get he token value
		//var nsResolver = xmldoc.createNSResolver( xmldoc.ownerDocument == null ? xmldoc.documentElement : xmldoc.ownerDocument.documentElement);
		//token = xmldoc.evaluate('//input[@token]', xmldoc, nsResolver, XPathResult.STRING_TYPE, null);
		var html = httpRequest.responseText;
		window.dump(html);
		var re = new RegExp(".*name=\"token\" value=\"\\w+\"", "gi" );
		var parts = html.match(re);
		//window.dump(parts.length);
		var line  = parts[0].split(" ");
		var tk = line[3];
		var start= tk.indexOf('"');
		var end = tk.lastIndexOf('"');
		//window.dump(tk);
		//window.dump(start + " " + end);
		token = tk.slice(start+1, end);
		//window.dump(token);
                //at this point say its OK and release requested details
		trustRequest('https://www.myopenid.com/trust_submit?');
            }else if( type == "trust_submit"){
                //redirect user to Consumer Site main page.
                redirectBrowser();
            }
        } else {
            alert('There was a problem with the request. '+httpRequest.status);
        }
    }
}


function authRequest(url,postData){
    LOG(postData);
    if (window.XMLHttpRequest) { // Mozilla, Safari, ...
        httpRequest = new XMLHttpRequest();
        if (httpRequest.overrideMimeType) {
            httpRequest.overrideMimeType('text/xml');
            // See note below about this line
        }
    } 

    if (!httpRequest) {
        alert('Giving up :( Cannot create an XMLHTTP instance');
        return false;
    }
    httpRequest.onreadystatechange = function() { alertContents(httpRequest, "checkid_setup"); };
    httpRequest.open('POST', url, true);
    httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    //POST the following values to the URL submitted
    httpRequest.send(postData);
}


//This is called after getting all the required params from the auth response.
function trustRequest(url){
    var reqID = removeNL(request_id[1]);
    //window.dump(reqID.length);
    window.dump("Submitting trust data\n");
    window.dump("request_id="+reqID+"&persona_id=154363&allow_once=Allow%20Once&token="+token+"\n");
    
     if (window.XMLHttpRequest) { // Mozilla, Safari, ...
        httpRequest = new XMLHttpRequest();
        if (httpRequest.overrideMimeType) {
            httpRequest.overrideMimeType('text/xml');
            // See note below about this line
        }
    } 

    if (!httpRequest) {
        alert('Giving up :( Cannot create an XMLHTTP instance');
        return false;
    }
    httpRequest.onreadystatechange = function() { alertContents(httpRequest, "trust_submit"); };
    httpRequest.open('POST', url, true);
    httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    //POST the following values to the URL submitted
    
    httpRequest.send("tid="+reqID+"&persona_id=154363&allow_once=Allow%20Once&token="+token);
}

/************************
 *REDIRCT BROWSER CODE
 ***********************/
function redirectBrowser(){
    LOG("RE-DIRECT FOR 500 Alex");
    var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
    var bw = wm.getMostRecentWindow("navigator:browser");
    //need to fix up the return_to parsing, right now its very basic and only works on a couple of sites
    LOG(Url.decode(returnURL[1]));
    bw.content.location.assign(Url.decode(returnURL[1]));    
}

//need this to remove new line character from request_id
function removeNL(s) {
  /*
  ** Remove NewLine, CarriageReturn and Tab characters from a String
  **   s  string to be processed
  ** returns new string
  */
  r = "";
  for (i=0; i < s.length; i++) {
    if (s.charAt(i) != '\n' &&
        s.charAt(i) != '\r' &&
        s.charAt(i) != '\t') {
      r += s.charAt(i);
      }
    }
  return r;
}

/*I got the below code from some JS scripting site, I can't remember where though*/
var Url = {

    // public method for url encoding
    encode : function (string) {
        return escape(this._utf8_encode(string));
    },

    // public method for url decoding
    decode : function (string) {
        return this._utf8_decode(unescape(string));
    },

    // private method for UTF-8 encoding
    _utf8_encode : function (string) {
        string = string.replace(/\r\n/g,"\n");
        var utftext = "";

        for (var n = 0; n < string.length; n++) {

            var c = string.charCodeAt(n);

            if (c < 128) {
                utftext += String.fromCharCode(c);
            }
            else if((c > 127) && (c < 2048)) {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            }
            else {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
            }

        }

        return utftext;
    },

    // private method for UTF-8 decoding
    _utf8_decode : function (utftext) {
        var string = "";
        var i = 0;
        var c = c1 = c2 = 0;

        while ( i < utftext.length ) {

            c = utftext.charCodeAt(i);

            if (c < 128) {
                string += String.fromCharCode(c);
                i++;
            }
            else if((c > 191) && (c < 224)) {
                c2 = utftext.charCodeAt(i+1);
                string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
                i += 2;
            }
            else {
                c2 = utftext.charCodeAt(i+1);
                c3 = utftext.charCodeAt(i+2);
                string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                i += 3;
            }

        }

        return string;
    }

}

//When browser loads fire login request to OP
window.addEventListener("load", function(e) {makeRequest('https://www.myopenid.com/signin_submit'); }, false);

The comments should give you rough idea of how things flow. You'll notice the addition of a couple of new functions, authRequest and trustRequest are the ones to pay attention to. authRequest is called when your consumer site asks your OP for more information and trustRequest is called in response to authRequest, it says 'OK' to parting with your info. The code should be used as a proof of concept more than anything (expect this to be re-written in the coming months).

If you want to try the above code you'll need to replace all USERNAME/PASSWORD and PERSONA_ID references with your own.

To-Do List

A lot needs to be refined here. Code needs to be modeled correctly, regex tweaking for return_to URL needs fixing, persona ID needs to be dynamically grabbed from OP, prompting of info requirements in chrome needs to be implemented, preferences panel needs to be added to Firefox preferences, username/password needs tying into password manager, support for multiple personas, and the ability to support more than one OP is also on the list(distant future).

Contributions

For the brave of course but if you would like to contribute I suggest making an account on http://www.myopenid.com and testing this extension by logging into multiple OpenID supported sites and seeing if there are any problems/bugs/issues/pain it causes you. Contact me via email or blog for the extension package.

  • Brandon Collins
  • Fima Kachinski
  • Simon Jung

Resources and Information

MozCoz - My OpenID Extension Blog.
OpenID.net - The OpenID resource on the net.
OpenIDWiki - Wiki Wiki!
Planet OpenID - They have their own planet!
Slide Shows - Talks/presentations on OpenID
Also on IRC at irc.freenode.net #openid

Personal tools
special sections