diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3dabc0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target + +/project/target +/project/project diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..cffc1fc --- /dev/null +++ b/build.sbt @@ -0,0 +1,14 @@ +name := "basicAuthenticator" + +organization := "net.habashi" + +version := "1.0.0" + +scalaVersion := "2.11.7" + +resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" + +libraryDependencies ++= Seq( + "com.typesafe.play" %% "play" % "2.5.6", + "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.0" % "test" +) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..9ad7e84 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.8 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..5708f81 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +logLevel := Level.Warn diff --git a/src/main/scala/net/habashi/BasicAuthenticationFilter.scala b/src/main/scala/net/habashi/BasicAuthenticationFilter.scala new file mode 100644 index 0000000..6c096a1 --- /dev/null +++ b/src/main/scala/net/habashi/BasicAuthenticationFilter.scala @@ -0,0 +1,61 @@ +package net.habashi + +import akka.stream.Materializer +import play.api.mvc.{Filter, RequestHeader, Result, Results} +import sun.misc.BASE64Decoder + +import scala.concurrent.{ExecutionContext, Future} + +/** + * Enables BasicAuthentication implemented as Filter for applications built with Play!. + * + * @param username to validate + * @param password to validate + * @param mat implicit materializer + * @param ec used executionContext + */ +class BasicAuthenticationFilter(val username: String, val password: String) + (implicit val mat: Materializer, ec: ExecutionContext) extends Filter { + + private object Constants { + lazy val AuthorizationHeaderName = "authorization" + lazy val BasicAuthenticationIdentifier = "Basic " + } + + private lazy val unauthorizedResult = Future.successful { + Results.Unauthorized.withHeaders(("WWW-Authenticate", """Basic realm="BasicAuthentication"""")) + } + + override def apply(nextFilter: (RequestHeader) => Future[Result])(requestHeader: RequestHeader): Future[Result] = { + requestHeader.headers.get(Constants.AuthorizationHeaderName) match { + case Some(authorizationBody) => + if (authorizationBody.startsWith(Constants.BasicAuthenticationIdentifier)) { + val basicAuthenticationBody = authorizationBody.replace(Constants.BasicAuthenticationIdentifier, "") + val decodedPayload = decodeBase64(basicAuthenticationBody) + + if (validateUsernamePassword(decodedPayload)) { + nextFilter(requestHeader) + } else { + unauthorizedResult + } + } else { + unauthorizedResult + } + case None => unauthorizedResult + } + } + + private def decodeBase64(string: String): String = { + val decodedByteArray = new BASE64Decoder().decodeBuffer(string) + new String(decodedByteArray, "UTF-8") + } + + private def validateUsernamePassword(payload: String): Boolean = { + val usernamePassword = payload.split(":") + + usernamePassword.length == 2 && + usernamePassword(0) == username && + usernamePassword(1) == password + } + +} diff --git a/src/test/scala/net/habashi/BasicAuthenticationFilterTest.scala b/src/test/scala/net/habashi/BasicAuthenticationFilterTest.scala new file mode 100644 index 0000000..6c443ed --- /dev/null +++ b/src/test/scala/net/habashi/BasicAuthenticationFilterTest.scala @@ -0,0 +1,100 @@ +package net.habashi + +import akka.stream.Materializer +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.{Matchers, Outcome, fixture} +import org.scalatestplus.play.OneAppPerSuite +import play.api.mvc.{RequestHeader, Result, Results} +import play.api.test.FakeRequest + +import scala.concurrent.{ExecutionContext, Future} + +class BasicAuthenticationFilterTest extends fixture.FlatSpec with OneAppPerSuite with Matchers with ScalaFutures { + + implicit lazy val im: ExecutionContext = scala.concurrent.ExecutionContext.global + + implicit lazy val materializer: Materializer = app.materializer + + private val authenticatedResult = Results.Ok + + private val unauthenticatedResult: Result = Results.Unauthorized.withHeaders(("WWW-Authenticate", """Basic realm="BasicAuthentication"""")) + + private val nextFilter = (requestHeader: RequestHeader) => Future.successful(authenticatedResult) + + case class FixtureParam(basicAuthenticationFilter: BasicAuthenticationFilter) + + override protected def withFixture(test: OneArgTest): Outcome = { + val username: String = "Clark Kent" + val password: String = "Pikachu" + + val basicAuthenticationFilter = new BasicAuthenticationFilter(username, password) + val fixtureParam = FixtureParam(basicAuthenticationFilter) + + super.withFixture(test.toNoArgTest(fixtureParam)) + } + + "A request without header" must "result in 401 Unauthorized" in { + fixParam => { + // given + val requestHeader = FakeRequest() + + // when + val result = fixParam.basicAuthenticationFilter.apply(nextFilter)(requestHeader) + + // then + ScalaFutures.whenReady(result) { + r => + r shouldBe unauthenticatedResult + } + } + } + + "A request without an Authorization-Header starting with the Basic-Identifier" must "result in 401 Unauthorized" in { + fixParam => { + // given + val requestHeader = FakeRequest().withHeaders(("authorization", "Some Clark Kent:Pikachu")) + + // when + val result = fixParam.basicAuthenticationFilter.apply(nextFilter)(requestHeader) + + // then + ScalaFutures.whenReady(result) { + r => + r shouldBe unauthenticatedResult + } + } + } + + "A request not authenticated with a Base64 decoded string" must "result in 401 Unauthorized" in { + fixParam => { + // given + val requestHeader = FakeRequest().withHeaders(("authorization", "Basic Clark Kent:Pikachu")) + + // when + val result = fixParam.basicAuthenticationFilter.apply(nextFilter)(requestHeader) + + // then + ScalaFutures.whenReady(result) { + r => + r shouldBe unauthenticatedResult + } + } + } + + "A well authenticated request" must "proceed with the nextFilters" in { + fixParam => { + // given + val requestHeader = FakeRequest().withHeaders(("authorization", "Basic Q2xhcmsgS2VudDpQaWthY2h1")) + + // when + val result = fixParam.basicAuthenticationFilter.apply(nextFilter)(requestHeader) + + // then + ScalaFutures.whenReady(result) { + r => + r shouldBe authenticatedResult + } + } + } + +}