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)
})
}
}