Enthusiastic Java, Scala and Haskell programmer with a long history of large and successful systems. Known author, speaker, motivator and coach. Jan is a DZone MVB and is not an employee of DZone and has posted 26 posts at DZone. You can read more from them at their website. View Full User Profile

Spray Client, Spray and Text Messages

11.16.2012
| 2148 views |
  • submit to reddit

 This is another sample that will make its way to the Akka Patterns project. But for now, you’ll have to settle for a short blog post explaining how Akka Patterns is going to implement two-phase login using text messaging. Just a word of warning–it is a long post, so grab some tea / coffee / $YOUR_FAVOURITE_POISON.

The flow

Now we would like to add two-phase authentication, where the user logs in with their username and password, but then has to type in some secret code that he or she received in a text message. The client application accesses the /login and /login2 REST endpoints to achieve that:

The flow begins with POST {"username":"XXX", "password":"YYY"} to /login, which “returns”:

  • HTTP 200 with {"token":"a3372060-2b3b-11e2-81c1-0800200c9a66"} when the user is logged in fully.
  • HTTP 300 with {"token":"b2c05470-2b3b-11e2-81c1-0800200c9a66"} when the user is logged in partially and a secret code has been sent to their mobile.
  • HTTP 401 when the username or password is invalid.

If the HTTP status is 200 or 401, the web application displays the appropriate error message and asks for the username and password again. If the status code is 300, the application remembers the temporary token and asks the user for the secret. When the user types in the secret, the web app posts {"token":"b2c05470-2b3b-11e2-81c1-0800200c9a66", "secret":"XYZ"} to /login2. That endpoint checks the secret and replies with:

  • HTTP 200 with {"token":"3169ddf0-2b3c-11e2-81c1-0800200c9a66"} when the user is logged in fully.
  • HTTP 401 when the secret does not match, but the user may try again.
  • HTTP 403 when the secret does not match and there are no more retries left.

Let’s now see how to construct such a thing.

The Messages

Let’s first take a look at the messages we’ll be sending around; and we’ll jump staight to the code that defines the messages for the first and second phase of the login

case class FirstLogin(username: String, password: String)

case class SecondLogin(token: UUID, secret: String)

Next, we have the responses. They fall roughly into two main groups: logged in and not logged in, with finer details buried inside each of the groups. In code, this is what they look like:

trait LoggedIn
case class LoggedInFully(token: UUID) extends LoggedIn
case class LoggedInPartially(token: UUID) extends LoggedIn

trait LoginFailure
trait FirstPhaseLoginFailure extends LoginFailure
trait SecondPhaseLoginFailure extends LoginFailure

case class BadUsernameOrPassword() extends FirstPhaseLoginFailure

case class BadPartialToken() extends SecondPhaseLoginFailure
case class TooManyBadSecrets() extends SecondPhaseLoginFailure

That covers the replies. The sunny day scenario of our 2 phase login is FistLogin -> LoggedInPartially -> SecondLogin -> LoggedInFully; the rainy days are best left unexplored (seeing that the winter is upon us).

Heiko (@hseeberger comments that when the case class instances are all the same; that is, when they carry no parameters, it is better to use case object. This would give us case object BadUsernameOrPassword and similar. (The Akka Patterns code actually sends back the username or the token, as appropriate, but I omitted the parameter here for brevity.)

The API

We begin with the API tier, that is, with the LoginService that exposes a route for the REST endpoints. As usual, we are using Spray.

trait LoginServiceMarshallers extends Marshalling {

  implicit val FirstLoginFormat = 
  	jsonFormat3(FirstLogin)
  implicit val SecondLoginFormat = 
  	jsonFormat2(SecondLogin)
  implicit val LoggedInPartiallyFormat = 
  	jsonFormat1(LoggedInPartially)
  implicit val LoggedInFullyFormat = 
  	jsonFormat1(LoggedInFully)

  implicit object LoginFailureMarshaller extends 
  	Marshaller[LoginFailure] {

    def apply(value: LoginFailure, ctx: MarshallingContext) {
      ctx.marshalTo(HttpEntity("Bad login"))
    }
  }

}


class LoginService(implicit val actorSystem: ActorSystem) 
  extends Directives with LoginServiceMarshallers 
  with MetaMarshallers with DefaultTimeout {

  def loginActor = actorSystem.actorFor("/user/application/login")

  import ExecutionContext.Implicits.global 

  /**
   * Processes the login message by sending it to the login actor
   * returns a function that handles the RequestContext
   * and, depending on the response, handles the context as:
   *
   * - 200 -> LoggedInFully(token)
   * - 300 -> LoggedInPartially(token, secret)
   * - 401 -> BadUsernameOrPassword() | BadPartialToken()
   * - 403 -> TooManyBadSecrets()
   *
   * @param msg the message to send to the login actor
   * @param ctx the RequestContext
   * @tparam A the type of the message
   * @return the function that completes the RequestContext appropriately
   */
  def loginFunction[A](msg: A)(ctx: RequestContext) {
     (loginActor ? msg).mapTo[Either[LoginFailure, LoggedIn]] onSuccess { 
      case Left(x: TooManyBadSecrets)     => 
      	ctx.complete(StatusCodes.Forbidden, x)
      case Left(x: BadUsernameOrPassword) => 
      	ctx.complete(StatusCodes.Unauthorized, x)
      case Left(x: BadPartialToken)       => 
      	ctx.complete(StatusCodes.Unauthorized, x)
      case Right(x: LoggedInPartially)    => 
      	ctx.complete(StatusCodes.MultipleChoices, x)
      case Right(x: LoggedInFully)        => 
      	ctx.complete(StatusCodes.OK, x)
      case _                              => 
      	ctx.complete(StatusCodes.InternalServerError)
    }
  }

  val route = {
    post {
      path("login") {
        entity(as[FirstLogin]) { loginFunction }
      } ~
      path("login2") {
        entity(as[SecondLogin]) { loginFunction }
      }
    }
  }
}
The actors

The LoginActor that receives the messages and reacts to them is actually quite simple; the interesting part will be the SecretDeliveryActor that sends the text messages. All right, let’s jump straight into the code.

case class AuthenticationToken(
	userRef: UUID, 
	token: UUID, 
	expires: Date, 
	partial: Boolean, 
	retries: Int, 
	secret: Option[String]) {

  /**
   * Decides whether the token is valid with respect to the given secret.
   *
   * @param s the given secret
   * @return ``true`` if the token is valid
   */
  def isValid(s: String) = secret == Some(s)
}

// other bits and pieces here

class LoginActor(secretDelivery: ActorRef) extends 
  Actor with SecretGenerator with TokenOperations {

  def receive = {
    case FirstLogin(username, password) =>
      // check that username & password are OK
      val token = UUID.randomUUID()
      val secret = generateSecret

      // save the token
      create(AuthenticationToken(UUID.randomUUID(), token, 
      	new Date(), true, 2, Some(secret)))

      // deliver the secret to the user
      val deliveryAddress = DeliveryAddress(Some("44759*******"), None)

      secretDelivery ! DeliverSecret(deliveryAddress, secret)

      // we're logged in partially
      sender ! Right(LoggedInPartially(token))
    case SecondLogin(token, secret) =>
      find(token) match {
        case None =>
          sender ! Left(BadPartialToken())
        case Some(at) if !at.isValid(secret) && at.retries == 0 =>
          // no more retries
          delete(at.token)
          sender ! Left(TooManyBadSecrets())
        case Some(at) if !at.isValid(secret) && at.retries > 0 =>
          // bad secret, but retries still allowed
          update(at.copy(retries = at.retries - 1))
          sender ! Left(BadPartialToken())
        case Some(at) if at.isValid(secret) =>
          // delete the old one
          delete(at.token)

          // generate new token
          val newToken = UUID.randomUUID()

          // save the token
          create(AuthenticationToken(UUID.randomUUID(), newToken, 
          	new Date(), false, 1, None))

          sender ! Right(LoggedInFully(newToken))
      }
  }

}

Onwards to the SecretDeliveryActor, which uses spray-client to perform the network IO. Because there are many different texting providers, we may want to pull out the functionality of sending the text messages into separate traits. The provider we have selected is nexmo.com. OK, the trait:

trait NexmoTextMessageDelivery {
  this: HttpIO =>

  /**
   * Returns the API key for Nexmo.
   * @return the API key
   */
  def apiKey: String

  /**
   * Returns the API secret for Nexmo
   * @return the API secret
   */
  def apiSecret: String

  private val pipeline = 
  	HttpConduit.sendReceive(makeHttpsConduit("rest.nexmo.com"))

  import scala.concurrent.ExecutionContext.Implicits.global

  /**
   * Delivers the text message ``secret`` to the phone number 
   * ``mobileNumber``. The ``mobileNumber`` needs to be in
   * full international format, without spaces, but without 
   * the leading "+", for example ``4477712345678`` for
   * a UK number ``0777 123 45678``
   *
   * @param mobileNumber the mobile number to send the message to
   * @param secret the secret to send
   */
  def deliverTextMessage(mobileNumber: String, secret: String) {
    val url = "/sms/json?api_key=%s&api_secret=%s&from=My%20App&to=%s&text=%s" 
    			format (apiKey, apiSecret, mobileNumber, secret)
    val request = HttpRequest(spray.http.HttpMethods.POST, url)
    pipeline(request) onSuccess  {
      case response =>
        // Sort out the response. 
        // Maybe bang to health agent if we're out of credits or some such
    }
  }

}

The message delivery actor mixes this trait in and satisfies the self-type annotation HttpIO by mixing in the ActorHttpIO, like so:

class SecretDeliveryActor extends 
  Actor with ActorHttpIO with NexmoTextMessageDelivery {

  def apiKey = "******"
  def apiSecret = "********"

  def receive = {
    case DeliverSecret(DeliveryAddress(Some(mobileNumber), _), secret) =>
      deliverTextMessage(mobileNumber, secret)
    case _ =>
      // we can only text for now
  }
}

That leaves us with the last component, the HttpIO and ActorHttpIO. These are indeed new beasts, so let’s see how they fit into our application’s structure.

Slightly modified structure

Because spray-client as well as spray-can use the same udnerlying network mechanism, namely the IOBridge instance, we will pull these out of the Web trait and move them to the HttpIO trait, that can be mixed in to the Web tier, but also used in the components that need to IOBridge. We arrive at:

/**
 * Instantiates & provides access to Spray's ``IOBridge``.
 *
 * @author janmachacek
 */
trait HttpIO {
  implicit def actorSystem: ActorSystem
  
  lazy val ioBridge = new IOBridge(actorSystem).start()
  actorSystem.registerOnTermination(ioBridge.stop())

  private lazy val httpClient = actorSystem.actorOf(
    props = Props(new HttpClient(ioBridge, 
    	ConfigFactory.parseString("spray.can.client.ssl-encryption = on"))),
    name = "http-client"
  )

  def makeHttpsConduit(host: String) =
    actorSystem.actorOf(
    	Props(new HttpConduit(
    		httpClient, host, port = 443, sslEnabled = true)))

}

/**
 * Convenience ``HttpIO`` implementation that can be mixed in to actors.
 */
trait ActorHttpIO extends HttpIO {
  this: Actor =>

  final implicit def actorSystem = context.system
}

With this final modificaiton, we can see how the NexmoTextMessageDelivery can get its hands on the underlying HTTP mechanism provided by the core of Spray; and how we can easily wire in the entire application togehter. Finally, if you actually sign up for Nexmo’s account, your Akka applicaiton will actually send you text messages. How brilliant is that?

Instead of the usual summary, I’ll just say to hang on for the full code to make its way to Akka Patterns next week. I am on holiday, so there’ll be plenty of things that I’ve accumulated that will be available.








Published at DZone with permission of Jan Machacek, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)