Providing a “Sign-in with Facebook” functionality using Scala


Recently we have integrated “Sign in with Facebook ” functionality in one of our social project that we are building with Lift 2.4 . This post summarizes the work done step by step.

1) Create a Facebook App (if you do not have one already)

Follow the link https://developers.facebook.com/apps and create an app. Enter all the details including Site URL . The Site URL could be something like http://www..com/api/facebook/auth.

Facebook would send response on this Site URL . Once you would save this apps , you would get a
App ID/API Key and secret .

Take a note of this
App ID/API Key and secret .
We would use them in our code.

2) If you are using Lift, then add the
App ID/API Key and secret key in your default.props.

facebook.key=<your_key>
facebook.secret=your <secret_key>
facebook.callbackurl=/api/facebook/auth

3) Download a Facebook login image from fb.png .

4) Let us now create FacebookGraph.scala to configure App ID/API Key , secret key and callbackURL . We would also use this scala file to request access token from Facebook .

package it.fbIntegration
package lib

import org.joda.time.DateTime

import net.liftweb._
import common._
import json._
import http.{ Factory, S, SessionVar }
import util.{ Helpers, Props }
import dispatch._
import it.fbIntegration.config.Site

object FacebookGraph extends Factory with AppHelpers with Loggable {
  /*
   * Config
   */

  val key = new FactoryMaker[String](Props.get("facebook.key", "")) {}
  val secret = new FactoryMaker[String](Props.get("facebook.secret", "")) {}
  val callbackUrl = new FactoryMaker[String](Props.get("facebook.callbackurl", "/api/facebook/auth")) {}
  val channelUrl = new FactoryMaker[String](Props.get("facebook.channelurl", "/facebook/channel")) {}
  val permissions = new FactoryMaker[String](Props.get("facebook.permissions", "email,user_birthday")) {}

  private def baseReq = :/("graph.facebook.com").secure

  object currentAccessToken extends SessionVar[Box[AccessToken]](Empty)
  object currentFacebookId extends SessionVar[Box[Int]](Empty)

  /*
   * Do something with the current access token.
   */
  private def doWithToken[T](f: AccessToken => Box[T]): Box[T] = {
    currentAccessToken.is.flatMap { at =>
      if (at.isExpired) { // refresh the token
        val newToken = accessToken(at.code)
        currentAccessToken(newToken)
        newToken.flatMap(t => f(t))
      } else
        f(at)
    }
  }

  // where to send the user after connecting with facebook
  object continueUrl extends SessionVar[String](Site.home.url)

  // CSRF token
  object csrf extends SessionVar[String](Helpers.nextFuncName)

  // url that sends user to facebook to authorize the app
  def authUrl = "http://www.facebook.com/dialog/oauth?client_id=%s&redirect_uri=%s&scope=%s&state=%s&display=popup"
    .format(key.vend, Helpers.urlEncode(S.hostAndPath + callbackUrl.vend), permissions.vend, csrf.is)

  /*
   * Make a request and process the output with the given function
   */
  private[lib] def doRequest[T](req: Request)(func: String => Box[T]): Box[T] =
    /*
     * See: http://dispatch.databinder.net/Choose+an+Executor.html
     */
    Http x (req as_str) {
      case (400, _, _, out) => Failure(parseError(out()))
      case (200, _, _, out) =>
        val o = out()
        logger.debug("output from facebook: " + o)
        func(o)
      case (status, b, c, out) =>
        //logger.debug("b: "+b.toString)
        //logger.debug("c: "+c.toString)
        Failure("Unexpected status code: %s - %s".format(status, out()))
    }

  private def parseError(in: String): String = Helpers.tryo {
    val jsn = JsonParser.parse(in)
    val JString(errMsg) = jsn \\ "message"
    errMsg
  } openOr "Error parsing error: " + in

  /*
   * Make a request and parse the output to json
   */
  private[lib] def doReq(req: Request): Box[JValue] =
    doRequest(req) { out => Full(JsonParser.parse(out)) }

  /*
   * Make a request with the access token as a parameter
   */
  private def doOauthReq(req: Request, token: AccessToken): Box[JValue] = {
    val params = Map("access_token" -> token.value)
    doReq(req <<? params)
  }

  /*
   * Request an access token from facebook
   */
  def accessToken(code: String): Box[AccessToken] = {
    val req = baseReq / "oauth" / "access_token" <<? Map(
      "client_id" -> key.vend,
      "client_secret" -> secret.vend,
      "redirect_uri" -> (S.hostAndPath + callbackUrl.vend),
      "code" -> code)

    doRequest(req) { out =>
      val map = Map.empty ++ out.split("&").map { param =>
        val pair = param.split("=")
        (pair(0), pair(1))
      }

      (map.get("access_token"), map.get("expires")) match {
        case (Some(at), Some(exp)) => Helpers.asInt(exp)
          .map(e => AccessToken(at, code, (new DateTime).plusSeconds(e)))
        case _ => Failure("Unable to parse access_token: " + map.toString)
      }
    }
  }

  def me(token: AccessToken): Box[JValue] = doOauthReq(baseReq / "me", token)

  def me(token: AccessToken, obj: String): Box[JValue] =
    doOauthReq(baseReq / "me" / obj, token)

  def me: Box[JValue] = doWithToken {
    token => me(token)
  }

  def me(obj: String): Box[JValue] = doWithToken {
    token => me(token, obj)
  }

  //def obj(id: String): Box[JValue] = doReq(baseReq / id)

  def deletePermission(facebookId: Int, perm: Box[String] = Empty): Box[Boolean] = doWithToken {
    token =>
      val req = baseReq.DELETE / facebookId.toString / "permissions" <<?
        Map("access_token" -> token.value) ++ perm.map(p => ("permission" -> p)).toList <:<
        Map("Content-Length" -> "0") // http://facebook.stackoverflow.com/questions/4933780/why-am-i-getting-a-method-not-implemented-error-when-attempting-to-delete-a-fa

      doRequest(req) { out =>
        out match {
          case "true" => Full(true)
          case _ => Empty
        }
      }
  }

  /*
   * http://forum.developers.facebook.net/viewtopic.php?pid=344787
   */
  def parseSignedRequest(in: String): Box[JValue] = {
    import java.util.Arrays
    import javax.crypto.Mac
    import javax.crypto.spec.SecretKeySpec
    import org.apache.commons.codec.binary.Base64

    import Helpers.tryo

    in.split("""\.""").toList match {
      case sig :: payload :: Nil =>
        // decode the data
        val base64 = new Base64(true) // url friendly
        val sentSig = base64.decode(sig.replaceAll("-", "+").replaceAll("_", "/").getBytes)
        (for {
          json <- tryo(JsonParser.parse(new String(base64.decode(payload))))
          algo <- extractAlgo(json)
          ok <- boolToBox(algo.toUpperCase == "HMAC-SHA256") ?~ "Unknown algorithm. Expected HMAC-SHA256"
        } yield {
          logger.debug("signed request json: " + pretty(render(json)))
          val mac = Mac.getInstance("HmacSHA256")
          mac.init(new SecretKeySpec(secret.vend.getBytes, "HmacSHA256"))
          (mac.doFinal(payload.getBytes), json)
        }) match {
          case Full((expectedSig, json)) =>
            if (Arrays.equals(expectedSig, sentSig)) Full(json)
            else {
              logger.debug("expectedSig: " + expectedSig.toString)
              logger.debug("sentSig: " + sentSig.toString)
              Failure("Bad Signed JSON signature!")
            }
          case Empty => Empty
          case f: Failure => f
        }
      case x => Failure("Couldn't split input: " + x.toString)
    }
  }

  private def extractAlgo(jv: JValue): Box[String] = Helpers.tryo {
    val JString(algo) = jv \\ "algorithm"
    algo
  }
}

case class AccessToken(val value: String, val code: String, val expires: DateTime = (new DateTime).plusSeconds(600)) {
  def isExpired: Boolean = expires.isBefore(new DateTime)
}

5) Now let us create Facebok snippet to show Facebook login link on the login page .

package it.fbIntegration
package snippet

import lib.FacebookGraph
import model.User

import scala.xml.{ NodeSeq, Text }

import net.liftweb._
import common._
import http.{ Factory, NoticeType, S, SHtml }
import http.js.JsCmds._
import http.js.JE._
import util.Props
import util.Helpers._

object Facebook extends Loggable {
  /**
   * If user is already connected, display a button with a direct login, otherwise
   * display a button that opens a facebook auth dialog window.
   */
  def link = {
    <span id="id_facebooklink">
      <a href="#"><img src="/img/fb.png" width="181" height="25"/></a>
    </span>
    <div lift="embed?what=/templates-hidden/parts/fb-init"></div>
    <script type="text/javascript">
      <![CDATA[
var Cataalog = {
  api: {
    facebook: {
      init: function(data, success) {
        $.ajax({
          type: "POST",
          url: "/api/facebook/init",
          data: data,
          success: success
        });
      },
      login: function(success) {
        $.ajax({
          type: "POST",
          url: "/api/facebook/login",
          success: success
        });
      }
    }
  },
  facebook: {
    init: function(input, func) {
      window.fbAsyncInit = function() {
        FB.init({
          appId : input.appId, // App ID
          channelURL : input.channelUrl,
          status : true, // check login status
          cookie : true, // enable cookies to allow the server to access the session
          oauth : true, // enable OAuth 2.0
          xfbml : false // parse XFBML
        });
        func();
      };
    }
  },
  util: {
  
    wopen: function (url, name, w, h) {
          w += 32;
      h += 96;
      wleft = (screen.width - w) / 2;
      wtop = (screen.height - h) / 2;
      var win = window.open(url,
        name,
        'width=' + w + ', height=' + h + ', ' +
        'left=' + wleft + ', top=' + wtop + ', ' +
        'location=no, menubar=no, ' +
        'status=no, toolbar=no, scrollbars=no, resizable=no');
      win.resizeTo(w, h);
      win.moveTo(wleft, wtop);
      win.focus();
    }
  }
}
      $("#id_facebooklink").click(function() { onClick(); });

      var onClick = function() {
        Cataalog.util.wopen("/facebook/connect", "facebook_connect", 640, 360);
        return false;
      };

      Cataalog.facebook.init(Input, function() {
        FB.getLoginStatus(function(response) {
          if (response.authResponse) {
            Cataalog.api.facebook.init(response.authResponse, function(data) {
              if (data.alert) {
                console.log(data.alert.level+": "+data.alert.message);
              }
              else if (data.status) {
                onClick = function() {
                  Cataalog.api.facebook.login(function(resp) {
                    if (resp.alert) {
                      console.log(resp.alert.level+": "+resp.alert.message);
                    }
                    else if (resp.url) {
                      window.location=resp.url
                    }
                  })
                  return false;
                };
              }
            })
          }
        })
      });
    ]]>
    </script>
  }

  def popupLink = {
    <span id="id_facebooklink">
      <a href="#"><img src="/img/fb.png" width="181" height="25"/></a>
    </span>
    <script type="text/javascript">
      <![CDATA[
      $("#id_facebooklink").click(function() {
        Cataalog.util.wopen("/facebook/connect", "facebook_connect", 640, 360);
        return false;
      });
    ]]>
    </script>
  }

  /**
   * Inject the data Facebook needs for initialization
   */
  def init =
    "#id_jsinit" #>
      Script(
        JsCrVar("Input", JsObj(
          ("appId", Str(FacebookGraph.key.vend)),
          ("channelUrl", Str(S.hostAndPath + FacebookGraph.channelUrl.vend)))))

  def close =
    Script(
      JsCrVar("Input", JsObj(
        ("url", Str(S.param("url").openOr(User.loginContinueUrl.is))))))

  /**
   * Only display if connected to facebook and access tokenis empty
   */
  def checkAuthToken(in: NodeSeq): NodeSeq =
    if (User.isConnectedToFaceBook && FacebookGraph.currentAccessToken.is.isEmpty)
      in
    else
      NodeSeq.Empty
}

6) Add Facebook login link in the login.html.

<div class="span4">
      <span lift="Facebook.link"/></span>
</div>

7) Now let us create Facebook Connect Menu in your SiteMap.scala .

val facebookConnect = MenuLoc(
    Menu.i("FacebookConnect") / "facebook" / "connect" >> EarlyResponse(() => {
      FacebookGraph.csrf(Helpers.nextFuncName)
      Full(RedirectResponse(FacebookGraph.authUrl, S.responseCookies: _*))
 }))

8) Finally create Facebook.api to get response from Facebook .

package it.fbIntegration
package api

import lib.{ AccessToken, AppHelpers, FacebookGraph }
import model.User
import net.liftweb._
import common._
import http._
import http.rest.RestHelper
import json._
import util.Helpers._
import it.cataalog.config.Site
import org.bson.types.ObjectId

object FacebookApiStateful extends RestHelper with AppHelpers with Loggable {
  def registerUrl = "%s?url=%s".format(Site.facebookClose.url, Site.register.url)
  def successUrl = Site.facebookClose.url
  def errorUrl = Site.facebookError.url
  def homeUrl = "%s?url=%s".format(Site.facebookClose.url, Site.home.url)

  serve("api" / "facebook" prefix {
    /*
     * This is the url that Facebook calls back to when authorizing a user
     */
    case "auth" :: Nil Get _ => {
      val redirectUrl: String =
        (S.param("code"), S.param("error"), S.param("error_reason"), S.param("error_description")) match {
          case (Full(code), _, _, _) =>
            (for {
              state <- S.param("state") ?~ "State not provided"
              ok <- boolToBox(state == FacebookGraph.csrf.is) ?~ "The state does not match. You may be a victim of CSRF."
              accessToken <- FacebookGraph.accessToken(code)
              json <- FacebookGraph.me(accessToken)
              facebookId <- extractId(json)
            } yield {
              logger.debug("auth json: " + pretty(render(json)))

              // set the access token session var
              FacebookGraph.currentAccessToken(Full(accessToken))

              User.findByFacebookId(facebookId) match {
                case Full(user) => validateUser(user) // already connected
                case _ =>
                  User.fromFacebookJson(json).map { facebookUser =>
                    User.findByEmail(facebookUser.email.is) match {
                      case Full(user) => // needs merging
                        validateUser(user)
                      case _ => // new user; send to register page with form pre-filled
                        val user = User
                        User.id(new ObjectId)
                        user.name(facebookUser.name.is)
                        user.email(facebookUser.email.is)
                        user.username(facebookUser.username.is)
                        user.password(facebookUser.username.is)
                        user.locale(facebookUser.locale.is)
                        user.verified(false)
                        user.save
                        User.logUserIn(user, true, true)
                        homeUrl
                    }
                  } openOr handleError("Error creating user from facebook json")
              }
            }) match {
              case Full(url) => url
              case Failure(msg, _, _) => handleError(msg)
              case Empty => handleError("Unknown error")
            }
          case (_, Full(error), Full(reason), Full(desc)) => // user denied authorization, ignore
            successUrl
          case _ => handleError("Unknown request type")
        }
      RedirectResponse(redirectUrl, S.responseCookies: _*)
    }

    /*
     * This is called by Facebook when a user deauthorizes this app on facebook.com
     */
    case "deauth" :: Nil Post _ => {
      (for {
        signedReq <- S.param("signed_request")
        json <- FacebookGraph.parseSignedRequest(signedReq)
        facebookId <- extractUserId(json)
        user <- User.findByFacebookId(facebookId)
      } yield {
        // deauthorize facebook
        User.disconnectFacebook(user)
      }) match {
        case Full(_) =>
        case Failure(msg, _, _) => handleError(msg)
        case Empty => handleError("Unknown error")
      }

      OkResponse()
    }

    /*
     * Call this via ajax when checking login status with JavaScript SDK.
     * Sets the access token and current facebookId.
     */
    case "init" :: Nil Post _ => boxJsonToJsonResponse {
      import JsonDSL._
      for {
        accessToken <- S.param("accessToken") ?~ "Token not provided"
        userId <- S.param("userID") ?~ "UserId not provided"
        facebookId <- asInt(userId) ?~ "Invalid Facebook user id"
        signedReq <- S.param("signedRequest") ?~ "Signed request not provided"
        expiresIn <- S.param("expiresIn") ?~ "ExpiresIn not provided"
        json <- FacebookGraph.parseSignedRequest(signedReq)
      } yield {
        val JString(code) = json \\ "code"
        logger.debug("expiresIn: " + expiresIn)
        // set the access token session var
        FacebookGraph.currentAccessToken(Full(AccessToken(accessToken, code)))
        // set the facebookId
        FacebookGraph.currentFacebookId(Full(facebookId))
        ("status" -> "ok")
      }
    }

    /*
     * Log in a user by their facebookId
     */
    case "login" :: Nil Post _ => boxJsonToJsonResponse {
      import JsonDSL._
      for {
        facebookId <- FacebookGraph.currentFacebookId.is ?~ "currentFacebookId not set"
        user <- User.findByFacebookId(facebookId) ?~ "User not found by facebookId"
      } yield {
        if (user.validate.length == 0) {
          User.logUserIn(user, true, true)
          ("url" -> User.loginContinueUrl.is)
        } else {
          User.regUser(user)
          ("url" -> Site.register.url)
        }
      }
    }
  })

  private def extractId(jv: JValue): Box[Int] = tryo {
    val JString(fbid) = jv \ "id"
    toInt(fbid)
  }

  private def extractUserId(jv: JValue): Box[Int] = tryo {
    val JString(fbid) = jv \ "user_id"
    toInt(fbid)
  }

  private def handleError(msg: String): String = {
    logger.error(msg)
    S.error(msg)
    errorUrl
  }

  private def validateUser(user: User): String = user.validate match {
    case Nil => User.logUserIn(user, true, true); successUrl
    case errs => User.regUser(user); registerUrl
  }
}

About Ayush Mishra

Ayush is the Sr. Software Consultant @ Knoldus Software LLP. In his 5 years of experience he has become developer with proven experience in architecting and developing web applications. Ayush has a Masters in Computer Application from U.P. Technical University, Ayush is a strong-willed and self-motivated professional who takes deep care in adhering to quality norms within projects. He is capable of managing challenging projects with remarkable deadline sensitivity without compromising code quality. .
This entry was posted in Scala. Bookmark the permalink.

8 Responses to Providing a “Sign-in with Facebook” functionality using Scala

  1. Pingback: Providing a “Sign-in with Google” functionality using Scala | Knoldus

  2. That was ultimately helpful, even with some methods is not there in lift by default.

  3. Nayeli Santacruz says:

    Hello
    Very interesting your post
    I’m doing my thesis project and I need some of your code
    Try to compile it but some files are missing
    Can you help with the missing files?

    Really I am very interested

  4. Hey Nayli ,

    We have given a basic idea to implement Facebook functioanlity.
    Is your project based on Scala and Lift ?
    Could you please write here error log , which you are getting while compilation .

  5. umair says:

    can u u share whole application plz

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s