Skip to content

Commit

Permalink
ground work for #2056
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuancelin committed Dec 16, 2024
1 parent 1330b6b commit a7328c9
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 35 deletions.
7 changes: 4 additions & 3 deletions otoroshi/app/auth/basic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.google.common.base.Charsets
import com.yubico.webauthn._
import com.yubico.webauthn.data._
import otoroshi.controllers.{routes, LocalCredentialRepository}
import otoroshi.controllers.{LocalCredentialRepository, routes}
import otoroshi.env.Env
import otoroshi.models._
import org.joda.time.DateTime
import org.mindrot.jbcrypt.BCrypt
import otoroshi.auth.implicits.ResultWithPrivateAppSession
import otoroshi.models.{OtoroshiAdminType, UserRight, UserRights, WebAuthnOtoroshiAdmin}
import otoroshi.utils.syntax.implicits._
import play.api.Logger
Expand Down Expand Up @@ -339,7 +340,7 @@ case class BasicAuthModule(authConfig: BasicAuthModuleConfig) extends AuthModule
Results
.Unauthorized("")
.withHeaders("WWW-Authenticate" -> s"""Basic realm="${authConfig.cookieSuffix(descriptor)}"""")
.addingToSession(
.addingToPrivateAppSession(
s"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
Expand Down Expand Up @@ -382,7 +383,7 @@ case class BasicAuthModule(authConfig: BasicAuthModuleConfig) extends AuthModule
env
)
)
.addingToSession(
.addingToPrivateAppSession(
s"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
Expand Down
5 changes: 3 additions & 2 deletions otoroshi/app/auth/ldap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import akka.http.scaladsl.util.FastFuture
import com.google.common.base.Charsets
import org.apache.pulsar.client.api.PulsarClientException.AuthenticationException
import otoroshi.auth.LdapAuthModuleConfig.fromJson
import otoroshi.auth.implicits.ResultWithPrivateAppSession
import otoroshi.controllers.routes
import otoroshi.env.Env

Expand Down Expand Up @@ -806,7 +807,7 @@ case class LdapAuthModule(authConfig: LdapAuthModuleConfig) extends AuthModule {
Results
.Unauthorized(otoroshi.views.html.oto.error("You are not authorized here", env))
.withHeaders("WWW-Authenticate" -> s"""Basic realm="${authConfig.cookieSuffix(descriptor)}"""")
.addingToSession(
.addingToPrivateAppSession(
s"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
Expand Down Expand Up @@ -842,7 +843,7 @@ case class LdapAuthModule(authConfig: LdapAuthModuleConfig) extends AuthModule {
env
)
)
.addingToSession(
.addingToPrivateAppSession(
s"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
Expand Down
5 changes: 3 additions & 2 deletions otoroshi/app/auth/oauth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import akka.http.scaladsl.util.FastFuture
import com.auth0.jwt.JWT
import org.apache.commons.codec.binary.{Base64 => ApacheBase64}
import org.joda.time.DateTime
import otoroshi.auth.implicits.{RequestHeaderWithPrivateAppSession, ResultWithPrivateAppSession}
import otoroshi.controllers.routes
import otoroshi.env.Env
import otoroshi.models.{TeamAccess, TenantAccess, UserRight, UserRights, _}
Expand Down Expand Up @@ -351,7 +352,7 @@ case class GenericOauth2Module(authConfig: OAuth2ModuleConfig) extends AuthModul

Redirect(
if (authConfig.noWildcardRedirectURI) s"$loginUrl&state=$state" else loginUrl
).addingToSession(
).addingToPrivateAppSession(
sessionParams ++ Map(
// s"${authConfig.id}-desc" -> descriptor.id,
"route" -> s"$isRoute",
Expand Down Expand Up @@ -687,7 +688,7 @@ case class GenericOauth2Module(authConfig: OAuth2ModuleConfig) extends AuthModul
clientSecret,
redirectUri,
config,
request.session.get(s"${authConfig.id}-code_verifier")
request.privateAppSession.get(s"${authConfig.id}-code_verifier")
)
.flatMap { rawToken =>
val accessToken = (rawToken \ authConfig.accessTokenField).as[String]
Expand Down
5 changes: 3 additions & 2 deletions otoroshi/app/auth/oauth1.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package otoroshi.auth

import akka.http.scaladsl.util.FastFuture
import otoroshi.auth.implicits.{RequestHeaderWithPrivateAppSession, ResultWithPrivateAppSession}
import otoroshi.controllers.routes
import otoroshi.env.Env
import otoroshi.models._
Expand Down Expand Up @@ -354,7 +355,7 @@ case class Oauth1AuthModule(authConfig: Oauth1ModuleConfig) extends AuthModule {
val hash = env.sign(s"${authConfig.id}:::${descriptor.id}")
val oauth_token = parameters("oauth_token")
Redirect(s"${authConfig.authorizeURL}?oauth_token=$oauth_token&perms=read")
.addingToSession(
.addingToPrivateAppSession(
"oauth_token_secret" -> parameters("oauth_token_secret"),
"desc" -> descriptor.id,
"ref" -> authConfig.id,
Expand Down Expand Up @@ -466,7 +467,7 @@ case class Oauth1AuthModule(authConfig: Oauth1ModuleConfig) extends AuthModule {
authConfig.accessTokenURL,
method,
authConfig.consumerSecret,
Some(request.session.get("oauth_token_secret").get)
Some(request.privateAppSession.get("oauth_token_secret").orElse(request.session.get("oauth_token_secret")).get)
)

(if (method == "POST") {
Expand Down
9 changes: 3 additions & 6 deletions otoroshi/app/auth/saml/SAMLClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,13 @@ import org.opensaml.security.x509.BasicX509Credential
import org.opensaml.xmlsec.SignatureSigningParameters
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver
import org.opensaml.xmlsec.keyinfo.impl.{
ChainingKeyInfoCredentialResolver,
StaticKeyInfoCredentialResolver,
X509KeyInfoGeneratorFactory
}
import org.opensaml.xmlsec.keyinfo.impl.{ChainingKeyInfoCredentialResolver, StaticKeyInfoCredentialResolver, X509KeyInfoGeneratorFactory}
import org.opensaml.xmlsec.signature.Signature
import org.opensaml.xmlsec.signature.impl.SignatureBuilder
import org.opensaml.xmlsec.signature.support.{SignatureConstants, SignatureException, SignatureSupport}
import org.w3c.dom.ls.DOMImplementationLS
import org.w3c.dom.{Document, Node}
import otoroshi.auth.implicits.ResultWithPrivateAppSession
import otoroshi.controllers.routes
import otoroshi.env.Env
import otoroshi.models._
Expand Down Expand Up @@ -115,7 +112,7 @@ case class SAMLModule(authConfig: SamlAuthModuleConfig) extends AuthModule {
s"${authConfig.singleSignOnUrl}?SAMLRequest=${URLEncoder.encode(encoded, "UTF-8")}&RelayState=$relayState"
}
Redirect(redirectUrl)
.addingToSession(
.addingToPrivateAppSession(
s"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}" -> redirectTo,
"hash" -> env.sign(s"${authConfig.id}:::${descriptor.id}"),
"desc" -> descriptor.id,
Expand Down
115 changes: 115 additions & 0 deletions otoroshi/app/auth/session.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package otoroshi.auth

import otoroshi.env.Env
import otoroshi.utils.syntax.implicits.BetterConfiguration
import play.api.{Configuration, Logger}
import play.api.http.{JWTConfiguration, SecretConfiguration, SessionConfiguration}
import play.api.libs.crypto.CookieSignerProvider
import play.api.mvc.Cookie.SameSite
import play.api.mvc.{Cookie, DefaultSessionCookieBaker, RequestHeader, Result, Session}

import scala.concurrent.duration.{DurationInt, FiniteDuration}

class PrivateAppsSessionManager(env: Env) {

private val logger = Logger("otoroshi-papps-session-manager")

private val config = env.configuration.get[Configuration]("otoroshi.privateapps.session")

private val jwtconfig = env.configuration.get[Configuration]("otoroshi.privateapps.session.jwt")

private val enabled = config.getOptionalWithFileSupport[Boolean]("enabled").getOrElse(false)

private val sessionConfig = SessionConfiguration(
cookieName = config.getOptionalWithFileSupport[String]("cookieName").getOrElse("SSO_SESSION"),
secure = config.getOptionalWithFileSupport[Boolean]("secure").getOrElse(false),
maxAge = config.getOptionalWithFileSupport[FiniteDuration]("maxAge"),
httpOnly = config.getOptionalWithFileSupport[Boolean]("httpOnly").getOrElse(true),
domain = config.getOptionalWithFileSupport[String]("domain"),
path = config.getOptionalWithFileSupport[String]("path").getOrElse("/"),
sameSite = config.getOptionalWithFileSupport[String]("sameSite").flatMap(v => SameSite.parse(v)),
jwt = JWTConfiguration(
signatureAlgorithm = jwtconfig.getOptionalWithFileSupport[String]("signatureAlgorithm").getOrElse("HS256"),
expiresAfter = jwtconfig.getOptionalWithFileSupport[FiniteDuration]("expiresAfter"),
clockSkew = jwtconfig.getOptionalWithFileSupport[FiniteDuration]("clockSkew").getOrElse(30.seconds),
dataClaim = jwtconfig.getOptionalWithFileSupport[String]("dataClaim").getOrElse("data"),
)
)

private val secretConfig = SecretConfiguration(
secret = env.secretSession
)

private val backer = new DefaultSessionCookieBaker(
sessionConfig,
secretConfig,
new CookieSignerProvider(secretConfig).get
)

def printStatus(): Unit = {
if (enabled) {
env.logger.info("Private apps. session is enabled !")
} else {
env.logger.info("Private apps. session is disabled")
}
}

def isEnabled: Boolean = enabled

def sessionDomain: String = sessionConfig.domain.get

def decodeFromCookies(request: RequestHeader): Session = {
val s = backer.decodeFromCookie(request.cookies.get(sessionConfig.cookieName))
if (logger.isDebugEnabled) logger.debug(s"decodeFromCookies: ${s.data}")
s
}

def encodeAsCookie(session: Session): Cookie = {
val r = backer.encodeAsCookie(session)
if (logger.isDebugEnabled) logger.debug(s"encodeAsCookie: ${r}")
r
}
}

object implicits {
implicit class RequestHeaderWithPrivateAppSession(val rh: RequestHeader) extends AnyVal {
def privateAppSession(implicit env: Env): Session = {
if (env.privateAppsSessionManager.isEnabled) {
env.privateAppsSessionManager.decodeFromCookies(rh)
} else {
rh.session
}
}
}
implicit class ResultWithPrivateAppSession(val result: Result) extends AnyVal {
def privateAppSession(implicit request: RequestHeader, env: Env): Session = {
if (env.privateAppsSessionManager.isEnabled) {
env.privateAppsSessionManager.decodeFromCookies(request)
} else {
request.session
}
}
def withPrivateAppSession(session: Session)(implicit env: Env): Result = {
if (env.privateAppsSessionManager.isEnabled) {
result.withCookies(env.privateAppsSessionManager.encodeAsCookie(session))
} else {
result.withSession(session)
}
}
def addingToPrivateAppSession(values: (String, String)*)(implicit request: RequestHeader, env: Env): Result = {
if (env.privateAppsSessionManager.isEnabled) {
withPrivateAppSession(new Session(privateAppSession.data ++ values.toMap))
} else {
result.withSession(values: _*)
}
}

def removingFromPrivateAppSession(keys: String*)(implicit request: RequestHeader, env: Env): Result = {
if (env.privateAppsSessionManager.isEnabled) {
withPrivateAppSession(new Session(privateAppSession.data -- keys))
} else {
result.removingFromPrivateAppSession(keys: _*)
}
}
}
}
35 changes: 18 additions & 17 deletions otoroshi/app/controllers/Auth0Controller.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import otoroshi.actions.{BackOfficeAction, BackOfficeActionAuth, PrivateAppsAction}
import otoroshi.auth._
import otoroshi.auth.implicits._
import otoroshi.env.Env
import otoroshi.events._
import otoroshi.gateway.Errors
Expand Down Expand Up @@ -69,7 +70,7 @@ class AuthController(
if (logger.isDebugEnabled) logger.debug(s"Decoded state : ${Json.prettyPrint(unsignedState)}")
(unsignedState \ "hash").asOpt[String].getOrElse("--")
case _ =>
req.getQueryString("hash").orElse(req.session.get("hash")).getOrElse("--")
req.getQueryString("hash").orElse(req.privateAppSession.get("hash")).orElse(req.session.get("hash")).getOrElse("--")
}

val expected = env.sign(s"${auth.id}:::$descId")
Expand Down Expand Up @@ -316,7 +317,7 @@ class AuthController(
}
FastFuture.successful(
Redirect(s"$redirection&hash=$hash")
.removingFromSession(
.removingFromPrivateAppSession(
s"pa-redirect-after-login-${auth.routeCookieSuffix(route)}",
"desc"
)
Expand Down Expand Up @@ -367,7 +368,7 @@ class AuthController(
}
FastFuture.successful(
Redirect(setCookiesRedirect)
.removingFromSession(
.removingFromPrivateAppSession(
s"pa-redirect-after-login-${auth.routeCookieSuffix(route)}",
"desc"
)
Expand Down Expand Up @@ -434,7 +435,7 @@ class AuthController(
}
FastFuture.successful(
Redirect(s"$redirection&hash=$hash")
.removingFromSession(
.removingFromPrivateAppSession(
s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}",
"desc"
)
Expand Down Expand Up @@ -484,7 +485,7 @@ class AuthController(
}
FastFuture.successful(
Redirect(setCookiesRedirect)
.removingFromSession(
.removingFromPrivateAppSession(
s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}",
"desc"
)
Expand Down Expand Up @@ -583,13 +584,13 @@ class AuthController(
val host = url.getHost

Redirect(redirectTo)
.removingFromSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
.removingFromPrivateAppSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
.withCookies(
env.createPrivateSessionCookies(host, paUser.randomId, descriptor, auth, paUser.some): _*
)

case _ =>
ctx.request.session
ctx.request.privateAppSession
.get(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}")
.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
Expand All @@ -605,7 +606,7 @@ class AuthController(
}
Redirect(
s"$redirection&hash=$hash"
).removingFromSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
).removingFromPrivateAppSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
.withCookies(
env.createPrivateSessionCookies(req.theHost, user.randomId, descriptor, auth, user.some): _*
)
Expand Down Expand Up @@ -639,13 +640,13 @@ class AuthController(
}
if (webauthn) {
Ok(Json.obj("location" -> setCookiesRedirect))
.removingFromSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
.removingFromPrivateAppSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
.withCookies(
env.createPrivateSessionCookies(host, paUser.randomId, descriptor, auth, paUser.some): _*
)
} else {
Redirect(setCookiesRedirect)
.removingFromSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
.removingFromPrivateAppSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
.withCookies(
env.createPrivateSessionCookies(host, paUser.randomId, descriptor, auth, paUser.some): _*
)
Expand All @@ -657,14 +658,14 @@ class AuthController(

var desc = ctx.request
.getQueryString("desc")
.orElse(ctx.request.session.get("desc"))
.orElse(ctx.request.privateAppSession.get("desc"))

var isRoute = ctx.request
.getQueryString("route")
.orElse(ctx.request.session.get("route"))
.orElse(ctx.request.privateAppSession.get("route"))
.contains("true")

var refFromRelayState: Option[String] = ctx.request.session.get("ref")
var refFromRelayState: Option[String] = ctx.request.privateAppSession.get("ref")

ctx.request.body.asFormUrlEncoded match {
case Some(body) =>
Expand Down Expand Up @@ -886,14 +887,14 @@ class AuthController(

((desc, stt) match {
case (Some(serviceId), _) if !isRoute =>
processService(serviceId).map(_.removingFromSession("desc", "ref", "route"))
case (Some(routeId), _) if isRoute => processRoute(routeId).map(_.removingFromSession("desc", "ref", "route"))
processService(serviceId).map(_.removingFromPrivateAppSession("desc", "ref", "route"))
case (Some(routeId), _) if isRoute => processRoute(routeId).map(_.removingFromPrivateAppSession("desc", "ref", "route"))
case (_, Some(state)) =>
if (logger.isDebugEnabled) logger.debug(s"Received state : $state")
val unsignedState = decryptState(ctx.request.requestHeader)
(unsignedState \ "descriptor").asOpt[String] match {
case Some(route) if isRoute => processRoute(route).map(_.removingFromSession("desc", "ref", "route"))
case Some(service) if !isRoute => processService(service).map(_.removingFromSession("desc", "ref", "route"))
case Some(route) if isRoute => processRoute(route).map(_.removingFromPrivateAppSession("desc", "ref", "route"))
case Some(service) if !isRoute => processService(service).map(_.removingFromPrivateAppSession("desc", "ref", "route"))
case _ =>
NotFound(otoroshi.views.html.oto.error(s"${if (isRoute) "Route" else "service"} not found", env)).vfuture
}
Expand Down
2 changes: 1 addition & 1 deletion otoroshi/app/controllers/PrivateAppsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class PrivateAppsController(ApiAction: ApiAction, PrivateAppsAction: PrivateApps
// .getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
// )
) //.removingFromSession("pa-redirect-after-login")
) //.removingFromPrivateAppSession("pa-redirect-after-login")
}

def error(message: Option[String] = None) =
Expand Down
Loading

0 comments on commit a7328c9

Please sign in to comment.