Replacing Plone's OpenID login with an OpenID selector for Google, Yahoo etc.
Update October 2012: Looks like the PLIP for this is dead, so here's a diff-style patch file implementing the changes.
Do you dislike the unfriendliness of Plone's default OpenID login page? Me too. It requires the user to know what OpenID is, the fact that it has anything to do with their existing logins on Google, Yahoo etc and so on. Here's what it looks like:

Why can't Plone's OpenID login page just have a set of familiar logos to click on to login? Say, something like this:

This is the default Plone OpenID user interface replaced with the one from http://code.google.com/p/openid-selector/, in fact you can see a demo of openid-selector at http://openid-selector.googlecode.com/svn/trunk/demo.html and as you'll see, it's identical to the above.
Want to see the new Plone OpenID user interface in action. Try here.
Step 1: Get the latest Plone OpenID implementation
Firstly we must take a copy of Plone's OpenID implementation. It might as well be the latest one intended for Plone v4.3, so go to your src directory and do:
svn co http://svn.plone.org/svn/plone/plone.openid/branches/sixfeetup_simpleregistration/ plone.openid svn co http://svn.plone.org/svn/plone/plone.app.openid/branches/sixfeetup_simpleregistration/ plone.app.openid
If you want to know more about the upcoming improvements to OpenID support, see this PLIP #12182
Enter each of plone.openid and plone.app.openid and run 'python setup.py build'.
Enter your buildout.cfg and add to the develop =
src/plone.openid
src/plone.app.openid
At the bottom of buildout.cfg add these version pins to [versions]:
plone.app.openid = 2.1
plone.openid = 2.1
Run 'buildout -v > buildout.out.txt' and make sure it's using plone.app.openid v2.1 by running 'cat buildout.out.txt | grep openid'.
Don't forget to visit protal_quickinstaller and upgrade every site which is using OpenID to v2.1.
Step 2: Patch the OpenID implementation
The following patch file literally contains http://code.google.com/p/openid-selector/source/browse/trunk/js/openid-jquery.js appended by http://code.google.com/p/openid-selector/source/browse/trunk/js/openid-en.js. There are, however, a few small changes to fix up usage for Plone.
There are also a few template changes (obviously enough). Copy and paste this patch file into some temporary file.
Index: plone/app/openid/portlets/login.pt
===================================================================
--- plone/app/openid/portlets/login.pt (revision 52406)
+++ plone/app/openid/portlets/login.pt (working copy)
@@ -19,10 +19,12 @@
<input type="hidden" name="form.submitted" value="1" />
<input type="hidden" name="came_from" value=""
tal:attributes="value context/@@plone_context_state/current_base_url" />
+
+ <a tal:attributes="href string:${here/portal_url}/login_form;"><img alt="openid selector" tal:attributes="src string:${here/portal_url}/openid-selector-login.png;" /></a>
- <div class="field">
- <label for="" i18n:translate="label_identity_url"
- tal:attributes="for ac_identity">OpenID URL</label>
+ <div class="field" style="text-align: center">
+ <label for="" i18n:translate="label_identity_url_quick"
+ tal:attributes="for ac_identity">Quick log in OpenID URL</label>
<br/>
<input type="text"
@@ -31,11 +33,11 @@
style=""
tal:attributes="name ac_identity;
id ac_identity;
- style string:background: url(${here/portal_url}/openid-icon.gif) no-repeat;; padding-left: 16px;"
+ style string:background: url(${here/portal_url}/openid-icon.gif) no-repeat;; padding-left: 16px;; text-align: left;"
/>
</div>
- <div class="formControls">
+ <div class="formControls" style="text-align: center">
<input class="context"
type="submit"
name="openid_submit"
Index: plone/app/openid/skins/ploneopenid/jquery.openid.css
===================================================================
--- plone/app/openid/skins/ploneopenid/jquery.openid.css (revision 0)
+++ plone/app/openid/skins/ploneopenid/jquery.openid.css (revision 0)
@@ -0,0 +1,72 @@
+/*
+ Simple OpenID Plugin
+ http://code.google.com/p/openid-selector/
+
+ This code is licensed under the New BSD License.
+*/
+
+#openid_form {
+}
+
+#openid_form legend {
+ font-weight: bold;
+}
+
+#openid_choice {
+ display: none;
+}
+
+#openid_input_area {
+ clear: both;
+ padding: 10px;
+}
+#openid_btns {
+ width: 550px;
+ margin-left: auto;
+ margin-right: auto;
+}
+#openid_btns, #openid_btns br {
+ clear: both;
+}
+
+#openid_highlight {
+ padding: 3px;
+ background-color: #FFFCC9;
+ float: left;
+}
+
+.openid_large_btn {
+ width: 100px;
+ height: 60px;
+/* fix for IE 6 only: http://en.wikipedia.org/wiki/CSS_filter#Underscore_hack */
+ _width: 102px;
+ _height: 62px;
+
+ border: 1px solid #DDD;
+ margin: 3px;
+ float: left;
+}
+
+.openid_small_btn {
+ width: 24px;
+ height: 24px;
+/* fix for IE 6 only: http://en.wikipedia.org/wiki/CSS_filter#Underscore_hack */
+ _width: 26px;
+ _height: 26px;
+
+ border: 1px solid #DDD;
+ margin: 3px;
+ float: left;
+}
+
+a.openid_large_btn:focus {
+ outline: none;
+}
+
+a.openid_large_btn:focus {
+ -moz-outline-style: none;
+}
+
+.openid_selected {
+ border: 4px solid #DDD;
+}
Index: plone/app/openid/skins/ploneopenid/jquery.openid.js
===================================================================
--- plone/app/openid/skins/ploneopenid/jquery.openid.js (revision 0)
+++ plone/app/openid/skins/ploneopenid/jquery.openid.js (revision 0)
@@ -0,0 +1,300 @@
+/*
+ Simple OpenID Plugin
+ http://code.google.com/p/openid-selector/
+
+ This code is licensed under the New BSD License.
+*/
+
+var providers;
+var openid;
+(function ($) {
+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 : '',
+ locale : null, // is set in openid-<locale>.js
+ sprite : null, // usually equals to locale, is set in
+ // openid-<locale>.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,
+
+ /**
+ * Class constructor
+ *
+ * @return {Void}
+ */
+ init : function(input_id) {
+ providers = $.extend({}, providers_large, providers_small);
+ var openid_btns = $('#openid_btns');
+ this.input_id = input_id;
+ $('#openid_choice').show();
+ $('#openid_input_area').empty();
+ var i = 0;
+ // add box for each provider
+ var id, box;
+ for (id in providers_large) {
+ box = this.getBoxHTML(id, providers_large[id], (this.all_small ? 'small' : 'large'), i++);
+ openid_btns.append(box);
+ }
+ if (providers_small) {
+ openid_btns.append('<br/>');
+ for (id in providers_small) {
+ box = this.getBoxHTML(id, providers_small[id], 'small', i++);
+ openid_btns.append(box);
+ }
+ }
+ $('#openid_form').submit(this.submit);
+ var box_id = this.readCookie();
+ if (box_id) {
+ this.signin(box_id, true);
+ }
+ },
+
+ /**
+ * @return {String}
+ */
+ getBoxHTML : function(box_id, provider, box_size, index) {
+ if (this.no_sprite) {
+ var image_ext = box_size == 'small' ? '.ico.gif' : '.gif';
+ return '<a title="' + this.image_title.replace('{provider}', provider.name) + '" href="javascript:openid.signin(\'' + box_id + '\');"'
+ + ' style="background: #FFF url(' + this.img_path + '../images.' + box_size + '/' + box_id + image_ext + ') no-repeat center center" '
+ + 'class="' + box_id + ' openid_' + box_size + '_btn"></a>';
+ }
+ var x = box_size == 'small' ? -index * 24 : -index * 100;
+ var y = box_size == 'small' ? -60 : 0;
+ return '<a title="' + this.image_title.replace('{provider}', provider.name) + '" href="javascript:openid.signin(\'' + box_id + '\');"'
+ + ' style="background: #FFF url(' + this.img_path + 'openid-providers-' + this.sprite + '.png); background-position: ' + x + 'px ' + y + 'px" '
+ + 'class="' + box_id + ' openid_' + box_size + '_btn"></a>';
+ },
+
+ /**
+ * Provider image click
+ *
+ * @return {Void}
+ */
+ signin : function(box_id, onload) {
+ var provider = providers[box_id];
+ if (!provider) {
+ return;
+ }
+ this.highlight(box_id);
+ this.setCookie(box_id);
+ this.provider_id = box_id;
+ this.provider_url = provider.url;
+ // prompt user for input?
+ if (provider.label) {
+ this.useInputBox(provider);
+ } else {
+ $('#openid_input_area').empty();
+ if (!onload) {
+ $('#openid_form').submit();
+ }
+ }
+ },
+
+ /**
+ * Sign-in button click
+ *
+ * @return {Boolean}
+ */
+ submit : function() {
+ var url = openid.provider_url;
+ if (url) {
+ url = url.replace('{username}', $('#openid_username').val());
+ openid.setOpenIdUrl(url);
+ }
+ if (openid.demo) {
+ alert(openid.demo_text + "\r\n" + document.getElementById(openid.input_id).value);
+ return false;
+ }
+ if (url && url.indexOf("javascript:") == 0) {
+ url = url.substr("javascript:".length);
+ eval(url);
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * @return {Void}
+ */
+ setOpenIdUrl : function(url) {
+ var hidden = document.getElementById(this.input_id);
+ if (hidden != null) {
+ hidden.value = url;
+ } else {
+ $('#openid_form').append('<input type="hidden" id="' + this.input_id + '" name="' + this.input_id + '" value="' + url + '"/>');
+ }
+ },
+
+ /**
+ * @return {Void}
+ */
+ highlight : function(box_id) {
+ // remove previous highlight.
+ var highlight = $('#openid_highlight');
+ if (highlight) {
+ highlight.replaceWith($('#openid_highlight a')[0]);
+ }
+ // add new highlight.
+ $('.' + box_id).wrap('<div id="openid_highlight"></div>');
+ },
+
+ setCookie : function(value) {
+ var date = new Date();
+ date.setTime(date.getTime() + (this.cookie_expires * 24 * 60 * 60 * 1000));
+ var expires = "; expires=" + date.toGMTString();
+ document.cookie = this.cookie_name + "=" + value + expires + "; path=" + this.cookie_path;
+ },
+
+ readCookie : function() {
+ var nameEQ = this.cookie_name + "=";
+ var ca = document.cookie.split(';');
+ for ( var i = 0; i < ca.length; i++) {
+ var c = ca[i];
+ while (c.charAt(0) == ' ')
+ c = c.substring(1, c.length);
+ if (c.indexOf(nameEQ) == 0)
+ return c.substring(nameEQ.length, c.length);
+ }
+ return null;
+ },
+
+ /**
+ * @return {Void}
+ */
+ useInputBox : function(provider) {
+ var input_area = $('#openid_input_area');
+ var html = '';
+ var id = 'openid_username';
+ var value = '';
+ var label = provider.label;
+ var style = '';
+ if (label) {
+ html = '<p>' + label + '</p>';
+ }
+ if (provider.name == 'OpenID') {
+ id = this.input_id;
+ value = 'http://';
+ style = 'background: #FFF url(' + this.img_path + 'openid-inputicon.gif) no-repeat scroll 0 50%; padding-left:18px;';
+ }
+ html += '<input id="' + id + '" type="text" style="' + style + '" name="' + id + '" value="' + value + '" />'
+ + '<input id="openid_submit" type="submit" value="' + this.signin_text + '"/>';
+ input_area.empty();
+ input_area.append(html);
+ $('#' + id).focus();
+ },
+
+ setDemoMode : function(demoMode) {
+ this.demo = demoMode;
+ }
+};
+})(jq);
+
+/*
+ Simple OpenID Plugin
+ http://code.google.com/p/openid-selector/
+
+ This code is licensed under the New BSD License.
+*/
+
+var providers_large = {
+ google : {
+ name : 'Google',
+ url : 'https://www.google.com/accounts/o8/id'
+ },
+ yahoo : {
+ name : 'Yahoo',
+ url : 'http://me.yahoo.com/'
+ },
+ aol : {
+ name : 'AOL',
+ label : 'Enter your AOL screenname.',
+ url : 'http://openid.aol.com/{username}'
+ },
+ myopenid : {
+ name : 'MyOpenID',
+ label : 'Enter your MyOpenID username.',
+ url : 'http://{username}.myopenid.com/'
+ },
+ openid : {
+ name : 'OpenID',
+ label : 'Enter your OpenID.',
+ url : null
+ }
+};
+
+var providers_small = {
+ livejournal : {
+ name : 'LiveJournal',
+ label : 'Enter your Livejournal username.',
+ url : 'http://{username}.livejournal.com/'
+ },
+ /* flickr: {
+ name: 'Flickr',
+ label: 'Enter your Flickr username.',
+ url: 'http://flickr.com/{username}/'
+ }, */
+ /* technorati: {
+ name: 'Technorati',
+ label: 'Enter your Technorati username.',
+ url: 'http://technorati.com/people/technorati/{username}/'
+ }, */
+ wordpress : {
+ name : 'Wordpress',
+ label : 'Enter your Wordpress.com username.',
+ url : 'http://{username}.wordpress.com/'
+ },
+ blogger : {
+ name : 'Blogger',
+ label : 'Your Blogger account',
+ url : 'http://{username}.blogspot.com/'
+ },
+ verisign : {
+ name : 'Verisign',
+ label : 'Your Verisign username',
+ url : 'http://{username}.pip.verisignlabs.com/'
+ },
+ /* vidoop: {
+ name: 'Vidoop',
+ label: 'Your Vidoop username',
+ url: 'http://{username}.myvidoop.com/'
+ }, */
+ /* launchpad: {
+ name: 'Launchpad',
+ label: 'Your Launchpad username',
+ url: 'https://launchpad.net/~{username}'
+ }, */
+ claimid : {
+ name : 'ClaimID',
+ label : 'Your ClaimID username',
+ url : 'http://claimid.com/{username}'
+ },
+ clickpass : {
+ name : 'ClickPass',
+ label : 'Enter your ClickPass username',
+ url : 'http://clickpass.com/public/{username}'
+ },
+ google_profile : {
+ name : 'Google Profile',
+ label : 'Enter your Google Profile username',
+ url : 'http://www.google.com/profiles/{username}'
+ }
+};
+
+openid.locale = 'en';
+openid.sprite = 'en'; // reused in german& japan localization
+openid.demo_text = 'In client demo mode. Normally would have submitted OpenID:';
+openid.signin_text = 'Sign-In';
+openid.image_title = 'log in with {provider}';
Index: plone/app/openid/skins/ploneopenid/logged_out.cpt
===================================================================
--- plone/app/openid/skins/ploneopenid/logged_out.cpt (revision 52406)
+++ plone/app/openid/skins/ploneopenid/logged_out.cpt (working copy)
@@ -6,9 +6,22 @@
i18n:domain="plone">
<head>
- <script type="text/javascript" metal:fill-slot="javascript_head_slot">
- </script>
+ <metal:javascript fill-slot="javascript_head_slot">
+ <script type="text/javascript" src="jquery.openid.js"></script>
+ <script type="text/javascript">
+ jq(document).ready(function() {
+ openid.init('__ac_identity_url');
+ jq("#openid_selector").show();
+ jq("#openid_plone_default").hide().appendTo("#openid_selector");
+ jq("#openid_toggle_ui").click(function() {
+ jq("#openid_plone_selector").toggle();
+ jq("#openid_plone_default").toggle();
+ });
+ });
+ </script>
+ </metal:javascript>
<metal:css fill-slot="style_slot">
+ <link rel="stylesheet" type="text/css" media="screen" href="jquery.openid.css" />
<style type="text/css" media="all">
input.openidUrl {
background: White url(openid-icon.gif) 2px 3px no-repeat;
Index: plone/app/openid/skins/ploneopenid/login_form.cpt
===================================================================
--- plone/app/openid/skins/ploneopenid/login_form.cpt (revision 52406)
+++ plone/app/openid/skins/ploneopenid/login_form.cpt (working copy)
@@ -5,9 +5,22 @@
metal:use-macro="context/main_template/macros/master"
i18n:domain="plone">
<head>
- <script type="text/javascript" metal:fill-slot="javascript_head_slot">
- </script>
+ <metal:javascript fill-slot="javascript_head_slot">
+ <script type="text/javascript" src="jquery.openid.js"></script>
+ <script type="text/javascript">
+ jq(document).ready(function() {
+ openid.init('__ac_identity_url');
+ jq("#openid_selector").show();
+ jq("#openid_plone_default").hide().appendTo("#openid_selector");
+ jq("#openid_toggle_ui").click(function() {
+ jq("#openid_plone_selector").toggle();
+ jq("#openid_plone_default").toggle();
+ });
+ });
+ </script>
+ </metal:javascript>
<metal:css fill-slot="style_slot">
+ <link rel="stylesheet" type="text/css" media="screen" href="jquery.openid.css" />
<style type="text/css" media="all">
input.openidUrl {
background: White url(openid-icon.gif) 2px 3px no-repeat;
@@ -54,6 +67,7 @@
<dd id="fieldset-openid-login">
<form tal:attributes="action python:context.absolute_url()+'/'+template.id"
+ id="openid_form"
class="enableAutoFocus"
method="post"
tal:condition="python:auth">
@@ -65,6 +79,35 @@
tal:attributes="value came_from" />
+
+
+<!-- ned changes -->
+ <fieldset id="openid_selector" style="display:none">
+ <legend>
+ <input id="openid_toggle_ui" type="checkbox" checked="checked" />
+ <span i18n:translate="help_openid_identity_existing_choose">Sign-in using an existing account from another provider or uncheck to enter a URL</span>
+ </legend>
+ <div id="openid_plone_selector">
+ <div id="openid_choice">
+ <p i18n:translate="help_openid_identity_existing">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" i18n:attributes="value label_openid_log_in;" value="Log 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>
+ </div>
+ </fieldset>
+ <div id="openid_plone_default">
+<!-- ned changes -->
+
+
+
+
<h3 i18n:translate="help_openid_identity_url">
Please type your OpenID URL below to log in.
</h3>
@@ -107,6 +150,13 @@
i18n:translate="info_openid">What is OpenID?</a>
</div>
+
+<!-- ned changes -->
+ </div>
+<!-- ned changes -->
+
+
+
</form>
</dd>
</tal:openid>
Enter your src/plone.app.openid directory. Do 'patch -p0 < <path to your temporary file>'. It should patch everything successfully.
Next you must upload some images to src/plone.app.openid/plone/app/openid/skins/ploneopenid:
1. src/plone.app.openid/plone/app/openid/skins/ploneopenid/openid-selector-login.png:

2. src/plone.app.openid/plone/app/openid/skins/ploneopenid/openid-providers-en.png:

Enter each of src/plone.openid and src/plone.app.openid and once again run 'python setup.py build'. You should now be able to run your plone and ensure that <your_site>/login_form does indeed show the new pretty UI.
You'll also note that if javascript is not available on the browser, the old default Plone OpenID interface still appears and the new UI is hidden (or you can untick the box at the top to achieve the same thing). This missing javascript problem is particularly important for Plone 4 running the default Sunburst theme, as we shall shortly see.
Step 3: Stop login_form being opened using AJAX overlays
While login_form does show correctly when loaded directly, unfortunately if you try clicking 'Log In' in the top right of your Plone site you'll get an AJAX overlay popup showing the login form and that login form is the old default Plone OpenID UI.
Why has this happened? Unfortunately Plone's form AJAX tools are currently incapable of honouring metal slots, so the slots inserting the supporting javascript and CSS never get executed. I have designed the login_form above to gracefully degrade to showing you the old UI for this situation, but really you want your new shiny UI to always appear.
The solution is easy: disable login_form from being AJAX overlaid. To do this, go to /portal_skins/plone_ecmascript and customise popupforms.js. Find the section relating to login_form and comment it out like this:
// advanced state
// This form needs additional JS and CSS.
// The AJAX form doesn't load it from the javascript_head_slot.
// login form
//$('#portal-personaltools a[href$=/login], #portal-personaltools a[href$=/login_form], .discussion a[href$=/login_form]').prepOverlay(
// {
// subtype: 'ajax',
// filter: common_content_filter,
// formselector: 'form#login_form',
// noform: function () {
// if (location.href.search(/pwreset_finish$/) >= 0) {
// return 'redirect';
// } else {
// return 'reload';
// }
// },
// redirect: function () {
// var href = location.href;
// if (href.search(/pwreset_finish$/) >= 0) {
// return href.slice(0, href.length-14) + 'logged_in';
// } else {
// return href;
// }
// }
// }
//);
All you need now is to recalculate the cached Plone javascripts, so go to /portal_javascripts, scroll to the end and hit Save.
Refresh your site. Clicking Log In at the top right should no longer load your login form using AJAX but instead take you to the proper login_form page where your shiny new OpenID selector is awaiting.
I hope you found this useful!
Document Actions