Sunday, December 25, 2011

Lift & OpenID. Customized login / logout form

Lately I've playing with Scala and Lift framework.
While building a web application, sooner or later you'll have to face the issue of user management. Fortunatley, Lift comes with OpenID support, to your work is  very little. I don't want to annoy you with an how-to that is already available in official Lift documentation at this URL.
What I find missing was a very user-friendly login form: the one provided expect the user to know his OpenID URL, which is very unlikely. A user may know his Google / AOL / Yahoo! credentials, but not the URL associated with them. Honestly, I'm one of them! :-)
I found a very user friendly login form built with jQuery here.
So I decided to build a Lift snippet once for all wrapping a full management of login / logout operations.
The purposes were:

  • have a user friendly login form
  • support for
    - Google
    - AOL
    - Yahoo!
    - OpenID
  • A widget embeddable in any web page to show the current user and a logout button or a link to the login page
The result is a Scala class:
import scala.xml.{ Text ⇒ T }
import scala.xml.NodeSeq
import net.liftweb.http.S

abstract class Login {
  
  /**
   * Builds the login form.
   * Parameters:
   * <ul>
   * <li><b>return_url</b>: the url the user is redirected to after successful login</li>
   * <li><b>login_enterusername</b>:</li>
   * <li><b>login_enteropenid</b>:</li>
   * <li><b>login_login</b>:</li>
   * </ul>
   */
  def login = {
    val r = S.param("return_url") openOr "/index"

    <head>
      <link rel="stylesheet" type="text/css" media="screen" href={S.param("login_css") openOr ""}/>
      <script type="text/javascript" src={S.param("jqueryopenid_js") openOr ""}></script>
      <script type="text/javascript">  { "$(function() { $(\"form.openid:eq(0)\").openid(); });" }</script>
    </head>
    <h2>{ S.?(S.param("login_title") openOr "Login") }</h2>
    <form class="openid" method="post" action={ (S.param("login_url") openOr "/openid/login") + "?ReturnUrl=" + {S.param("return_url") openOr "/index"} }>
      <div>
        <ul class="providers">
          <li class="openid" title="OpenID"><img src="/classpath/org/scz/openid/ui/images/openidW.png" alt="icon"/> <span>
                                                                                                                      <strong>{ "http://{your-openid-url}" }</strong>
                                                                                                                    </span></li>
          <li class="direct" title="Google">
            <img src="/classpath/org/scz/openid/ui/images/googleW.png" alt="icon"/>
            <span>https://www.google.com/accounts/o8/id</span>
          </li>
          <li class="direct" title="Yahoo"><img src="/classpath/org/scz/openid/ui/images/yahooW.png" alt="icon"/><span>http://yahoo.com/</span></li>
          <li class="username" title="AOL screen name"><img src="/classpath/org/scz/openid/ui/images/aolW.png" alt="icon"/><span>
                                                                                                                             http://openid.aol.com/<strong>username</strong>
                                                                                                                           </span></li>
          <li class="username" title="MyOpenID user name">
            <img src="/classpath/org/scz/openid/ui/images/myopenid.png" alt="icon"/><span>http://<strong>username</strong>.myopenid.com/</span>
          </li>
          <li class="username" title="Flickr user name">
            <img src="/classpath/org/scz/openid/ui/images/flickr.png" alt="icon"/><span>http://flickr.com/<strong>username</strong>/</span>
          </li>
          <li class="username" title="Technorati user name">
            <img src="/classpath/org/scz/openid/ui/images/technorati.png" alt="icon"/><span>http://technorati.com/people/technorati/<strong>username</strong>/</span>
          </li>
          <li class="username" title="Wordpress blog name">
            <img src="/classpath/org/scz/openid/ui/images/wordpress.png" alt="icon"/><span>http://<strong>username</strong>.wordpress.com</span>
          </li>
          <li class="username" title="Blogger blog name">
            <img src="/classpath/org/scz/openid/ui/images/blogger.png" alt="icon"/><span>http://<strong>username</strong>.blogspot.com/</span>
          </li>
          <li class="username" title="LiveJournal blog name">
            <img src="/classpath/org/scz/openid/ui/images/livejournal.png" alt="icon"/><span>http://<strong>username</strong>.livejournal.com</span>
          </li>
          <li class="username" title="ClaimID user name"><img src="/classpath/org/scz/openid/ui/images/claimid.png" alt="icon"/><span>
                                                                                                                                  http://claimid.com/<strong>username</strong>
                                                                                                                                </span></li>
          <li class="username" title="Vidoop user name">
            <img src="/classpath/org/scz/openid/ui/images/vidoop.png" alt="icon"/><span>http://<strong>username</strong>.myvidoop.com/</span>
          </li>
          <li class="username" title="Verisign user name">
            <img src="/classpath/org/scz/openid/ui/images/verisign.png" alt="icon"/><span>http://<strong>username</strong>.pip.verisignlabs.com/</span>
          </li>
        </ul>
      </div>
      <fieldset>
        <label for="openid_username">{ S.?(S.param("login_enterusername") openOr "Enter your username") }</label>
        <div>
          <span></span><input type="text" name="openid_username"/><span></span>
          <input type="submit" value="Login"/>
        </div>
      </fieldset>
      <fieldset>
        <label for="openid_identifier">{ S.?(S.param("login_enteropenid") openOr "Enter your OpenID") }</label>
        <div>
          <input type="text" name="openid_identifier"/><input type="submit" value={S.?(S.param("login_login") openOr "Login")}/>
        </div>
      </fieldset>
    </form>
  }

  /**
   * Widget shown where user is authenticated.<br/>
   * If user is logged a logout form will be shown.<br/>
   * If the user is not logged in, a link to the login page will be shown. Parameters
   * <ul>
   * <li><b>login_widget_as</b></li>
   * <li><b>login_widget_logout</b></li>
   * <li><b>login_logout_path</b></li>
   * <li><b>login_path</b></li>
   * <li><b>login_widget_login</b></li>
   * </ul>
   */
  def widget: NodeSeq = {
    if (authenticated())
      <span id="logout">
        { S.?(S.param("login_widget_as") openOr "Logged in as") }
         <span>{ currentUser() }</span>
         
        <a id="logout_button" href="#">{ S.?(S.param("login_widget_logout") openOr "Logout") }</a>
        <script>{ "$('#logout_button').click(function(){document.forms['openid_form_logout'].submit();})" }</script>
        <form style="display: none" id="openid_form_logout" method="post" action={ S.param("login_logout_path") openOr "/openid/logout" }></form>
      </span>
    else
      <span id="login"><a href={ S.param("login_path") openOr "/login" }>{
        S.?(S.param("login_widget_login") openOr "Login")
      }</a></span>
  }
  
  protected val authenticated: () => Boolean
  protected val currentUser: () => String
}

As you can see, the class is abstract and provides two vals that must be overridden:
val authenticated: ()=>Boolean
val currentUser: ()=>String
The other two functions provided builds respectively the form and the login / logout widget.
Calling this snippets you can add the parameters as specified in the scaladoc comments to customize the labels, that can be localized too.

Hope you'll find usefull