acb's technical journal

Adventures with Scala, Play, SecureSocial and ReactiveMongo, Part 1

Recently I have been playing with Scala, and particularly with the Play web framework. For what it's worth, Scala is a functional object-oriented language, somewhere between Python and Haskell with the odd Erlangism, bits of Java sticking out in places and an infamously Turing-complete type system, and Play is a Ruby-style web framework implemented in it. The main selling point of Scala, other than the functional niftiness of it, is that it's, as the name suggests, highly scalable; in fact, when Twitter had scaling problems, they rewrote their systems from Ruby on Rails to Scala.)

In the course of my experiments, I have been looking at SecureSocial, a reasonably flexible web identity/authentication framework which allows one to allow users to log in using either local accounts or remote OAuth/OpenID-based services. Also, for the sake of being modern (not to mention scalability), I decided to eschew the traditional SQL databases and go with MongoDB, using the highly asynchronous ReactiveMongo library. Finally, I decided to extend SecureSocial's standard user model, adding extra fields to the user object (among them, capabilities for access control). Unfortunately, there has not until now been much solid documentation on how to bring these elements together, hence this article.

SecureSocial and ReactiveMongo: ReactiveMongoUserService

Because applications using SecureSocial may have a number of different ways of storing user data, SecureSocial requires the application to implement a user service class, which connects to wherever it stores information on users and can find and store user objects and account creation tokens. The objects representing users are, in theory, any which provide the Identity trait (i.e., the interface specifying what constitutes a SecureSocial user); in practice, SecureSocial provides a SocialUser case class whcih just implements this trait, and will at times return it to your code. We can start using this.

In summary, we will need to implement a user service class which uses ReactiveMongo to store users in a MongoDB collection. The methods for fetching user info would look something like this:

 def collection = db[BSONCollection]("users")

 def findByEmailAndProvider(email: String, providerId: String): Option[Identity] = {
    val cursor  = collection.find(
        BSONDocument("userid"->BSONString(email), "provider"->BSONString(providerId))
    ).cursor[User]
    Await.result(cursor.headOption, 5 seconds)
  }

  def find(id: IdentityId): Option[Identity] = 
   findByEmailAndProvider(id.userId,id.providerId)
where User is our user class, which implements the Identity trait, and also comes with implicit readers/writers for translating between it and ReactiveMongo's BSONDocument type.

We also need methods which store and retrieve the Token case class, which is used for storing account-creation tokens (the randomly generated values emailed to a new user when they ask to create an account). We need four methods: Save(t:Token) (which stores a token), findToken(uuid:String) (which attempts to find a token with a given ID, returning an Option[Token]), deleteToken(uuid:String) (whose function you can undoubtedly infer), and deleteExpiredTokens() (which deletes all the tokens past their expiration dates). These are fairly easy to implement using MongoDB, though first, we'll need some code for serialising and deserialising the Token class to/from a BSONDocument:

def tokens = db[BSONCollection]("tokens")

def deserializeToken(doc:BSONDocument): Token = 
  Token(
    doc.getAs[String]("uuid").get, 
    doc.getAs[String]("email").get, 
    new DateTime(doc.getAs[BSONDateTime]("creation_time").get.value),
    new DateTime(doc.getAs[BSONDateTime]("expiration_time").get.value),
    doc.getAs[Boolean]("isSignUp").get
  )

  def serializeToken(token: Token): BSONDocument = 
    BSONDocument(
      "uuid" -> token.uuid,
      "email" -> token.email,
      "creation_time" -> BSONDateTime(token.creationTime.getMillis()),
      "expiration_time" -> BSONDateTime(token.expirationTime.getMillis()),
      "isSignUp" -> token.isSignUp
    )

  def save(token: Token) {
    tokens.save(serializeToken(token))
  }

  def findToken(uuid: String): Option[Token] = {
    val cursor = tokens.find(BSONDocument("uuid" -> uuid)).cursor[BSONDocument]
    Await.result(cursor.headOption, 5 seconds).map { d:BSONDocument => deserializeToken(d)}
  }

  def deleteToken(uuid: String) {
    tokens.remove(BSONDocument("uuid" -> uuid))
  }

  def deleteExpiredTokens() {
  	val now = new DateTime()
  	tokens.remove(BSONDocument("expiration_time" -> BSONDocument( "$lt" -> BSONDateTime(now.getMillis()))))
  }

As you may have noticed, all the code which retrieves data explicitly waits for it to return. While ReactiveMongo is radically asynchronous, returning Futures for any data, SecureSocial, alas, is more immediate-minded, and wants actual data on return; hence, we Await.result the results of the relevant Futures.

We also need code for saving a user object to the database, but that's complicated by our extending of the user object, so we'll get to that after a brief digression.

A custom User class

Any site which has user accounts typically wants to store various information about the users; some of this is intrinsic to the user mechanism (such as user names and authentication credentials), whereas other information will be site-specific. SecureSocial takes care of the former, and keeps it (relatively, though not entirely) minimal, leaving the latter to the site developers calling it. Given MongoDB's freeform document-based approach, we would like to store both SecureSocial authentication data and application-specific data in the same place (i.e., all the data for one user in a MongoDB document representing the site's dealings with that user). Fortunately, this is possible (with a bit of hacking around); to do this, we need to define a user case class, implementing the Identity trait, as well as anything else we wish to store for that user. We'll call this class, simply, User.

In this example, we will want to add the following information to our user data:

  • We want to allow our users to have some profile information they can put up, such as a link to a personal homepage or web site, and a free-form self-description.
  • Different users will have different permissions; some users, for example, will be able to administer the site, add/remove users and perform other privileged tasks. On a complex site, this could extend to roles such as moderating comments, editing the content of various sections, and more. A relatively simple way to implement this is to have a list of capabilities, each one of which will be a string. We can use SecureSocial to require specific capabilities to access certain URLs.

Finally, it might be useful to have access to the MongoDB _id field, the unique identifier each MongoDB document automatically gets.

As such, our User case class will look like this:

case class User(
	// Fields from SecureSocial's Identity trait
	identityId: IdentityId, 
	firstName: String, 
	lastName: String, 
	fullName: String, 
	email: Option[String],                  
    avatarUrl: Option[String], 
    authMethod: AuthenticationMethod,
                      
	oAuth1Info: Option[OAuth1Info] = None,
	oAuth2Info: Option[OAuth2Info] = None,
	passwordInfo: Option[PasswordInfo] = None,
	// Our own fields.
	description: Option[String] = None,
	homepageUrl: Option[String] = None,
 	capabilities: List[String] = List(),
 	id: BSONObjectID
) extends Identity
This class will live in a file named User.scala in the models directory of the app. It will also have a companion object, but more of that later.

Saving our User objects

Saving a user to the database, meanwhile, is slightly more complicated; as mentioned, we have our own User class, which gives all the values Identity defines, plus a few of our own. However, occasoinally, SecureSocial's logic will return a user object that is not our User class, but an implementation of Identity containing just the basic values (typically when the user logs in). This will be when a new user object, without our additional data, has been created. In this case, we want to update just the provided fields without clobbering any of our own fields.

Our basic save method looks fairly simple; it accepts an Identity instance and writes it to the collection, like so:

  def save(user: Identity): Identity = {
    user match {
    	/* the user can be a rich User object or a basic Identity trait; if the latter,
    	   make sure to do an upsert and not clobber the extra fields. */
    	case u:User => collection.save(u)
    	case i:Identity => collection.save(User.encodeIdentity(i))
    	case _ =>
    }
    user
  }

Of course, this depends on a lot of other logic in the background (or, more precisely, inside the User companion object); if u is a User, collection.save(u) will need a way to automagically convert between User objects and BSONDocuments. This is done by defining two implicit objects within User: a subclass of BSONDocumentReader[User] and one of BSONDocumentWriter[User]. These each define a conversion method, and Scala's mighty type system does the rest. The reader side of the equation looks as follows:

object User {


implicit object UserBSONReader extends BSONDocumentReader[User] {
  def read(doc:BSONDocument): User =
    User(
      new IdentityId(doc.getAs[String]("userid").get, doc.getAs[String]("provider").get),
      doc.getAs[String]("firstname").get,
      doc.getAs[String]("lastname").get,
      doc.getAs[String]("firstname").get, // FIXME
      doc.getAs[String]("email"),
      doc.getAs[String]("avatar"),
      new AuthenticationMethod(doc.getAs[String]("authmethod").get),
      doc.getAs[BSONDocument]("oauth1") map { oAuth1Info =>
        new OAuth1Info(
          oAuth1Info.getAs[String]("token").get,
          oAuth1Info.getAs[String]("secret").get
        )
      },
      doc.getAs[BSONDocument]("oauth2") map { oAuth2Info =>
        new OAuth2Info( 
          oAuth2Info.getAs[String]("accessToken").get,
          oAuth2Info.getAs[String]("tokenType"),
          oAuth2Info.getAs[Int]("expiresIn"),
          oAuth2Info.getAs[String]("refreshToken")
        )               
      },
      doc.getAs[BSONDocument]("password") map { passwordInfo =>
        new PasswordInfo(
          passwordInfo.getAs[String]("hasher").get,
          passwordInfo.getAs[String]("password").get,
          passwordInfo.getAs[String]("salt")
        )
      },
      doc.getAs[String]("description"),
      doc.getAs[String]("homepageUrl"),
      doc.getAs[List[String]]("capabilities").getOrElse(List()),
      doc.getAs[BSONObjectID]("_id").get
    )
}

Before we write the corresponding BSONDocumentWriter[User], we should go back to our save function and look at the case of encoding a bare Identity object to just its component fields, i.e.,

    	case i:Identity => collection.save(User.encodeIdentity(i))

For which we'll need an encodeIdentity helper function which makes a BSONDocument out of the core SecureSocial identity fields, like so:

def encodeIdentity(id:Identity) : BSONDocument = BSONDocument(
  "userid" -> id.identityId.userId,
  "provider" -> id.identityId.providerId,
  "firstname" -> id.firstName,
  "lastname" -> id.lastName,
  "email" -> id.email,
  "avatar" -> id.avatarUrl,
  "authmethod" -> id.authMethod.method,
  "oauth1" -> (id.oAuth1Info map { oAuth1Info => BSONDocument(
      "token" -> oAuth1Info.token,
      "secret" -> oAuth1Info.secret
    ) } ),
  "oauth2" -> (id.oAuth2Info map { oAuth2Info => BSONDocument(
      "accessToken" -> oAuth2Info.accessToken,
      "tokenType" -> oAuth2Info.tokenType,
      "expiresIn" -> oAuth2Info.expiresIn,
      "refreshToken" -> oAuth2Info.refreshToken
    )}),
  "password" -> (id.passwordInfo map { passwordInfo => BSONDocument(
      "hasher"->passwordInfo.hasher, 
      "password"->passwordInfo.password,
      "salt"->passwordInfo.salt
    )}) 
  )

Once we have this, we can also use it in UserBSONWriter:

  implicit object UserBSONWriter extends BSONDocumentWriter[User] {
    def write(user: User): BSONDocument = 
      User.encodeIdentity(user) ++ BSONDocument(
        "description" -> user.description,
        "homepageUrl" -> user.homepageUrl,
        "capabilities" -> user.capabilities
      )
    }

So now we have the beginnings of a SecureSocial-enabled Play app that uses MongoDB. We can test this by writing a basic "who am I" page, which displays some details about the currently logged-in user; we'll put this in the Application controller:

object Application extends Controller with SecureSocial {

  def index = UserAwareAction { implicit request =>
  	val resp = request.user match {
      case Some(user:User) => "You are "+user.fullName+"; "+(user.capabilities match {
        	case List() => "You have no capabilities"
        	case l:List[String] => ("Your capabilities are " + l.mkString(", "))
        })
      case _ => "You are not logged in"

  	}
    Ok(resp)
  }
}

Now starting the app and going to http://localhost:9000 (in the default configuration) should inform you that You are not logged in. Logging in as a newly-created user, you should see You are ___; You have no capabilities. To add some capabilities, start a MongoDB shell connected to your database and (assuming that the user is named me, enter:

db.users.update({"userid":"me"}, {"$addToSet": {"capabilities":"admin"}})

...and loading the page should inform you that "Your capabilities are admin".

This has been the first part of a series; in subsequent parts, I will cover checking for capabilities in controllers, querying/updating the database, and further customisations of the SecureSocial user model. There will also be some example code posted to GitHub soon.

There are 3 comments on "Adventures with Scala, Play, SecureSocial and ReactiveMongo, Part 1":

Posted by: vdbaan

Sun Dec 7 14:23:26 2014

I like your article, when will you post some example code on GitHub as I learn from looking at code.

Posted by: Madalien

Sun Apr 19 13:59:55 2015

Nice article you have there, i was looking for this for some time now. Do you have a github repo on which we can find a sample of this?

Posted by: swathi
http://www.atozexams.com/
Fri Jun 16 15:59:30 2017

thank you nice information.

Want to say something? Do so here.

Post pseudonymously

Display name:
URL:(optional)
To prove that you are not a bot,
please enter the text in the image on the right
in the field below it.

Your Comment:

Please keep comments on topic and to the point. Inappropriate comments may be deleted.

Note that markup is stripped from comments; URLs will be automatically converted into links.