Max Rosenbaum

Max Rosenbaum

Software developer

Friday, 23 April 2021

Adding basic auth to play 2.8 in scala

Play is an awesome async web framework based on Akka actors that is perfect for highly scalable systems. I found I needed to add basic auth to an endpoint and there is surprisingly little available in the official documentation and around the internet.

I stumbled across this fantastic blog post in 2014, which enables basic auth in a much older version of play. Here is an adaptation of that code for play 2.8;

We need to add this to our application.conf

play.filters {
  enabled += filter.BasicAuth
}

Then in our app/filter folder, create this file called BasicAuth;

package filter

import akka.stream.Materializer
import com.google.inject.Inject
import play.api.Logging
import play.api.mvc._

import scala.concurrent.{ExecutionContext, Future}

class BasicAuth @Inject() (implicit val mat: Materializer, ec: ExecutionContext)
    extends Filter
    with Logging {
  private lazy val unauthResult = Results.Unauthorized.withHeaders(
    ("www-authenticate", "Basic realm=\"default\"")
  )
  private lazy val credentials: Map[String, String] =
    Map(
      "oldmate" -> "pleaseChangeMeIAmVeryInsecure",
    )

  // need the space at the end
  private lazy val basicSt = "basic "

  //This is needed if you are behind a load balancer or a proxy
  private def getUserIPAddress(request: RequestHeader): String = {
    request.headers
      .get("x-forwarded-for")
      .getOrElse(request.remoteAddress)
  }

  private def logFailedAttempt(requestHeader: RequestHeader): Unit = {
    logger.warn(
      s"IP address ${getUserIPAddress(requestHeader)} failed to log in, " +
        s"requested uri: ${requestHeader.uri}"
    )
  }

  private def decodeBasicAuth(auth: String): Option[(String, String)] = {
    if (auth.length() < basicSt.length()) {
      return None
    }
    val basicReqSt = auth.substring(0, basicSt.length())
    if (basicReqSt.toLowerCase() != basicSt) {
      return None
    }
    val basicAuthSt = auth.replaceFirst(basicReqSt, "")
    val decodedAuthSt =
      new String(java.util.Base64.getDecoder.decode(basicAuthSt), "UTF-8")
    val usernamePassword = decodedAuthSt.split(":")
    if (usernamePassword.length >= 2) {
      //account for ":" in passwords
      return Some(usernamePassword(0), usernamePassword.splitAt(1)._2.mkString)
    }
    None
  }

  def apply(
      nextFilter: RequestHeader => Future[Result]
  )(requestHeader: RequestHeader): Future[Result] = {

    requestHeader.headers
      .get("authorization")
      .map { basicAuth =>
        decodeBasicAuth(basicAuth) match {
          case Some((user, pass)) =>
            if (credentials.exists(_ == user -> pass)) {
              return nextFilter(requestHeader)
            }
          case _ => ;
        }
        logFailedAttempt(requestHeader)
        return Future.successful(unauthResult)
      }
      .getOrElse({
        logFailedAttempt(requestHeader)
        Future.successful(unauthResult)
      })
  }
}
picture of the author
Tuesday, 30 November 2021

Plugin architecture

The Swiss army knife of architectures

medium software-architecture python

Continue reading >>