OpenID Authentication with Socko Webserver

by simbo1905

In my last post we took a look at Immutable Session State in Scala. That outlined an immutable SessionState data structure suitable to wrap in an Actor running in the mighty yet diminutive Socko Web Server.  In this post we will pick up where we left off and use the SessionState data structure wrapped in an Actor to implement user registration with openid4java.

Why OpenID? Well we all get sick of forgetting passwords for different sites and using password reset features. We should also worry about which websites have not patched their servers. We should all use two-factor authentication to login into the internet yet too few sites support this. A good OpenID provider like the free, Swiss, privacy respecting, Clavid.com will let you use any of a  number of different two-factor authentication mechanisms. Login once safely and you are logged onto thousands of other sites which support OpenID. I use a $25 yubikey one-time-password dongle with Clavid.com. I have also tried a free Mobile-OTP mobile phone PIN generating app. I can use either secure mechanism to safely login to StackOverflow via Clavid.com.

tl;dr OpenID is good, m’kay?

Lets get a demo working on Socko where you can login to your app via Clavid.com using a free mobile phone one-time password app. For this demo I coped the code from the excellent J Steven Perry article “OpenID for Java Web applications, Part 1“. As Steven does an excellent job explaining exactly what is going on I will just focus on the getting the same approach plumbed into a Socko web app. For the impatient the full sourcecode is over at github.

First up we are going to need https, secure session cookies, and sessions. Socko has a https example app which we can copy. It does not provide sessions out-of-the-box. This is easily rectified. First up lets create code to manage a secure session cookie:

  val JSESSIONID = "JSESSIONID"

  // note the browser *MUST* see a https connection from it's side else it will refuse to pass the secure cookie
  def setSessionCookie(value: String)(implicit event: HttpRequestEvent): Unit = {
    val cookie = new DefaultCookie(JSESSIONID, value)
    cookie.setSecure(true)
    event.response.headers.append("Set-Cookie", ServerCookieEncoder.encode(cookie))
  }

  def clearSessionCookie(implicit event: HttpRequestEvent): Unit = {
    val cookie = new DefaultCookie(JSESSIONID, "deleted")
    cookie.setSecure(true)
    cookie.setDiscard(true)
    event.response.headers.append("Set-Cookie", ServerCookieEncoder.encode(cookie))
  }

  def cookies(event: HttpRequestEvent): Map[String, String] = {
    event.request.headers.get("Cookie") match {
      case None => Map.empty
      case Some(cookies) =>
        (cookies.split(";") map {
          case kv if kv.contains("=") =>
            val ab = kv.split("=")
            Some((ab(0).trim, ab(1).trim))
          case c =>
            None
        }).flatten.toMap
    }
  }

  def getSessionCookie()(implicit event: HttpRequestEvent): Option[String] = {
    cookies(event).get(JSESSIONID)
  }

In this code we make the socko HttpRequestEvent implicit as it is ubiquitous when handing cookies. Note that we mark the cookie as secure; this means that the browser will only pass it down an encrypted https connection.

Next up we need to hold session state against the secure cookie. The state we will hold on the server is:

sealed trait SessionState {
  def sessionId: String
}
case class Discovery(sessionId: String, openId: String, discovery: DiscoveryInformation) extends SessionState
case class Authentication(sessionId: String, openId: String, email: String) extends SessionState

The Discovery represents the users OpenID details which are used in the dance of redirects required to authenticate the user with the OpenID provider. An Authentication represents an authenticated user. We are going to ask the OpenID provider to pass back the users email address. The user will have to authorise that at the provider with a click of the mouse; but they wont have to type it.

In a bigger application the main authenticated session could hold an immutable Map with quite a few user details and preferences. In this application we only go as far as holding the user email address.

Next we define the SessionsActor to manage the session state. The actor wraps an immutable SessionState introduced in the last post which expiries stale data automatically:

class SessionsActor(val name: String, timeout: FiniteDuration) extends Actor with Logger {
  import scala.concurrent.duration._
  implicit def currentTime: Long = System.currentTimeMillis

  log.info(s"$name timeout is $timeout")
  var sessions: Sessions[String, SessionState] = Sessions(timeout)

  def receive = {
    case sessionKey: String =>
      sessions = sessions(sessionKey) match {
        case (updatedSessions, Some(authentication)) =>
          sender ! Some(authentication)
          updatedSessions
        case (updatedSessions, None) =>
          sender ! None
          updatedSessions
      }
    case a: SessionState =>
      val sessionKey = a.sessionId
      (sessions - sessionKey) + (sessionKey, a) match {
        case Success(updatedSessions) =>
          sessions = updatedSessions
          log.debug(s"have added $a under $sessionKey")
        case Failure(ex) =>
          log.error(s"failed to add $a")
      }
    case unknown =>
      log.error(s"unknown message $unknown")
  }
}

We could have a single session actor hold all user session state. Yet discovery information is only used during authentication so can timeout faster than any other state. So we will create two actors; one for the authenticated user with a generous timeout and one for the discovery information with a shorter timeout:

  actorSystem.actorOf(Props(classOf[SessionsActor], "AuthenticatedSessions",
    timeoutMilliseconds("socko-openid.sessions-timeout")), "sessions")

  def sessions = actorSystem.actorSelection("/user/sessions")

  actorSystem.actorOf(Props(classOf[SessionsActor], "DiscoverySessions",
    timeoutMilliseconds("socko-openid.discovery-timeout")), "discoveries")

  def discoveries = actorSystem.actorSelection("/user/discoveries")

An example of doing an asynchronous “set” or an asynchronous “get” of the session state is as follows:

  // set fire-and-forget with the akka "tell" operator '!'
  sessions ! Authentication(cookie, verifiedIdentifier.getIdentifier(), email)

  // get returns a future response  with the akka "ask" operator '?'
  val future = sessions ? cookie

  future.onSuccess {
    case Some(a) =>
      log.info(s"cookie $cookie has authenticated session $a")
    case unknown =>
      log.info(s"cookie $cookie did not have a session: $unknown")
  }

  future.onFailure {
    case ex =>
      log.info(s"timeout trying to fetch cookie $cookie: $ex")
  }

Finally we are ready to dance the OpenID hokey-cokey and register a user!

The demo app has the following structure:

./src/main/resources/private/private.html
./src/main/resources/public/403.html
./src/main/resources/public/500.html
./src/main/resources/public/formlayout.css
./src/main/resources/public/index.html
./src/main/resources/public/registration.html

The private.html file is protected. You need to register to get to it. In a real application there would also be a regular login form on the home page which has been left out for brevity. The 403 and 500 pages are for when we get problems. The index page simply has a logout link and a link to the following registration page:

<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
      <link href="formlayout.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <form id="OpenIdRegister" action="/registerauth" method="post">
      <h1>Please provide you OpenID below to register with this site.</h1>
      <br />
      <table border="0">
        <tr>
          <td><label>Your Open ID:</label></td>
          <td>
            <input class="biginput" type="text" name="openId"/>
            <input type="submit" id="confirmOpenIdButton" value="Confirm OpenID"/>
          </td>
        </tr>
      </table>
    </form>
  </body>
</html>

This simply posts the “openId” input text to “/registerauth”. If you were using Clavid.com this would be something like “ninjadev.clavid.com”. In the server we have a route which maps that form post as follows:

       case POST(PathSegments(Seq("registerauth"))) =>

          // will do http discovery output calls on a background thread
          implicit val executionContextIO = ioBoundThreadPool
          implicit val timeoutIO = discoveryTimeout

          // set a secure session cookie
          val sessionKey = UUID.randomUUID().toString.replace("-", "")
          setSessionCookie(sessionKey)

          val openId = formAttribute("openId")

          // run the http fetches on an io bound threadpool
          val associatedDiscoveriesFuture = future {
            log.info(s"performing discovery on $openId")
            val discoveries = consumerManager.discover(openId);
            log.info(s"performing association on $openId")
            val info = consumerManager.associate(discoveries);
            log.info(s"completed association for $openId")
            info
          }

          associatedDiscoveriesFuture onFailure {
            case t =>
              log.error(s"was not able to discover and associate on $openId due to $t")
              bounceTo("/public/registration.html")
          }

          associatedDiscoveriesFuture onSuccess {
            case discoveryInformation =>
              val d = Discovery(sessionKey, openId, discoveryInformation)
              log.info(s"successfully discovered $openId caching $d")
              discoveries ! d

              val authRequest = Try {
                log.info(s"creating authentication request")
                // authenticated doesn't authenticate it just builds the request in which we ask for the user email
                val authRequest = consumerManager.authenticate(discoveryInformation, returnUrl)
                val sRegRequest = SRegRequest.createFetchRequest();
                sRegRequest.addAttribute("email", false);
                authRequest.addExtension(sRegRequest);
                authRequest.getDestinationUrl(true)
              }

              authRequest match {
                case Failure(e) =>
                  log.error(s"was not able to build auth request on $openId due to $e")
                  bounceTo("/public/registration.html")
                case Success(opUrl) =>
                  log.info(s"successfully built auth request for $openId redirecting to $opUrl")
                  bounceTo(opUrl)
              }
          }

Running through that code:

  • Line 1 matches the form post
  • Lines 4, 5 setups up futures to run on background threads
  • Lines 8, 9 places a fresh cookie for this login attempt
  • Line 11 gets us the users input OpenID identity string (e.g. “ninjadude.clavid.com”)
  • Lines 14-21 perform OpenID discovery on the background threadpool
  • Lines 23-27 handles timeout, network or other issues which may cause the discovery to fail
  • Lines 31-33 fires the successful discovery at the discovery sessions actor with a non-blocking tell (“!”)
  • Lines 35-43 create the encoded redirect to send the browser over to the OpenID provider with a request to provide the users email
  • Lines 49-51 redirect the user over to the OpenID provider

Now our intrepid user is passed over to their preferred OpenID provider. We give them one minute to complete the process. Plenty of time for them to use their yubikey or their mobile phone to do a one-time password login and click the button to approve handing out their email address. If they have already logged in to their provider recently then they will only have to click the approve button.

The return url encoded into the authentication request is https://localhost:8888/registerreturn When the user goes ahead at the provider they are redirected back with encoded data such as their email. This is verified in the following route:

case GET(PathSegments(Seq("registerreturn"))) =>
  implicit val executionContext = cpuBoundThreadPool
  implicit val timeout = askTimeout
  getSessionCookie match {
    case Some(sessionKey) =>
      val askFuture = discoveries ? sessionKey
      askFuture onSuccess {
        case Some(Discovery(_, openId, discoveryInfo)) =>
          log.info(s"have discovery for $openId")
          implicit val executionContextIO = ioBoundThreadPool
          implicit val timeoutIO = verifyTimeout

          val verifyFuture = future {
            log.info(s"trying verify for $openId")
            val verificationResult = consumerManager.verify(returnUrl, parameters, discoveryInfo)
            val verifiedIdentifier = verificationResult.getVerifiedId();
            val authSuccess: AuthSuccess = verificationResult.getAuthResponse().asInstanceOf[AuthSuccess]
            val extension = authSuccess.getExtension(SRegMessage.OPENID_NS_SREG)
            val sRegResponse = extension.asInstanceOf[SRegResponse]
            val email = sRegResponse.getAttributeValue("email")
            val authentication = Authentication(sessionKey, verifiedIdentifier.getIdentifier(), email)
            log.info(s"verify confirmed email is ${email}")
            authentication
          }
          verifyFuture onSuccess {
            case authentication =>
              sessions ! authentication
              log.info(s"successfully verified $authentication under sessionKey $sessionKey")
              bounceTo("/private/private.html")
          }
          verifyFuture onFailure {
            case e =>
              log.error(s"error during verify of $sessionKey : $e")
              bounceTo("/public/registration.html")
          }
        case unknown =>
          log.error(s"unknown discovery message $unknown trying to resolve $sessionKey")
          bounceTo("/public/registration.html")
      }
      askFuture onFailure {
        case t =>
          log.error(s"failed to resolve sessionKey $sessionKey with exception $t")
          bounceTo("/public/registration.html")
      }
    case None =>
      log.info(s"no session cookie")
      bounceTo("/public/registration.html")
  }

Running through that code:

  • Line 1 matches a GET to the return url the OpenID provider sends the user back to
  • Lines 2,3 set up any future work to happen on the cpu bound threadpool with a short timeout. This is for the ask “?” at line 6
  • Line 5 checks for the session cookie else they are bounced back to the registration page
  • Line 6 asks the discovery session actor for the discovery data associated with the session cookie. This returns a future running on the cpu bound threadpool
  • Lines 7, 8 matches the discovery session having being obtained from the session actor
  • Lines 10,11 setup the any future work to happen on the io bound threadpool with a timeout long enough to do any verification callback to the provider (reading the verify code it has a code path which can make a second http fetch to the provider; although my ping time was twice the verification time indicating it took the in-memory  code path)
  • Lines 13-24 perform the verification on the io bound threadpool
  • Lines 25-30 fire the successful authentication including the email passed by the provider to the authentication sessions actor. The users browser is then bounced to the private page

Now we have successfully authenticated a user and have been told their email by their OpenID provider. The final act is to let them into the private page:

case GET(PathSegments(Seq("private", fileName))) =>
  getSessionCookie match {
    case Some(cookie) =>
      implicit val timeout = askTimeout
      implicit val ex = cpuBoundThreadPool
      val future = sessions ? cookie
      future.onSuccess {
        case Some(authentication) =>
          log.info(s"user has authenticated session")
          staticContentHandlerRouter ! fileInFolderRequest("private", fileName)
        case x =>
          log.info(s"user has cookie but session timed out or was never there got back $x")
          bounceTo("/public/403.html")
      }
      future.onFailure {
        case ex =>
          log.info(s"did not get back a session $ex")
          bounceTo("/public/403.html")
      }
    case None =>
      log.info(s"user has no session cookie")
      bounceTo("/public/403.html")
  }

By now this code should be straight forward to follow. When the browser tries to access a file in the “private” folder the authenticated sessions actor is asked with the session cookie. If the authentication exists and has not expired then the private page is served up to the browser. If not then the user is bounced to the 403 page.

[Errata: The bounceTo calls sending to the public 403.html page will be returning code 200 to the browser. They should be replaced with a fobidden() method which sets the correct response code.]

End.

About these ads