Skip to content

Commit

Permalink
Merge pull request #396 from Opetushallitus/tor-xxxx-scala-cas-client…
Browse files Browse the repository at this point in the history
…-uri-korjaus

Scala CAS Client URI -korjaus
  • Loading branch information
alehuo authored Nov 12, 2024
2 parents 740aa1e + 9a08769 commit 6fdebc9
Show file tree
Hide file tree
Showing 8 changed files with 504 additions and 17 deletions.
22 changes: 7 additions & 15 deletions parser/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,13 @@
<scala.version>2.12.16</scala.version>
<scala.compat.version>2.12</scala.compat.version>
<ch.qos.logback.contrib.version>0.1.5</ch.qos.logback.contrib.version>
<http4s.version>0.23.13</http4s.version>
<http4s.version>0.23.14</http4s.version>
<org.json4s.version>4.0.6</org.json4s.version>
<com.amazonaws.version>1.12.772</com.amazonaws.version>
<spec2.version>4.16.1</spec2.version>
</properties>

<dependencies>

<!-- Internal dependencies -->
<dependency>
<groupId>fi.vm.sade</groupId>
<artifactId>scala-cas_2.12</artifactId>
<version>3.0.2-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.http4s</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- https://mvnrepository.com/artifact/org.http4s/http4s-blaze-client -->
<dependency>
<groupId>org.http4s</groupId>
Expand Down Expand Up @@ -88,6 +74,12 @@
<version>${scala.version}</version>
</dependency>

<dependency>
<groupId>org.scala-lang.modules</groupId>
<artifactId>scala-xml_2.12</artifactId>
<version>2.0.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/io.symphonia/lambda-logging -->
<dependency>
<groupId>io.symphonia</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package fi.vm.sade.utils.cas

import cats.effect.IO
import cats.effect.kernel.Resource
import cats.effect.std.Hotswap
import fi.vm.sade.utils.cas.CasClient.SessionCookie
import org.http4s.client.Client
import org.http4s.{Request, Response, Status}
import org.typelevel.ci.CIString


/**
* Middleware that handles CAS authentication automatically. Sessions are maintained by keeping
* a central cache of session cookies per service url. If a session cookie is not found for requested service, it is obtained using
* CasClient. Stale sessions are detected and refreshed automatically.
*/
object CasAuthenticatingClient extends Logging {
val DefaultSessionCookieName = "JSESSIONID"

private val sessions: collection.mutable.Map[CasParams, SessionCookie] = collection.mutable.Map.empty

def apply(
casClient: CasClient,
casParams: CasParams,
serviceClient: Client[IO],
clientCallerId: String,
sessionCookieName: String = DefaultSessionCookieName
): Client[IO] = {
def openWithCasSession(request: Request[IO], hotswap: Hotswap[IO, Response[IO]]): IO[Response[IO]] = {
getCasSession(casParams).flatMap(requestWithCasSession(request, hotswap, retry = true))
}

def requestWithCasSession
(request: Request[IO], hotswap: Hotswap[IO, Response[IO]], retry: Boolean)
(sessionCookie: SessionCookie)
: IO[Response[IO]] = {
val fullRequest = FetchHelper.addDefaultHeaders(
request.addCookie(sessionCookieName, sessionCookie),
clientCallerId
)
// Hotswap use inspired by http4s Retry middleware:
hotswap.swap(serviceClient.run(fullRequest)).flatMap {
case r: Response[IO] if sessionExpired(r) && retry =>
logger.info("Session for " + casParams + " expired")
refreshSession(casParams).flatMap(requestWithCasSession(request, hotswap, retry = false))
case r: Response[IO] => IO.pure(r)
}
}

def isRedirectToLogin(resp: Response[IO]): Boolean =
resp.headers.get(CIString("Location")).exists(_.exists(header =>
header.value.contains("/cas/login") || header.value.contains("/cas-oppija/login")
))

def sessionExpired(resp: Response[IO]): Boolean =
isRedirectToLogin(resp) || resp.status == Status.Unauthorized

def getCasSession(params: CasParams): IO[SessionCookie] = {
synchronized(sessions.get(params)) match {
case None =>
logger.debug(s"No existing $sessionCookieName found for " + params + ", creating new")
refreshSession(params)
case Some(session) =>
IO.pure(session)
}
}

def refreshSession(params: CasParams): IO[SessionCookie] = {
casClient.fetchCasSession(params, sessionCookieName).map { session =>
logger.debug("Storing new session for " + params)
synchronized(sessions.put(params, session))
session
}
}

Client { req =>
Hotswap.create[IO, Response[IO]].flatMap { hotswap =>
Resource.eval(openWithCasSession(req, hotswap))
}
}
}
}
Loading

0 comments on commit 6fdebc9

Please sign in to comment.