diff --git a/otoroshi/app/auth/basic.scala b/otoroshi/app/auth/basic.scala index 5052169d9..85ec47fa1 100644 --- a/otoroshi/app/auth/basic.scala +++ b/otoroshi/app/auth/basic.scala @@ -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 @@ -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) ) @@ -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) ) diff --git a/otoroshi/app/auth/ldap.scala b/otoroshi/app/auth/ldap.scala index 14fc4ed46..d40deec5b 100644 --- a/otoroshi/app/auth/ldap.scala +++ b/otoroshi/app/auth/ldap.scala @@ -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 @@ -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) ) @@ -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) ) diff --git a/otoroshi/app/auth/oauth.scala b/otoroshi/app/auth/oauth.scala index 17a30061c..d25c48d3e 100644 --- a/otoroshi/app/auth/oauth.scala +++ b/otoroshi/app/auth/oauth.scala @@ -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, _} @@ -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", @@ -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] diff --git a/otoroshi/app/auth/oauth1.scala b/otoroshi/app/auth/oauth1.scala index dc16dd688..22b597860 100644 --- a/otoroshi/app/auth/oauth1.scala +++ b/otoroshi/app/auth/oauth1.scala @@ -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._ @@ -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, @@ -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") { diff --git a/otoroshi/app/auth/saml/SAMLClient.scala b/otoroshi/app/auth/saml/SAMLClient.scala index 54ef6f837..43cd5af4a 100644 --- a/otoroshi/app/auth/saml/SAMLClient.scala +++ b/otoroshi/app/auth/saml/SAMLClient.scala @@ -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._ @@ -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, diff --git a/otoroshi/app/auth/session.scala b/otoroshi/app/auth/session.scala new file mode 100644 index 000000000..154ee11d2 --- /dev/null +++ b/otoroshi/app/auth/session.scala @@ -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: _*) + } + } + } +} diff --git a/otoroshi/app/controllers/Auth0Controller.scala b/otoroshi/app/controllers/Auth0Controller.scala index 9a55637c5..2cf8a5b5e 100644 --- a/otoroshi/app/controllers/Auth0Controller.scala +++ b/otoroshi/app/controllers/Auth0Controller.scala @@ -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 @@ -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") @@ -316,7 +317,7 @@ class AuthController( } FastFuture.successful( Redirect(s"$redirection&hash=$hash") - .removingFromSession( + .removingFromPrivateAppSession( s"pa-redirect-after-login-${auth.routeCookieSuffix(route)}", "desc" ) @@ -367,7 +368,7 @@ class AuthController( } FastFuture.successful( Redirect(setCookiesRedirect) - .removingFromSession( + .removingFromPrivateAppSession( s"pa-redirect-after-login-${auth.routeCookieSuffix(route)}", "desc" ) @@ -434,7 +435,7 @@ class AuthController( } FastFuture.successful( Redirect(s"$redirection&hash=$hash") - .removingFromSession( + .removingFromPrivateAppSession( s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc" ) @@ -484,7 +485,7 @@ class AuthController( } FastFuture.successful( Redirect(setCookiesRedirect) - .removingFromSession( + .removingFromPrivateAppSession( s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc" ) @@ -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) @@ -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): _* ) @@ -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): _* ) @@ -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) => @@ -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 } diff --git a/otoroshi/app/controllers/PrivateAppsController.scala b/otoroshi/app/controllers/PrivateAppsController.scala index ef13ecc82..a9160a7c9 100644 --- a/otoroshi/app/controllers/PrivateAppsController.scala +++ b/otoroshi/app/controllers/PrivateAppsController.scala @@ -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) = diff --git a/otoroshi/app/env/Env.scala b/otoroshi/app/env/Env.scala index 624780b96..a59d60a98 100644 --- a/otoroshi/app/env/Env.scala +++ b/otoroshi/app/env/Env.scala @@ -13,7 +13,7 @@ import otoroshi.metrics.{HasMetrics, Metrics} import org.joda.time.DateTime import org.mindrot.jbcrypt.BCrypt import org.slf4j.LoggerFactory -import otoroshi.auth.{AuthModuleConfig, SessionCookieValues} +import otoroshi.auth.{AuthModuleConfig, PrivateAppsSessionManager, SessionCookieValues} import otoroshi.cluster._ import otoroshi.events._ import otoroshi.gateway.{AnalyticsQueue, CircuitBreakersHolder} @@ -1167,6 +1167,9 @@ class Env( lazy val http2ClientProxyPort = configuration.getOptionalWithFileSupport[Int]("otoroshi.next.experimental.http2-client-proxy.port").getOrElse(8555) + lazy val privateAppsSessionManager: PrivateAppsSessionManager = new PrivateAppsSessionManager(this) + privateAppsSessionManager.printStatus() + lazy val defaultConfig = GlobalConfig( initWithNewEngine = true, trustXForwarded = initialTrustXForwarded, @@ -1612,7 +1615,7 @@ class Env( ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - lazy val sessionDomain = configuration.getOptionalWithFileSupport[String]("play.http.session.domain").get + lazy val sessionDomain = if (privateAppsSessionManager.isEnabled) privateAppsSessionManager.sessionDomain else configuration.getOptionalWithFileSupport[String]("play.http.session.domain").get lazy val playSecret = configuration.getOptionalWithFileSupport[String]("play.http.secret.key").get def sign(message: String): String = diff --git a/otoroshi/build.sbt b/otoroshi/build.sbt index 3796d635d..0758169d1 100644 --- a/otoroshi/build.sbt +++ b/otoroshi/build.sbt @@ -372,6 +372,8 @@ reStart / javaOptions ++= Seq( "-Dotoroshi.tunnels.default.url=http://127.0.0.1:9999", "-Dotoroshi.instance.name=dev", "-Dotoroshi.vaults.enabled=true", + "-Dotoroshi.privateapps.session.enabled=true", + "-Dotoroshi.loggers.otoroshi-papps-session-manager=DEBUG", //"-Dotoroshi.privateapps.subdomain=otoroshi", "-Dotoroshi.ssl.fromOutside.clientAuth=None", //"-Dotoroshi.ssl.fromOutside.clientAuth=Need", diff --git a/otoroshi/conf/application.conf b/otoroshi/conf/application.conf index 8691b3336..ec3bbd1f7 100644 --- a/otoroshi/conf/application.conf +++ b/otoroshi/conf/application.conf @@ -194,6 +194,27 @@ app { exp = 86400000 # the privateapps cookie expiration exp = ${?APP_PRIVATEAPPS_SESSION_EXP} # the privateapps cookie expiration exp = ${?OTOROSHI_PRIVATEAPPS_SESSION_EXP} # the privateapps cookie expiration + enabled = false + enabled = ${?OTOROSHI_PRIVATEAPPS_SESSION_ENABLED} + secure = false # the cookie for otoroshi privateapps should be exhanged over https only + secure = ${?OTOROSHI_PRIVATEAPPS_SESSION_SECURE_ONLY} # the cookie for otoroshi privateapps should be exhanged over https only + httpOnly = true # the cookie for otoroshi privateapps is not accessible from javascript + maxAge = 259200000 # the cookie for otoroshi privateapps max age + maxAge = ${?OTOROSHI_PRIVATEAPPS_SESSION_MAX_AGE} # the cookie for otoroshi privateapps max age + domain = ${app.privateapps.subdomain}"."${otoroshi.domain} # the cookie for otoroshi privateapps domain + domain = ${?OTOROSHI_PRIVATEAPPS_SESSION_DOMAIN} # the cookie for otoroshi privateapps domain + cookieName = "otoroshi-sso-session" # the cookie for otoroshi privateapps name + cookieName = ${?OTOROSHI_PRIVATEAPPS_SESSION_NAME} # the cookie for otoroshi privateapps name + sameSite = "lax" + sameSite = ${?OTOROSHI_PRIVATEAPPS_SESSION_SAMESITE} + jwt { + signatureAlgorithm = "HS256" + signatureAlgorithm = ${?OTOROSHI_PRIVATEAPPS_SESSION_JWT_ALG} + clockSkew = "30s" + clockSkew = ${?OTOROSHI_PRIVATEAPPS_SESSION_JWT_CLOCK_SKEW} + dataClaim = "data" + dataClaim = ${?OTOROSHI_PRIVATEAPPS_SESSION_JWT_DATA_CLAIM} + } } } adminapi {