Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop v28 support #462

Merged
merged 1 commit into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 8 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import sbtversionpolicy.withsbtrelease.ReleaseVersion.fromAggregatedAssessedComp

lazy val baseSettings = Seq(
scalaVersion := "2.13.14",
crossScalaVersions := Seq(scalaVersion.value, "3.3.3"),
organization := "com.gu.play-secret-rotation",
licenses := Seq("Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0")),
scalacOptions ++= Seq("-deprecation", "-unchecked", "-release:11"),
Test / testOptions +=
Tests.Argument(TestFrameworks.ScalaTest,"-u", s"test-results/scala-${scalaVersion.value}")
)

lazy val crossCompileScala3 = crossScalaVersions := Seq(scalaVersion.value, "3.3.3")

lazy val core =
project.settings(crossCompileScala3, baseSettings).settings(
project.settings(baseSettings).settings(
libraryDependencies ++= Seq(
"com.github.blemale" %% "scaffeine" % "5.2.1",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.5",
Expand All @@ -23,7 +22,7 @@ lazy val core =
)

lazy val `aws-parameterstore-secret-supplier-base` =
project.in(file("aws-parameterstore/secret-supplier")).settings(crossCompileScala3, baseSettings).dependsOn(core)
project.in(file("aws-parameterstore/secret-supplier")).settings(baseSettings).dependsOn(core)

val awsSdkForVersion = Map(
1 -> "com.amazonaws" % "aws-java-sdk-ssm" % "1.12.716",
Expand All @@ -32,26 +31,25 @@ val awsSdkForVersion = Map(

def awsParameterStoreWithSdkVersion(version: Int)=
Project(s"aws-parameterstore-sdk-v$version", file(s"aws-parameterstore/secret-supplier/aws-sdk-v$version"))
.settings(crossCompileScala3, baseSettings)
.settings(baseSettings)
.dependsOn(`aws-parameterstore-secret-supplier-base`)
.settings(libraryDependencies += awsSdkForVersion(version))

lazy val `aws-parameterstore-sdk-v1` = awsParameterStoreWithSdkVersion(1)
lazy val `aws-parameterstore-sdk-v2` = awsParameterStoreWithSdkVersion(2)

lazy val `aws-parameterstore-lambda` = project.in(file("aws-parameterstore/lambda"))
.settings(crossCompileScala3, baseSettings).dependsOn(`secret-generator`).settings(
.settings(baseSettings).dependsOn(`secret-generator`).settings(
libraryDependencies ++= Seq(
"com.amazonaws" % "aws-lambda-java-core" % "1.2.3",
"com.amazonaws" % "aws-lambda-java-events" % "3.11.5",
awsSdkForVersion(1)
)
)

lazy val `secret-generator` = project.settings(crossCompileScala3, baseSettings)
lazy val `secret-generator` = project.settings(baseSettings)

val exactPlayVersions = Map(
"28" -> "com.typesafe.play" %% "play" % "2.8.21",
"29" -> "com.typesafe.play" %% "play" % "2.9.2",
"30" -> "org.playframework" %% "play" % "3.0.2"
)
Expand All @@ -63,16 +61,13 @@ def playVersion(majorMinorVersion: String)= {
.settings(libraryDependencies += exactPlayVersions(majorMinorVersion))
}


lazy val `play-v28` = playVersion("28")
lazy val `play-v29` = playVersion("29").settings(crossCompileScala3)
lazy val `play-v30` = playVersion("30").settings(crossCompileScala3)
lazy val `play-v29` = playVersion("29")
lazy val `play-v30` = playVersion("30")


lazy val `play-secret-rotation-root` = (project in file("."))
.aggregate(
core,
`play-v28`,
`play-v29`,
`play-v30`,
`aws-parameterstore-secret-supplier-base`,
Expand Down
104 changes: 0 additions & 104 deletions play/play-v28/RotatingSecretComponents.scala

This file was deleted.

1 change: 0 additions & 1 deletion play/play-v29/RotatingSecretComponents.scala

This file was deleted.

104 changes: 104 additions & 0 deletions play/play-v29/RotatingSecretComponents.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.gu.play.secretrotation

import java.time.Clock
import java.time.Clock.systemUTC

import play.api.http._
import play.api.mvc._
import play.api.mvc.request.{DefaultRequestFactory, RequestFactory}
import play.api.{BuiltInComponentsFromContext, Configuration}
import play.api.libs.crypto.{CSRFTokenSigner, CSRFTokenSignerProvider, DefaultCSRFTokenSigner, DefaultCookieSigner}
import java.security.MessageDigest
import com.github.blemale.scaffeine.{Scaffeine, LoadingCache}
import scala.concurrent.duration._


trait RotatingSecretComponents extends BuiltInComponentsFromContext {

val secretStateSupplier: SnapshotProvider

override def configuration: Configuration = {
val nonRotatingSecretOnlyUsedToSatisfyConfigChecks = secretStateSupplier.snapshot().secrets.active

Configuration("play.http.secret.key" -> nonRotatingSecretOnlyUsedToSatisfyConfigChecks).withFallback(super.configuration)
}

override lazy val requestFactory: RequestFactory =
RotatingSecretComponents.requestFactoryFor(secretStateSupplier, httpConfiguration)

override lazy val csrfTokenSigner: CSRFTokenSigner =
new RotatingSecretComponents.RotatingKeyCSRFTokenSigner(secretStateSupplier, systemUTC())
}

object RotatingSecretComponents {
def requestFactoryFor(snapshotProvider: SnapshotProvider, hc: HttpConfiguration): RequestFactory =
new DefaultRequestFactory(
new DefaultCookieHeaderEncoding(hc.cookies),
new RotatingKeySessionCookieBaker(hc.session, snapshotProvider),
new RotatingKeyFlashCookieBaker(hc.flash, snapshotProvider)
)


trait RotatingSecretCookieCodec extends CookieDataCodec {
val snapshotProvider: SnapshotProvider
val jwtConfiguration: JWTConfiguration

implicit val c: Clock = systemUTC()

private def jwtCodecFor(secret: String) = DefaultJWTCookieDataCodec(SecretConfiguration(secret), jwtConfiguration)

override def encode(data: Map[String, String]): String =
jwtCodecFor(snapshotProvider.snapshot().secrets.active).encode(data)

override def decode(data: String): Map[String, String] = {
snapshotProvider.snapshot().decode[Map[String, String]](jwtCodecFor(_).decode(data), _.nonEmpty).getOrElse(Map.empty)
}
}

class RotatingKeySessionCookieBaker(
val config: SessionConfiguration,
val snapshotProvider: SnapshotProvider) extends SessionCookieBaker with RotatingSecretCookieCodec {
override val jwtConfiguration: JWTConfiguration = config.jwt
}

class RotatingKeyFlashCookieBaker(
val config: FlashConfiguration,
val snapshotProvider: SnapshotProvider) extends FlashCookieBaker with RotatingSecretCookieCodec {
override val jwtConfiguration: JWTConfiguration = config.jwt
}

class RotatingKeyCSRFTokenSigner(
snapshotProvider: SnapshotProvider,
clock: Clock) extends CSRFTokenSigner {

private val signerCache: LoadingCache[String, DefaultCSRFTokenSigner] = Scaffeine()
.expireAfterAccess(1.day) // stop the cache growing indefinitely
.build(secret => new DefaultCSRFTokenSigner(new DefaultCookieSigner(SecretConfiguration(secret)), clock))

private def signerForActiveSecret() = signerCache.get(snapshotProvider.snapshot().secrets.active)

override def signToken(token: String): String = signerForActiveSecret().signToken(token)
override def generateToken: String = signerForActiveSecret().generateToken
override def generateSignedToken: String = signerForActiveSecret().generateSignedToken
override def constantTimeEquals(a: String, b: String): Boolean = signerForActiveSecret().constantTimeEquals(a, b)

/**
* This method verifies tokens which may have been signed with a previous secret that we still consider valid
* for now. It tries all applicable secrets to see if any of them can verify the token.
*/
override def extractSignedToken(token: String): Option[String] =
snapshotProvider.snapshot().decodeOpt(secret => signerCache.get(secret).extractSignedToken(token))

/**
* It's important that this method doesn't just delegate to an underlying `DefaultCSRFTokenSigner`, because this
* method uses the `extractSignedToken()` method, and we need to use the tolerant version of that method that's
* only available in _this_ class.
*/
override def compareSignedTokens(tokenA: String, tokenB: String): Boolean = {
for {
rawA <- extractSignedToken(tokenA)
rawB <- extractSignedToken(tokenB)
} yield MessageDigest.isEqual(rawA.getBytes("utf-8"), rawB.getBytes("utf-8"))
}.getOrElse(false)
}
}
2 changes: 1 addition & 1 deletion play/play-v30/RotatingSecretComponents.scala