Posted in Play, Scala

Login and Registration in Play 2.3 with SecureSocial

A quick and easy way to setup login and registration in Play is using the SecureSocial play plugin.

The SecureSocial plugin provides out of the box:

  • Twitter (OAuth1)
  • Facebook (OAuth2)
  • GitHub (OAuth2)
  • Google (OAuth2)
  • LinkedIn (OAuth1 and OAuth2)
  • Foursquare (OAuth2)
  • Instagram (OAuth2)
  • VK (OAuth2)
  • XING (OAuth1)
  • Username/Password with signup and reset password functionality.

The main thing I was looking for was just simple username / password with signup so all the extra social integration is really a bonus.

There is currently no stable version of SecureSocial for Play 2.3 and as far as I can tell there isn’t any documentation on how to get SecureSocial working using the current milestone release of v3.0. The configuration has changed a lot since the v2 release so here are my notes mostly based on how the sample apps are built.

Prereq : Configure play mailer plugin

See https://github.com/playframework/play-mailer/tree/2.3.1

Include SecureSocial as a dependency

Add the dependency to the sbt build file

libraryDependencies ++= Seq(
  //...existing dependencies
  "ws.securesocial" %% "securesocial" % "3.0-M1"
)

Create a custom UserService

Create a custom UserService to map your internal user model to the SecureSocial model. The InMemoryUserService provides a good starter for testing. This is based on the sample in SecureSocial project InMemoryUserService.scala. This gives you an implementation that can be replaced with a database backed implementation.

// a simple User class that can have multiple identities
case class MyUser(main: BasicProfile, identities: List[BasicProfile])
import securesocial.core._
import securesocial.core.providers.{MailToken, UsernamePasswordProvider}
import securesocial.core.services.{SaveMode, UserService}

import scala.concurrent.Future

class InMemoryUserService extends UserService[MyUser] with common.Logger {
  var users = Map[(String, String), MyUser]()
  private var tokens = Map[String, MailToken]()

  def find(providerId: String, userId: String): Future[Option[BasicProfile]] = {
    logger.debug(s"findByUserId $userId, users = $users")

    val result = for (
      user <- users.values;
      basicProfile <- user.identities.find(su => su.providerId == providerId && su.userId == userId)
    ) yield {
      basicProfile
    }
    Future.successful(result.headOption)
  }

  def findByEmailAndProvider(email: String, providerId: String): Future[Option[BasicProfile]] = {
    logger.debug(s"findByEmail $email, users = $users")

    val someEmail = Some(email)
    val result = for (
      user <- users.values;
      basicProfile <- user.identities.find(su => su.providerId == providerId && su.email == someEmail)
    ) yield {
      basicProfile
    }
    Future.successful(result.headOption)
  }

  private def findProfile(p: BasicProfile) = {
    logger.debug(s"findByProfile $p, users = $users")

    users.find {
      case (key, value) if value.identities.exists(su => su.providerId == p.providerId && su.userId == p.userId) => true
      case _ => false
    }
  }

  private def updateProfile(user: BasicProfile, entry: ((String, String), MyUser)): Future[MyUser] = {
    logger.debug(s"updateProfile $user, $entry, users = $users")

    val identities = entry._2.identities
    val updatedList = identities.patch(identities.indexWhere(i => i.providerId == user.providerId && i.userId == user.userId), Seq(user), 1)
    val updatedUser = entry._2.copy(identities = updatedList)
    users = users + (entry._1 -> updatedUser)
    Future.successful(updatedUser)
  }

  def save(user: BasicProfile, mode: SaveMode): Future[MyUser] = {
    logger.debug(s"save $user, users = $users, mode = $mode" )

    mode match {
      case SaveMode.SignUp =>
        val newUser = MyUser(user, List(user))
        users = users + ((user.providerId, user.userId) -> newUser)
        Future.successful(newUser)
      case SaveMode.LoggedIn =>
        // first see if there is a user with this BasicProfile already.
        findProfile(user) match {
          case Some(existingUser) =>
            updateProfile(user, existingUser)

          case None =>
            val newUser = MyUser(user, List(user))
            users = users + ((user.providerId, user.userId) -> newUser)
            Future.successful(newUser)
        }

      case SaveMode.PasswordChange =>
        findProfile(user).map { entry => updateProfile(user, entry) }.getOrElse(
          // this should not happen as the profile will be there
          throw new Exception("missing profile)")
        )
    }
  }

  def link(current: MyUser, to: BasicProfile): Future[MyUser] = {
    logger.debug(s"link $current, to $to, users = $users" )

    if (current.identities.exists(i => i.providerId == to.providerId && i.userId == to.userId)) {
      Future.successful(current)
    } else {
      val added = to :: current.identities
      val updatedUser = current.copy(identities = added)
      users = users + ((current.main.providerId, current.main.userId) -> updatedUser)
      Future.successful(updatedUser)
    }
  }

  def saveToken(token: MailToken): Future[MailToken] = {
    logger.debug(s"saveToken $token, users = $users" )

    Future.successful {
      tokens += (token.uuid -> token)
      token
    }
  }

  def findToken(token: String): Future[Option[MailToken]] = {
    logger.debug(s"findToken $token, users = $users" )

    Future.successful { tokens.get(token) }
  }

  def deleteToken(uuid: String): Future[Option[MailToken]] = {
    logger.debug(s"deleteToken $uuid, users = $users" )

    Future.successful {
      tokens.get(uuid) match {
        case Some(token) =>
          tokens -= uuid
          Some(token)
        case None => None
      }
    }
  }

  def deleteExpiredTokens() {
    logger.debug(s"deleteExpiredTokens" )

    tokens = tokens.filter(!_._2.isExpired)
  }

  override def updatePasswordInfo(user: MyUser, info: PasswordInfo): Future[Option[BasicProfile]] = {
    logger.debug(s"updatePasswordInfo $user, users = $users" )

    Future.successful {
      for (
        found <- users.values.find(_ == user);
        identityWithPasswordInfo <- found.identities.find(_.providerId == UsernamePasswordProvider.UsernamePassword)
      ) yield {
        val idx = found.identities.indexOf(identityWithPasswordInfo)
        val updated = identityWithPasswordInfo.copy(passwordInfo = Some(info))
        val updatedIdentities = found.identities.patch(idx, Seq(updated), 1)
        val updatedEntry = found.copy(identities = updatedIdentities)
        users = users + ((updatedEntry.main.providerId, updatedEntry.main.userId) -> updatedEntry)
        updated
      }
    }
  }

  override def passwordInfoFor(user: MyUser): Future[Option[PasswordInfo]] = {
    logger.debug(s"passwordInfoFor $user, users = $users" )

    Future.successful {
      for (
        found <- users.values.find(u => u.main.providerId == user.main.providerId && u.main.userId == user.main.userId);
        identityWithPasswordInfo <- found.identities.find(_.providerId == UsernamePasswordProvider.UsernamePassword)
      ) yield {
        identityWithPasswordInfo.passwordInfo.get
      }
    }
  }
}

Update the controllers to use Secure Social

This involves changing your controllers from objects to classes that take the RuntimeEnvironment as a contructor parameter. The controllers will now need to extend SecureSocial instead of controller. Any actions that are to be secured should return SecuredAction instead of Action.

class Application(override implicit val env: RuntimeEnvironment[MyUser]) extends securesocial.core.SecureSocial[MyUser] {

  def index = SecuredAction {
    Ok(views.html.index("Your new application is ready."))
  }
}

Update the routes

Update the routes to include the controllers as classes and include the Secure Social routes for login and registration in your play application. This is now a lot easier than the previous version as the default routes can be added with one line. Note the ‘@’ sign needed for the controllers as they are now classes that need to be instantiated.

GET         /                                     @controllers.Application.index
// all secure social routes for login and registration
->          /auth                                 securesocial.Routes

Add to the Global.scala

Update Global.scala (this should be in default package unless you explicitly configured to be somewhere else) to create SecureSocial runtime and inject into the controller classes on request.

package app

import java.lang.reflect.Constructor

import actor.SyncActorGuardian
import common.Logger
import play.api.libs.concurrent.Akka
import play.api.{Application, GlobalSettings}
import securesocial.core.RuntimeEnvironment
import securesocial.core.providers.UsernamePasswordProvider
import services.{LoginEventListener, MyUser, InMemoryUserService}

import scala.collection.immutable.ListMap

object Global extends GlobalSettings with Logger {
  /**
   * The runtime environment
   */
  object SecureSocialRuntimeEnvironment extends RuntimeEnvironment.Default[MyUser] {
    //override lazy val routes = new CustomRoutesService()
    override lazy val userService: InMemoryUserService = new InMemoryUserService()
    override lazy val eventListeners = List(new LoginEventListener())
    override lazy val providers = ListMap(
      include(new UsernamePasswordProvider[MyUser](userService, avatarService, viewTemplates, passwordHashers))
      // ... other providers
    )
  }

  /**
   * An implementation that checks if the controller expects a RuntimeEnvironment and
   * passes the instance to it if required.
   */
  override def getControllerInstance[A](controllerClass: Class[A]): A = {
    val instance = controllerClass.getConstructors.find { c =>
      val params = c.getParameterTypes
      params.length == 1 && params(0) == classOf[RuntimeEnvironment[MyUser]]
    }.map {
      _.asInstanceOf[Constructor[A]].newInstance(SecureSocialRuntimeEnvironment)
    }
    instance.getOrElse(super.getControllerInstance(controllerClass))
  }
}

Configure Secure Social

The configuration guide is still valid for the current version http://securesocial.ws/guide/configuration.html. I’m just configuring the user password provider.

securesocial {
  onLoginGoTo=/
  onLogoutGoTo=/login
  ssl=false
  userpass {
    withUserNameSupport=false
    sendWelcomeEmail=false
    enableGravatarSupport=false
    signupSkipLogin=true
    tokenDuration=60
    tokenDeleteInterval=5
    minimumPasswordLength=6
    enableTokenJob=true
    hasher=bcrypt
  }
}

Done!
 

Advertisements
Posted in Play, Scala

Sending Emails with Play 2.3 and Gmail

On recent project I found myself in the need to send emails from a Scala Play app. The documentation I hit first online was a bit misleading so I’ve written some basic notes.

Email was removed from the core play framework in 2.0 which is ok but means you have to evaluate your options. This basically comes to two options

  1. Directly use Apache Commons Email (http://commons.apache.org/proper/commons-email/). It is the defacto standard java library for email, its mature, feature rich and there is plenty of support.
  2. Play Mailer Plugin. This appears to be the defacto standard plugin to use email on play. It hasn’t tried to reinvent the wheel and instead it uses apache commons under the covers with integration into the play framwork. Unfortunately in the 2.3.1 version it still looks like a Java api. The current snapshot is looking much better but I’m sticking with stable versions.

I’ve gone with the Play Mailer Plugin. It’s integrated into play and lets me configure it using the play configuration and a number of other play plugins (e.g. SecureSocial) can use it as a dependency for sending email.

This first mistake I made was trying trying to follow the documentation at https://github.com/playframework/play-mailer. This is of course the readme from the master branch but it appears its been half updated so it still refers to configuring 2.3.1 but the examples don’t work. The real documentation you want to access is on v2.3.1 tag https://github.com/playframework/play-mailer/tree/2.3.1. Once you’re following the right guide its easy.

So to setup play to send emails using a gmail.

  1. Add the play mailer as a dependency in the sbt build file
    libraryDependencies ++= Seq(
      //...
      "com.typesafe.play.plugins" %% "play-plugins-mailer" % "2.3.1"
      //...
    )
    
  2. Configure the mailer as a play plugin by adding this line to the play.plugins config file
    1500:com.typesafe.plugin.CommonsMailerPlugin
    
  3. Configure sending of email using your mail account. (Gmail in this example)
    smtp {
      host = smtp.gmail.com
      port = 587
      user = "mygmail@gmail.com"
      password = "mypassword"
      from = "John User <mygmail@gmail.com>"
      tls = yes
      ssl = no
    }
    
  4. Lets test it out
    val mail = use[MailerPlugin].email
    mail.setSubject("mailer test")
    mail.setRecipient("myfriend@gmail.com")
    mail.send("Hello, this is a test. ")
    

It works!

Posted in Play, Scala

Application Logging Trait

Typically the Play Logger framework works is good enough and is simple configure based on the doco here.

One thing I find handy to avoid a bit of repetition is to wrap the logger in a trait than defines how I like the name spacing to be done.

trait Logger {
  lazy val logger = play.api.Logger("application." + getClass.getName)
}

Then in the consuming class I can just mix in the trait.

object Application extends Controller with Logger  {
  def index = Action {
    logger.debug("A debug")
    Ok(views.html.index("Your new application is ready."))
  }
}