Menu

Official website

Play 2, SecureSocial and Slick


04 Jul 2013

min read

If you want to have any kind of login on your Play 2 app, and don’t want to spend the time coding one yourself, the SecureSocial module is the way to go. This article will show you how to combine it with Slick, a Scala persistence API, to store your user’s authentication details.

SecureSocial

SecureSocial is a Play 2 module that makes it very easy to add authentication to your Play app. It supports several authentication methods and allows you to add your own. Out of the box, it supports logging using bunch of social networks, as well as a more conventional password-based sign-up and login method.

SecureSocial is not usable out-of-the-box. You need to implement some classes in order integrate it into your application. Specifically, you need to provide a model class that implements the Identity trait to represent user information, and tell SecureSocial how to store and find instances of that model by providing it with an implementation of the UserService trait.

Identity

Let’s start with the model class. SecureSocial uses securesocial.core.Identity objects to represent login details. Identity objects store all data that identifies a user:

  • A combination of identity provider and the user’s ID for that provider

  • First name

  • Last name

  • Full name

  • Email address

  • The authentication method (OAuth 1, OAuth2, OpenId or user name and password)

  • One of:

  • OAuth 1 information (identifier, secret)

  • OAuth 2 information (accessToken, tokenType, ttl, refreshToken)

  • Password information (password hasher, password, salt)

If you are the kind of person that prefers code, take a look at the actual source code.

This trait is easy to implement if we use a case class:

case class User(uid: Option[Long] = None,
                id: UserId,
                firstName: String,
                lastName: String,
                fullName: String,
                email: Option[String],
                avatarUrl: Option[String],
                authMethod: AuthenticationMethod,
                oAuth1Info: Option[OAuth1Info],
                oAuth2Info: Option[OAuth2Info],
                passwordInfo: Option[PasswordInfo] = None) extends Identity

Now that we have our model class, we’re going to have to find a way to store instances of it. This is managed by an implementation of the UserService trait. The sample application has an implementation that stores these in memory, but for a real-world application you want to store this information somewhere else, like in a database. So, it’s time to use Slick to map our class to a database table.

Mapping our User case class

With Slick, mapping database tables to case classes is usually easy – all you need to do if define a * projection method using the case class’ apply and unapply methods. That usually looks something like this:

-case class Address(id: Option[Long], street: String,
  postalCode: String, city: String, country: String)

object Organisations extends Table[Address]("address") {
  // table definitions would go here

  def * = id ~ street ~ postalCode ~ city ~
    country <> (Address.apply _)(Organisation.unapply _)

What that actually does is provide Slick with a function needed to go from a tuple of table type to the model class. That happens to be exactly what a case class’ apply and unapply methods do.

Unfortunately, our User case class is a little more complex: five of the properties are of types that can’t be mapped to a table directly: UserId, AuthenticationMethod, OAuth1Info, OAuth2Info and PasswordInfo. That means that we can’t just use the apply and unapply methods. We’ll have to do a little more work.

First, let’s just flatten' our datamodel, and map the properties for those classes as if they were part of our `User class. The code is in the listing, below.

object Users extends Table[User]("user") {

  def uid = column[Long]("id", O.PrimaryKey, O.AutoInc)
  def userId = column[String]("userId")
  def providerId = column[String]("providerId")

  def email = column[Option[String]]("email")
  def firstName = column[String]("firstName")
  def lastName = column[String]("lastName")
  def fullName = column[String]("fullName")
  def avatarUrl = column[Option[String]]("avatarUrl")

  def authMethod = column[AuthenticationMethod]("authMethod")

  // oAuth 1
  def token = column[Option[String]]("token")
  def secret = column[Option[String]]("secret")

  // oAuth 2
  def accessToken = column[Option[String]]("accessToken")
  def tokenType = column[Option[String]]("tokenType")
  def expiresIn = column[Option[Int]]("expiresIn")
  def refreshToken = column[Option[String]]("refreshToken")

  // passwordInfo
  def hasher = column[String]("hasher")
  def password = column[String]("password")
  def salt = column[String]("salt")
}

In order to create the projection, we need to write functions that go from this list of columns to a instance of User, and the other way around. To do that, we actually have five things we have to convert first:

  • userId and providerId must become a UserId instance

  • authMethod must become an AuthenticationMethod instance

  • token, secret, must be combined into an Option[OAuth1Info]

  • accessToken, tokenType, expiresIn and refreshToken must be combined into an Option[OAuth2Info]

  • hasher, password, and salt must be combined into an Option[PasswordInfo]

While we could do that in the function definition itself, it’s cleaner to just create methods for it. In fact, we could make the methods implicit conversions, and we would’t even have to call them ourselves. Here are the methods:

implicit def tuple2UserId(tuple: (String, String)) = tuple match {
  case (userId, providerId) => UserId(userId, providerId)
}

implicit def string2AuthenticationMethod: TypeMapper[AuthenticationMethod] =
  MappedTypeMapper.base[AuthenticationMethod, String](
    authenticationMethod => authenticationMethod.method,
    string => AuthenticationMethod(string)
  )

implicit def tuple2OAuth1Info(tuple: (Option[String], Option[String])) =
  tuple match {
    case (Some(token), Some(secret)) => Some(OAuth1Info(token, secret))
    case _ => None
  }

implicit def tuple2OAuth2Info(tuple: (Option[String], Option[String],
  Option[Int], Option[String])) = tuple match {
    case (Some(token), tokenType, expiresIn, refreshToken) =>
      Some(OAuth2Info(token, tokenType, expiresIn, refreshToken))
    case _ => None
}

implicit def tuple2PasswordInfo(tuple: (String, String, Option[String])) =
  tuple match {
    case (Some(hasher), Some(password), salt) =>
      Some(PasswordInfo(hasher, password, salt))
    case _ => None
  }

Now, we can write our projection. Given the number of fields, it’s not exactly the prettiest code, but it does its job. Here’s the code:

def * = uid.? ~ userId ~ providerId ~ firstName ~ lastName ~ fullName ~
 email ~ avatarUrl ~ authMethod ~ token ~ secret ~ accessToken ~ tokenType ~
 expiresIn ~ refreshToken <>(
    u => User(u._1,
      (u._2, u._3),
      u._4,
      u._5,
      u._6,
      u._7,
      u._8,
      u._9,
      (u._10, u._11),
      (u._12, u._13, u._14, u._15),
      (u._16, u._17, u._18)),
    (u: User) => Some((u.uid, u.id.id,
       u.id.providerId,
       u.firstName,
       u.lastName,
       u.fullName,
       u.email,
       u.authMethod,
       u.avatarUrl,
       u.oAuth1Info.map(_.token),
       u.oAuth1Info.map(_.secret),
       u.oAuth2Info.map(_.accessToken),
       u.oAuth2Info.flatMap(_.tokenType),
       u.oAuth2Info.flatMap(_.expiresIn),
       u.oAuth2Info.flatMap(_.refreshToken),
       u.passwordInfo.map(_.hasher),
       u.passwordInfo.map(_.password),
       u.passwordInfo.flatMap(_.salt))))

As you can see, by putting the right fields in parentheses, we create tuples of the right form, which the implict methods then convert in the proper object for us. The reverse function is a little simpler, and mainly involves a lot of mapping and flatmapping from Option types.

Implementing UserService

Now that we mapped this class, we can add save and find methods to the User class:

def findByUserId(userId: UserId): Option[User] = DB.withSession {
implicit session =>
  val query = for {
    user <- Users
    if (user.userId is userId.id) && (user.providerId is userId.providerId)
  } yield user

  query.firstOption
}

def save(i: Identity): User = this.save(User(i))

def save(user: User) = DB.withSession {
implicit session =>
  findByUserId(user.id) match {
    case None => { // Insert a new user (sign-up)
      val uid = this.autoInc.insert(user)
      user.copy(uid = Some(uid))
    }
    case Some(existingUser) => { // Update an existing user
      val userRow = for {
        u <- Users
        if u.uid is existingUser.uid
      } yield u

      val updatedUser = user.copy(uid = existingUser.uid)
      userRow.update(updatedUser)
      updatedUser
    }
  }
}

Now all that’s left to do is implement our UserService, which has become very simple now:

class UserService(application: Application)
  extends UserServicePlugin(application) {

  def find(id: UserId) = Users.findByUserId(id)
  def save(user: Identity) = Users.save(user)

  // Ok, I'm cheating a little. I'm not explainig how to persist Token
  // But I'm sure you can figure out this part!
  def findByEmailAndProvider(email: String, providerId: String) = None
  def save(token: Token) = ???

  def findToken(token: String) = ???

  def deleteToken(uuid: String) = ???

  def deleteExpiredTokens() = ???
}

Conclusion

Mapping SecureSocial’s Identity class to the database is not really straightforward, especially when using Slick. The main difficulty is that it uses some other case classes, which require some special consideration when mapping. This article shows one way to approach this, and the good news is that you can reuse this on any Play 2 project that uses Slick and SecureSocial.

If you want to see this code with all the context of an actual application, I pushed the results of my experiments to GitHub. If you’d like to improve on it (such as implement password-based login, which I didn’t need), pull requests are welcome.

expand_less