Skip to content

Commit

Permalink
Merge pull request #21 from ing-bank/feature/endpoint-security
Browse files Browse the repository at this point in the history
Add token authentication for isActiveCredentials endpoint
  • Loading branch information
arempter authored Mar 20, 2019
2 parents abce91c + 1fcb3e1 commit 7985225
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 32 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import scalariform.formatter.preferences._

name := "airlock-sts"

version := "0.1.8"
version := "0.1.9"

scalaVersion := "2.12.8"

Expand Down Expand Up @@ -46,6 +46,7 @@ libraryDependencies ++= Seq(
"ch.qos.logback.contrib" % "logback-jackson" % logbackJson,
"com.fasterxml.jackson.core" % "jackson-databind" % "2.9.8",
"org.scalatest" %% "scalatest" % "3.0.5" % "test, it",
"com.auth0" % "java-jwt" % "3.8.0",
"com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test,
"com.typesafe.akka" %% "akka-stream-testkit" % "2.5.19" % Test,
"com.amazonaws" % "aws-java-sdk-sts" % "1.11.467" % IntegrationTest)
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ airlock {
masterKey = ${?STS_MASTER_KEY}
encryptionAlgorithm = ${?STS_ENCRYPTION_ALGORITHM}
adminGroups = ${?STS_ADMIN_GROUPS}
decodeSecret = ${?STS_DECODE_SECRET}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ airlock {
masterKey = "MakeSureYouChangeMasterKeyToRandomString"
encryptionAlgorithm = "AES"
adminGroups = ""
decodeSecret = "jwtprivatekey"
}
}

Expand Down
41 changes: 24 additions & 17 deletions src/main/scala/com/ing/wbaa/airlock/sts/api/UserApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package com.ing.wbaa.airlock.sts.api
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import com.ing.wbaa.airlock.sts.data.{ STSUserInfo, UserGroup }
import com.ing.wbaa.airlock.sts.data.aws.{ AwsAccessKey, AwsSessionToken }
import com.ing.wbaa.airlock.sts.data.{ STSUserInfo, UserGroup }
import com.ing.wbaa.airlock.sts.util.JwtToken
import com.typesafe.scalalogging.LazyLogging
import spray.json.RootJsonFormat

import scala.concurrent.Future

trait UserApi extends LazyLogging {
trait UserApi extends LazyLogging with JwtToken {

protected[this] def isCredentialActive(awsAccessKey: AwsAccessKey, awsSessionToken: Option[AwsSessionToken]): Future[Option[STSUserInfo]]

Expand All @@ -27,21 +28,27 @@ trait UserApi extends LazyLogging {
def isCredentialActive: Route = logRequestResult("debug") {
path("isCredentialActive") {
get {
parameters(('accessKey, 'sessionToken.?)) { (accessKey, sessionToken) =>
onSuccess(isCredentialActive(AwsAccessKey(accessKey), sessionToken.map(AwsSessionToken))) {

case Some(userInfo) =>
logger.info("isCredentialActive ok for accessKey={}, sessionToken={}", accessKey, sessionToken)
complete((StatusCodes.OK, UserInfoToReturn(
userInfo.userName.value,
userInfo.userGroup.map(_.value),
userInfo.awsAccessKey.value,
userInfo.awsSecretKey.value)))

case None =>
logger.info("isCredentialActive forbidden for accessKey={}, sessionToken={}", accessKey, sessionToken)
complete(StatusCodes.Forbidden)
}
headerValueByName("Authorization") { bearerToken =>

if (verifyInternalToken(bearerToken)) {

parameters(('accessKey, 'sessionToken.?)) { (accessKey, sessionToken) =>
onSuccess(isCredentialActive(AwsAccessKey(accessKey), sessionToken.map(AwsSessionToken))) {

case Some(userInfo) =>
logger.info("isCredentialActive ok for accessKey={}, sessionToken={}", accessKey, sessionToken)
complete((StatusCodes.OK, UserInfoToReturn(
userInfo.userName.value,
userInfo.userGroup.map(_.value),
userInfo.awsAccessKey.value,
userInfo.awsSecretKey.value)))

case None =>
logger.info("isCredentialActive forbidden for accessKey={}, sessionToken={}", accessKey, sessionToken)
complete(StatusCodes.Forbidden)
}
}
} else { complete(StatusCodes.Forbidden) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class StsSettings(config: Config) extends Extension {
val masterKey: String = airlockStsConfig.getString("masterKey")
val encryptionAlgorithm: String = airlockStsConfig.getString("encryptionAlgorithm")
val adminGroups = airlockStsConfig.getString("adminGroups").split(",").map(_.trim).toList
val decodeSecret = airlockStsConfig.getString("decodeSecret")
}

object StsSettings extends ExtensionId[StsSettings] with ExtensionIdProvider {
Expand Down
33 changes: 33 additions & 0 deletions src/main/scala/com/ing/wbaa/airlock/sts/util/JwtToken.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.ing.wbaa.airlock.sts.util

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.ing.wbaa.airlock.sts.config.StsSettings
import com.typesafe.scalalogging.LazyLogging

import scala.util.{ Failure, Success, Try }

trait JwtToken extends LazyLogging {
protected[this] def stsSettings: StsSettings

def verifyInternalToken(bearerToken: String): Boolean =
Try {
val algorithm = Algorithm.HMAC256(stsSettings.decodeSecret)
val verifier = JWT.require(algorithm)
.withIssuer("airlock")
.build()
verifier.verify(bearerToken)
} match {
case Success(t) =>
val serviceName = t.getClaim("service").asString()
if (serviceName == "airlock") {
logger.debug(s"Successfully verified internal token for $serviceName")
true
} else {
logger.debug(s"Failed to verify internal token")
false
}
case Failure(exception) => throw exception
}

}
48 changes: 34 additions & 14 deletions src/test/scala/com/ing/wbaa/airlock/sts/api/UserApiTest.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.ing.wbaa.airlock.sts.api

import akka.actor.ActorSystem
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.{ MissingQueryParamRejection, Route }
import akka.http.scaladsl.model.headers.RawHeader
import akka.http.scaladsl.server.{ MissingHeaderRejection, MissingQueryParamRejection, Route }
import akka.http.scaladsl.testkit.ScalatestRouteTest
import com.ing.wbaa.airlock.sts.config.StsSettings
import com.ing.wbaa.airlock.sts.data.aws.{ AwsAccessKey, AwsSecretKey, AwsSessionToken }
import com.ing.wbaa.airlock.sts.data.{ STSUserInfo, UserGroup, UserName }
import org.scalatest.{ BeforeAndAfterAll, DiagrammedAssertions, WordSpec }
Expand All @@ -19,32 +22,49 @@ class UserApiTest extends WordSpec
Future.successful(Some(STSUserInfo(UserName("username"), Set(UserGroup("group1"), UserGroup("group2")), AwsAccessKey("a"), AwsSecretKey("s"))))
}

private[this] val testRoute: Route = new testUserApi {}.userRoutes
val bearerToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXJ2aWNlIjoiYWlybG9jayIsImlzcyI6ImFpcmxvY2sifQ.JcHdC29bJyPNpP8tSBZQhNWt2pb2lnaaf1iI-syPg2c"

val testSystem: ActorSystem = ActorSystem.create("test-system")

private[this] val testRoute: Route = new testUserApi {
override protected[this] def stsSettings: StsSettings = new StsSettings(testSystem.settings.config)
}.userRoutes

"User api" should {
"check isCredentialActive" that {

"returns user info" in {
Get(s"/isCredentialActive?accessKey=accesskey&sessionToken=sessionToken")
.addHeader(RawHeader("Authorization", bearerToken)) ~> testRoute ~> check {
assert(status == StatusCodes.OK)
val response = responseAs[String]
assert(response == """{"userName":"username","userGroups":["group1","group2"],"accessKey":"a","secretKey":"s"}""")
}
}

"fails to return user info without authentication" in {
Get(s"/isCredentialActive?accessKey=accesskey&sessionToken=sessionToken") ~> testRoute ~> check {
assert(status == StatusCodes.OK)
val response = responseAs[String]
assert(response == """{"userName":"username","userGroups":["group1","group2"],"accessKey":"a","secretKey":"s"}""")
assert(rejection == MissingHeaderRejection("Authorization"))
}
}

"check credential and return rejection because missing the accessKey param" in {
Get("/isCredentialActive") ~> testRoute ~> check {
assert(rejection == MissingQueryParamRejection("accessKey"))
}
Get("/isCredentialActive")
.addHeader(RawHeader("Authorization", bearerToken)) ~> testRoute ~> check {
assert(rejection == MissingQueryParamRejection("accessKey"))
}
}

"check credential and return status forbidden because wrong the accessKey" in {
Get(s"/isCredentialActive?accessKey=access&sessionToken=session") ~> new testUserApi {
override def isCredentialActive(awsAccessKey: AwsAccessKey, awsSessionToken: Option[AwsSessionToken]): Future[Option[STSUserInfo]] =
Future.successful(None)
}.userRoutes ~> check {
assert(status == StatusCodes.Forbidden)
}
Get(s"/isCredentialActive?accessKey=access&sessionToken=session")
.addHeader(RawHeader("Authorization", bearerToken)) ~> new testUserApi {
override protected[this] def stsSettings: StsSettings = new StsSettings(testSystem.settings.config)

override def isCredentialActive(awsAccessKey: AwsAccessKey, awsSessionToken: Option[AwsSessionToken]): Future[Option[STSUserInfo]] =
Future.successful(None)
}.userRoutes ~> check {
assert(status == StatusCodes.Forbidden)
}
}
}
}
Expand Down

0 comments on commit 7985225

Please sign in to comment.