code-small.jpg

Cake Team Blogs

Purity and dirty secrets

Posted by Jan Machacek

Find me on:

11/11/12 14:04

I left a dirty secret lurking in my previous post about 2 phase authentication with text messages. I hope many of you felt uneasy reading the val secret = generateSecret line. The generateSecret thing has no place in purely functional code. It does some weird side-effect wizardry and computes values that are always different. Read on to find out how to keep tabs on the dirty secrets (and other IO) in your code.


The problem again

Recall the bit of code that generated the authentication token and the secret to be delivered to the user.

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("447*******"), None)
second ! DeliverSecret(deliveryAddress, secret)

// we're logged in partially
sender ! Right(LoggedInPartially(token))

The lines that offends my sense of proper code are val token = UUID.randomUUID() and val secret = generateSecret. Let's pick one of them and dissect it. It says that secret is equal to generateSecret, therefore, it should be possible to replace secret with generateSecret in the code below.

Oh wait. We can't do that. The function generateSecret is not really a function at all. It does something and spouts out secrets. Just like the Daily Mail-o-matic. If we replace both occurrences of secret with generateSecret in the code above, it will fail. We must contain the side-effects.

The containment

We still need to perform these weird side-effects, but we need to contain them. We are going to use the type system with the assistance of the compiler to make sure we don't do anything funky. Instead of having a function that returns secrets as Strings, we'll have a function that returns boxes that carry the secret Strings. We will also notice that we can compose these boxes together to form bigger boxes.

Right-ho.

Thinking about this further, it turns out that it's not just the secret that's randomly generated, it is also the token number. In fact, we can expand the randomness to the entire AuthenticationToken. And so, we'll have the AuthenticationTokenGenerator.

private[authentication] trait AuthenticationTokenGenerator {

  def generateAuthenticationToken(userRef: UUID):
    IO[AuthenticationToken] =

    IO(AuthenticationToken(
    	userRef, UUID.randomUUID(), new Date(), false, 0, None))

  def generateSecret(token: AuthenticationToken):
    IO[AuthenticationToken] =

    IO(token.copy(
        retries = 2,
        secret = Some(UUID.randomUUID().toString.substring(0, 5)),
        partial = true))

}

Notice the return types of the generateAuthenticationToken and generateSecret. They are no longer the dirty, random AuthenticationToken values themselves, but boxes that carry the random values. This is rather important. Whenever we call generateAuthenticationToken, we get a box that carries the generated AuthenticationToken. And so, we cannot use it when we need just the AuthenticationToken. This leads us quite nicely to being able to wire these boxes together. I will show you the LoginActor again in its entirety:

/**
 * Login actor that supervises the actors in the login process.
 */
class LoginActor(secretDelivery: ActorRef) extends
  Actor with AuthenticationTokenGenerator with TokenOperations {

  import scalaz.syntax.monad._

  /**
   * Saves the token in some persistent store
   *
   * @param at the token to be saved
   * @return the IO of the saved token
   */
  def createToken(at: AuthenticationToken): IO[AuthenticationToken] =
  	IO(create(at))

  /**
   * Sends the secret to the user
   *
   * @param at the authentication token
   * @return the IO of the token
   */
  def deliverSeret(at: AuthenticationToken): IO[AuthenticationToken] = {
    val deliveryAddress = DeliveryAddress(None, Some("some@email.com"))

    secretDelivery ! DeliverSecret(deliveryAddress, at.secret.get)

    IO(at)
  }

  def receive = {
    case FirstLogin(username, password, clientSignature) =>
      // check that username & password are OK
      if (username == "root" && password == "p4ssw0rd") {
        val userRef = UUID.fromString("a3372060-2b3b-11e2-81c1-0800200c9a66")
        // the account is there, and needs 2nd phase auth

        val action = generateAuthenticationToken(userRef) >>=
                     generateSecret >>=
                     createToken >>=
                     deliverSeret >>=
                     { at => IO(sender ! Right(LoggedInPartially(at.token))) }

        action.unsafePerformIO()
      } else {
        // not hardcoded username or password, so...
        sender ! Left(BadUsernameOrPassword())
      }
    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)

          val action = generateAuthenticationToken(at.userRef) >>=
                       createToken >>=
                       { at => IO(sender ! Right(LoggedInFully(at.token))) }

          action.unsafePerformIO()
      }
  }

}

So, ther you have it. We have carefully packaged up the side-effects into boxes and assembled the boxes together to get a bigger box. Together with the type checking we get from the compiler, we can ensure that we do not let dirty secrets escape into the rest of our codebase. And, I'm also happy that the = symbol means just that I can replace the symbol with whatever is on the right hand side. And the world is a bit nicer place.

Topics: Akka, Spray, Monads, Scalaz

Subscribe to Email Updates