Skip to content

Commit

Permalink
Merge pull request #3842 from nationalarchives/update/play-pac4j-12.0…
Browse files Browse the repository at this point in the history
….0-PLAY3.0

Update/play pac4j 12.0.0 play3.0
  • Loading branch information
Tom-Hallett authored Apr 12, 2024
2 parents ea6d3be + 95ee0f3 commit 73c419c
Show file tree
Hide file tree
Showing 16 changed files with 103 additions and 65 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ jobs:
npm --prefix npm ci
npm --prefix npm run checks
sbt scalafmtCheckAll test
java-version: '17'
secrets:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
14 changes: 9 additions & 5 deletions app/auth/TokenSecurity.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package auth
import com.nimbusds.oauth2.sdk.token.BearerAccessToken
import configuration.KeycloakConfiguration
import io.opentelemetry.api.trace.Span
import org.pac4j.core.profile.{ProfileManager, UserProfile}
import org.pac4j.core.profile.UserProfile
import org.pac4j.oidc.profile.OidcProfile
import org.pac4j.play.PlayWebContext
import org.pac4j.play.context.PlayFrameworkParameters
import play.api.i18n.I18nSupport
import play.api.mvc.{Action, AnyContent, Request, Result}
import services.ConsignmentService
Expand All @@ -24,14 +26,16 @@ trait TokenSecurity extends OidcSecurity with I18nSupport {
val userIdKey = "UserId"

def getProfile(request: Request[AnyContent]): Optional[UserProfile] = {
val webContext = new PlayWebContext(request)
val profileManager = new ProfileManager(webContext, sessionStore)
val parameters = new PlayFrameworkParameters(request)
val webContext = controllerComponents.config.getWebContextFactory.newContext(parameters).asInstanceOf[PlayWebContext]
val sessionStore = config.getSessionStoreFactory.newSessionStore(parameters)
val profileManager = controllerComponents.config.getProfileManagerFactory.apply(webContext, sessionStore)
profileManager.getProfile
}

implicit def requestToRequestWithToken(request: Request[AnyContent]): RequestWithToken = {
val profile = getProfile(request)
val token: BearerAccessToken = profile.get().getAttribute("access_token").asInstanceOf[BearerAccessToken]
val profile = getProfile(request).get().asInstanceOf[OidcProfile]
val token: BearerAccessToken = profile.getAccessToken.asInstanceOf[BearerAccessToken]
val accessToken: Option[Token] = keycloakConfiguration.token(token.getValue)
RequestWithToken(request, accessToken)
}
Expand Down
12 changes: 8 additions & 4 deletions app/auth/UnprotectedPageController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ package auth

import com.nimbusds.jwt.SignedJWT
import com.nimbusds.oauth2.sdk.token.BearerAccessToken
import org.pac4j.core.profile.{CommonProfile, ProfileManager}
import org.pac4j.core.profile.{CommonProfile, ProfileManager, UserProfile}
import org.pac4j.oidc.profile.OidcProfile
import org.pac4j.play.PlayWebContext
import org.pac4j.play.scala.{Security, SecurityComponents}
import org.pac4j.play.context.PlayFrameworkParameters
import org.pac4j.play.scala.{Pac4jScalaTemplateHelper, Security, SecurityComponents}
import play.api.mvc.{AnyContent, Request}

import javax.inject.Inject

class UnprotectedPageController @Inject() (val controllerComponents: SecurityComponents) extends Security[CommonProfile] {

private def getProfile(request: Request[AnyContent]): ProfileManager = {
val parameters = new PlayFrameworkParameters(request)
val sessionStore = config.getSessionStoreFactory.newSessionStore(parameters)
val webContext = new PlayWebContext(request)
new ProfileManager(webContext, sessionStore)
}
Expand All @@ -26,7 +30,7 @@ class UnprotectedPageController @Inject() (val controllerComponents: SecurityCom
val profileManager = getProfile(request)
val profile = profileManager.getProfile
if (profile.isPresent) {
val token: BearerAccessToken = profile.get().getAttribute("access_token").asInstanceOf[BearerAccessToken]
val token: BearerAccessToken = profile.get().asInstanceOf[OidcProfile].getAccessToken.asInstanceOf[BearerAccessToken]
val parsedToken = SignedJWT.parse(token.getValue).getJWTClaimsSet
parsedToken.getClaim("name").toString
} else {
Expand All @@ -38,7 +42,7 @@ class UnprotectedPageController @Inject() (val controllerComponents: SecurityCom
val profileManager = getProfile(request)
val profile = profileManager.getProfile
if (profile.isPresent) {
val token: BearerAccessToken = profile.get().getAttribute("access_token").asInstanceOf[BearerAccessToken]
val token: BearerAccessToken = profile.get().asInstanceOf[OidcProfile].getAccessToken.asInstanceOf[BearerAccessToken]
val parsedToken = SignedJWT.parse(token.getValue).getJWTClaimsSet
parsedToken.getBooleanClaim("judgment_user")
} else {
Expand Down
16 changes: 10 additions & 6 deletions app/configuration/AccessLoggingFilter.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package configuration

import akka.stream.Materializer
import auth.OidcSecurity
import com.nimbusds.oauth2.sdk.token.BearerAccessToken
import org.apache.pekko.stream.Materializer
import org.pac4j.core.profile.ProfileManager
import org.pac4j.oidc.profile.OidcProfile
import org.pac4j.play.PlayWebContext
import org.pac4j.play.context.PlayFrameworkParameters
import org.pac4j.play.scala.SecurityComponents
import play.api.Logging
import play.api.mvc.{Filter, RequestHeader, Result}

import javax.inject.Inject
import scala.compat.java8.OptionConverters._
import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.OptionConverters.RichOptional

class AccessLoggingFilter @Inject() (implicit
val mat: Materializer,
Expand All @@ -29,11 +30,14 @@ class AccessLoggingFilter @Inject() (implicit
nextFilter(request)
} else {
nextFilter(request).map { result =>
val parameters = new PlayFrameworkParameters(request)
val sessionStore = config.getSessionStoreFactory.newSessionStore(parameters)
val webContext = new PlayWebContext(request)
val profileManager = new ProfileManager(webContext, sessionStore)
val userId: String = profileManager.getProfile.asScala
.map(_.getAttribute("access_token").asInstanceOf[BearerAccessToken])
.flatMap(token => keycloakConfiguration.token(token.getValue))

val userId: String = profileManager.getProfile.toScala
.map(_.asInstanceOf[OidcProfile].getAccessToken.toString)
.flatMap(token => keycloakConfiguration.token(token))
.map(_.userId.toString)
.getOrElse("user-logged-out")

Expand Down
21 changes: 11 additions & 10 deletions app/configuration/CustomSavedRequestHandler.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package configuration

import org.pac4j.core.context.WebContext
import org.pac4j.core.context.session.SessionStore
import org.pac4j.core.context.{CallContext, WebContext}
import org.pac4j.core.engine.savedrequest.SavedRequestHandler
import org.pac4j.core.exception.http.{FoundAction, HttpAction}
import org.pac4j.core.util.{HttpActionHelper, Pac4jConstants}
Expand All @@ -10,25 +9,27 @@ import play.api.Logging
import scala.jdk.OptionConverters.RichOptional

class CustomSavedRequestHandler extends SavedRequestHandler with Logging {
override def save(context: WebContext, sessionStore: SessionStore): Unit = {
override def save(context: CallContext): Unit = {
logger.info("Saving webContext")
val webContext = context.webContext()

val requestedUrl = getRequestedUrl(context)
val requestedUrl = getRequestedUrl(webContext)

// Need to specify the type of SessionStore so that we can pass the context into the set method context.
sessionStore
.set(context, Pac4jConstants.REQUESTED_URL, new FoundAction(requestedUrl))
context.sessionStore().set(webContext, Pac4jConstants.REQUESTED_URL, new FoundAction(requestedUrl))
}

private def getRequestedUrl(context: WebContext): String = context.getFullRequestURL

override def restore(context: WebContext, sessionStore: SessionStore, defaultUrl: String): HttpAction = {
val optRequestedUrl = sessionStore
.get(context, Pac4jConstants.REQUESTED_URL)
override def restore(context: CallContext, defaultUrl: String): HttpAction = {
val webContext = context.webContext()
val optRequestedUrl = context
.sessionStore()
.get(webContext, Pac4jConstants.REQUESTED_URL)

val redirectAction = optRequestedUrl.toScala
.map(_.asInstanceOf[FoundAction])
.getOrElse(new FoundAction(defaultUrl))
HttpActionHelper.buildRedirectUrlAction(context, redirectAction.getLocation)
HttpActionHelper.buildRedirectUrlAction(webContext, redirectAction.getLocation)
}
}
5 changes: 3 additions & 2 deletions app/errors/ErrorHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.nimbusds.jwt.SignedJWT
import com.nimbusds.oauth2.sdk.token.BearerAccessToken
import org.pac4j.core.exception.TechnicalException
import org.pac4j.core.profile.CommonProfile
import org.pac4j.oidc.profile.OidcProfile
import org.pac4j.play.scala.Pac4jScalaTemplateHelper
import play.api.Logging
import play.api.http.HttpErrorHandler
Expand All @@ -25,7 +26,7 @@ class ErrorHandler @Inject() (val messagesApi: MessagesApi, implicit val pac4jTe
.getCurrentProfiles(request)
.headOption
.map(profile => {
val token = profile.getAttribute("access_token").asInstanceOf[BearerAccessToken]
val token = profile.asInstanceOf[OidcProfile].getAccessToken.asInstanceOf[BearerAccessToken]
val parsedToken = SignedJWT.parse(token.getValue).getJWTClaimsSet
parsedToken.getClaim("name").toString
})
Expand All @@ -39,7 +40,7 @@ class ErrorHandler @Inject() (val messagesApi: MessagesApi, implicit val pac4jTe
.getCurrentProfiles(request)
.headOption
.exists(profile => {
val token = profile.getAttribute("access_token").asInstanceOf[BearerAccessToken]
val token = profile.asInstanceOf[OidcProfile].getAccessToken.asInstanceOf[BearerAccessToken]
val parsedToken = SignedJWT.parse(token.getValue).getJWTClaimsSet
parsedToken.getBooleanClaim("judgment_user")
})
Expand Down
3 changes: 2 additions & 1 deletion app/modules/FrontendHttpActionAdaptor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import org.pac4j.play.http.PlayHttpActionAdapter
import play.mvc.{Result, Results}

import scala.compat.java8.OptionConverters.RichOptionalGeneric
import scala.jdk.OptionConverters.RichOptional

class FrontendHttpActionAdaptor extends PlayHttpActionAdapter {

def isAjax(context: WebContext): Boolean =
context.getRequestHeader("X-Requested-With").asScala.contains("XMLHttpRequest")
context.getRequestHeader("X-Requested-With").toScala.contains("XMLHttpRequest")

override def adapt(action: HttpAction, context: WebContext): Result = {
action match {
Expand Down
8 changes: 6 additions & 2 deletions app/modules/SecurityModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod
import configuration.CustomSavedRequestHandler
import org.pac4j.core.client.Clients
import org.pac4j.core.config.Config
import org.pac4j.core.context.session.SessionStore
import org.pac4j.core.context.FrameworkParameters
import org.pac4j.core.context.session.{SessionStore, SessionStoreFactory}
import org.pac4j.core.engine.{DefaultCallbackLogic, DefaultSecurityLogic}
import org.pac4j.core.profile.CommonProfile
import org.pac4j.oidc.client.OidcClient
Expand Down Expand Up @@ -61,7 +62,7 @@ class SecurityModule extends AbstractModule {
}

@Provides
def provideConfig(oidcClient: OidcClient): Config = {
def provideConfig(oidcClient: OidcClient, sessionStore: SessionStore): Config = {
val clients = new Clients(oidcClient)
val config = new Config(clients)
config.setHttpActionAdapter(new FrontendHttpActionAdaptor())
Expand All @@ -72,6 +73,9 @@ class SecurityModule extends AbstractModule {
config.setCallbackLogic(callbackLogic)
val securityLogic = DefaultSecurityLogic.INSTANCE
securityLogic.setSavedRequestHandler(customRequestHandler)
config.setSessionStoreFactory(new SessionStoreFactory {
override def newSessionStore(parameters: FrameworkParameters): SessionStore = sessionStore
})

config
}
Expand Down
2 changes: 1 addition & 1 deletion app/views/standard/confirmTransfer.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ <h1 class="govuk-heading-l">Confirm transfer</h1>
Files uploaded for transfer
</dt>
<dd class="govuk-summary-list__value">
@summary.totalFiles @if(summary.totalFiles == 1) {file} else {files} uploaded
@summary.totalFiles @if(summary.totalFiles == 1) {file} else {files}&nbsp;uploaded
</dd>
</div>
</dl>
Expand Down
21 changes: 11 additions & 10 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@ scalaVersion := "2.13.12"
libraryDependencies += guice
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "5.1.0" % Test

//Needed to run the tests. Prevents incompatible databind version errors.
//More details on a similar error here: https://stackoverflow.com/questions/43841091/spark2-1-0-incompatible-jackson-versions-2-7-6
dependencyOverrides += "com.fasterxml.jackson.core" % "jackson-databind" % "2.11.4" % Test

val playPac4jVersion = "11.1.0-PLAY2.8"
val pac4jVersion = "5.7.2"
val playVersion = "3.0.2"
val playPac4jVersion = "12.0.0-PLAY3.0"
val pac4jVersion = "6.0.2"
val sttpVersion = "2.3.0"

libraryDependencies ++= Seq(
"org.pac4j" %% "play-pac4j" % playPac4jVersion,
"org.pac4j" % "pac4j-http" % pac4jVersion exclude ("com.fasterxml.jackson.core", "jackson-databind"),
"org.pac4j" % "pac4j-oidc" % pac4jVersion exclude ("commons-io", "commons-io") exclude ("com.fasterxml.jackson.core", "jackson-databind"),
"org.pac4j" %% "play-pac4j" % playPac4jVersion excludeAll (ExclusionRule("commons-io", "commons-io"), ExclusionRule(organization = "com.fasterxml.jackson.core")),
"org.pac4j" % "pac4j-http" % pac4jVersion excludeAll (ExclusionRule(organization = "com.fasterxml.jackson.core")),
"org.pac4j" % "pac4j-oidc" % pac4jVersion,
"io.circe" %% "circe-core" % "0.14.6",
"io.circe" %% "circe-generic" % "0.14.6",
"com.softwaremill.sttp.client" %% "core" % sttpVersion,
Expand All @@ -49,11 +46,15 @@ libraryDependencies ++= Seq(
"org.mockito" % "mockito-core" % "5.10.0" % Test,
"org.scalatestplus" %% "mockito-3-4" % "3.2.10.0" % Test
)
libraryDependencies += "org.scala-lang.modules" %% "scala-java8-compat" % "1.0.2"

dependencyOverrides += "com.fasterxml.jackson.core" % "jackson-databind" % "2.14.0"
dependencyOverrides += "com.fasterxml.jackson.core" % "jackson-core" % "2.14.0"

disablePlugins(PlayLogback)
scalacOptions ++= Seq("-language:implicitConversions")

libraryDependencies += play.sbt.PlayImport.cacheApi
libraryDependencies += "com.github.karelcemus" %% "play-redis" % "2.7.0"
libraryDependencies += "com.github.karelcemus" %% "play-redis" % "4.0.0"

pipelineStages := Seq(digest)
4 changes: 2 additions & 2 deletions conf/application.base.conf
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# The base config file for all dev and environment-specific config files

# This is needed for the play-redis library. Without it, it won't store auth information in the cache and you can't log in.
akka.actor.allow-java-serialization = "on"
akka.actor.warn-about-java-serializer-usage = "off"
pekko.actor.allow-java-serialization = "on"
pekko.actor.warn-about-java-serializer-usage = "off"

auth.secret = ${AUTH_SECRET}
auth.url=${AUTH_URL}
Expand Down
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.19")
addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.2")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
resolvers += Resolver.jcenterRepo
addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.4")
Expand Down
3 changes: 2 additions & 1 deletion test/controllers/AddAdditionalMetadataControllerSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package controllers

import akka.Done
import cats.implicits.catsSyntaxOptionId
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock.{okJson, post, urlEqualTo}
Expand All @@ -19,6 +18,8 @@ import io.circe.Printer
import io.circe.generic.auto._
import io.circe.parser.decode
import io.circe.syntax._
import org.apache.pekko.Done
import org.mockito.Mockito.when
import org.pac4j.play.scala.SecurityComponents
import org.scalatest.matchers.should.Matchers._
import play.api.Play.materializer
Expand Down
2 changes: 1 addition & 1 deletion test/controllers/ConfirmTransferControllerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ class ConfirmTransferControllerSpec extends FrontEndTestHelper {

confirmTransferPageAsString must include(
s""" <dd class="govuk-summary-list__value">
| ${consignmentSummaryResponse.totalFiles} files uploaded
| ${consignmentSummaryResponse.totalFiles} files&nbsp;uploaded
| </dd>""".stripMargin
)

Expand Down
10 changes: 8 additions & 2 deletions test/errors/ErrorHandlerSpec.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package errors

import org.pac4j.core.config.Config
import org.pac4j.core.context.session.SessionStore
import org.pac4j.core.context.FrameworkParameters
import org.pac4j.core.context.session.{SessionStore, SessionStoreFactory}
import org.pac4j.core.exception.TechnicalException
import org.pac4j.core.profile.CommonProfile
import org.pac4j.play.scala.Pac4jScalaTemplateHelper
Expand All @@ -19,7 +20,12 @@ import java.util.concurrent.CompletionException
class ErrorHandlerSpec extends AnyFlatSpec with Matchers {

val sessionStore: SessionStore = mock[SessionStore]
val pac4jTemplateHelper: Pac4jScalaTemplateHelper[CommonProfile] = new Pac4jScalaTemplateHelper[CommonProfile](sessionStore, Config.INSTANCE)
val config = new Config()
config.setSessionStoreFactory(new SessionStoreFactory {
override def newSessionStore(parameters: FrameworkParameters): SessionStore = sessionStore
})

val pac4jTemplateHelper: Pac4jScalaTemplateHelper[CommonProfile] = new Pac4jScalaTemplateHelper[CommonProfile](config)
val errorHandler = new ErrorHandler(new DefaultMessagesApi(), pac4jTemplateHelper)

"client error handler" should "return a Default response for any status code not explicitly handled" in {
Expand Down
Loading

0 comments on commit 73c419c

Please sign in to comment.