controllers.Auth0Controller.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.controllers
import akka.http.scaladsl.util.FastFuture
import akka.util.ByteString
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import otoroshi.actions.{BackOfficeAction, BackOfficeActionAuth, PrivateAppsAction}
import otoroshi.auth._
import otoroshi.env.Env
import otoroshi.events._
import otoroshi.gateway.Errors
import otoroshi.models.{BackOfficeUser, CorsSettings, PrivateAppsUser, ServiceDescriptor}
import otoroshi.next.models.NgRoute
import otoroshi.next.plugins.{MultiAuthModule, NgMultiAuthModuleConfig}
import otoroshi.security.IdGenerator
import otoroshi.utils.syntax.implicits._
import otoroshi.utils.{RegexPool, TypedMap}
import play.api.Logger
import play.api.libs.json._
import play.api.mvc._
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.Base64
import java.util.concurrent.TimeUnit
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.jdk.CollectionConverters._
object AuthController {
val logger = Logger("otoroshi-auth-controller")
}
class AuthController(
BackOfficeActionAuth: BackOfficeActionAuth,
PrivateAppsAction: PrivateAppsAction,
BackOfficeAction: BackOfficeAction,
cc: ControllerComponents
)(implicit env: Env)
extends AbstractController(cc) {
implicit lazy val ec = env.otoroshiExecutionContext
lazy val logger = AuthController.logger
def decryptState(req: RequestHeader): JsValue = {
val secretToBytes = env.otoroshiSecret.padTo(16, "0").mkString("").take(16).getBytes
val cipher: Cipher = Cipher.getInstance("AES")
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secretToBytes, "AES"))
val decoded =
java.util.Base64.getUrlDecoder.decode(req.getQueryString("state").getOrElse(Json.stringify(Json.obj())))
scala.util.Try {
Json.parse(new String(cipher.doFinal(decoded)))
} recover { case _ =>
Json.obj()
} get
}
def verifyHash(descId: String, auth: AuthModuleConfig, req: RequestHeader)(
f: AuthModuleConfig => Future[Result]
): Future[Result] = {
val hash: String = auth match {
case module: GenericOauth2ModuleConfig if module.noWildcardRedirectURI =>
val unsignedState = decryptState(req)
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("--")
}
val expected = env.sign(s"${auth.id}:::$descId")
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger.debug(s"verifyHash - found hash: ${hash}, expected: ${expected}")
}
if (
(hash != "--" && hash == expected) || auth.isInstanceOf[SamlAuthModuleConfig] || auth
.isInstanceOf[Oauth1ModuleConfig]
) {
f(auth)
} else {
Errors.craftResponseResult(
"Auth. config. bad signature",
Results.BadRequest,
req,
None,
Some("errors.auth.bad.signature"),
attrs = TypedMap.empty
)
}
}
def withMultiAuthConfig(route: NgRoute, req: RequestHeader, refFromRelayState: Option[String] = None)(
f: AuthModuleConfig => Future[Result]
): Future[Result] = {
lazy val error = Errors.craftResponseResult(
"Multi Auth. config. ref not found on the route",
Results.InternalServerError,
req,
None,
Some("errors.multi.auth.config.ref.not.found"),
maybeRoute = Some(route),
attrs = TypedMap.empty
)
route.plugins.getPluginByClass[MultiAuthModule] match {
case None => error
case Some(authPlugin) => {
(req.getQueryString("ref"), refFromRelayState) match {
case (Some(ref), _) =>
env.proxyState.authModuleAsync(ref).flatMap {
case None => error
case Some(auth) => f(auth)
}
case (_, Some(ref)) =>
env.proxyState.authModuleAsync(ref).flatMap {
case None => error
case Some(auth) => f(auth)
}
case (_, _) =>
req.getQueryString("email") match {
case None => error
case Some(email) =>
NgMultiAuthModuleConfig.format.reads(authPlugin.config.raw) match {
case JsSuccess(config, _) =>
config.usersGroups.value
.find(p =>
p._2
.asOpt[Seq[String]]
.getOrElse(Seq.empty)
.exists(em => RegexPool.theRegex(em).map(e => e.matches(email)).getOrElse(email == em))
) match {
case Some(auth) =>
env.proxyState.authModuleAsync(auth._1).flatMap {
case None => error
case Some(auth) => f(auth)
}
case None =>
logger.error(s"Email does not match any groups")
error
}
case JsError(_) =>
logger.error(s"Failed to read auth module configuration")
error
}
}
}
}
}
}
def withAuthConfig(descriptor: ServiceDescriptor, req: RequestHeader)(
f: AuthModuleConfig => Future[Result]
): Future[Result] = {
descriptor.authConfigRef match {
case None =>
Errors.craftResponseResult(
"Auth. config. ref not found on the descriptor",
Results.InternalServerError,
req,
Some(descriptor),
Some("errors.auth.config.ref.not.found"),
attrs = TypedMap.empty
)
case Some(ref) => {
//env.datastores.authConfigsDataStore.findById(ref).flatMap {
env.proxyState.authModuleAsync(ref).flatMap {
case None =>
Errors.craftResponseResult(
"Auth. config. not found on the descriptor",
Results.InternalServerError,
req,
Some(descriptor),
Some("errors.auth.config.not.found"),
attrs = TypedMap.empty
)
case Some(auth) => f(auth)
}
}
}
}
def computeSec(user: PrivateAppsUser): String = env.aesEncrypt(user.json.stringify)
def confidentialAppLoginPageOptions() =
PrivateAppsAction.async { ctx =>
val cors = CorsSettings(
enabled = true,
allowOrigin = s"${env.rootScheme}${env.privateAppsHost}",
exposeHeaders = Seq.empty[String],
allowHeaders = Seq.empty[String],
allowMethods = Seq("GET")
)
if (cors.shouldNotPass(ctx.request)) {
Errors.craftResponseResult(
"Cors error",
Results.BadRequest,
ctx.request,
None,
Some("errors.cors.error"),
attrs = TypedMap.empty
)
} else {
FastFuture.successful(Results.Ok(ByteString.empty).withHeaders(cors.asHeaders(ctx.request): _*))
}
}
def confidentialAppSimpleLoginPage() =
multiLoginPage()((auths: JsObject, route: NgRoute, redirect: Option[String]) => {
Results
.Ok(otoroshi.views.html.privateapps.simplelogin(env, redirect, route.id))
.vfuture
})
def confidentialAppMultiLoginPage() =
multiLoginPage()((auths: JsObject, route: NgRoute, redirect: Option[String]) => {
Results
.Ok(otoroshi.views.html.privateapps.multilogin(env, Json.stringify(auths), redirect, route.id))
.vfuture
})
private def multiLoginPage()(f: (JsObject, NgRoute, Option[String]) => Future[Result]) = PrivateAppsAction.async {
ctx =>
ctx.request.getQueryString("route") match {
case None => NotFound(otoroshi.views.html.oto.error("Route not found", env)).vfuture
case Some(routeId) =>
env.proxyState.route(routeId).vfuture.flatMap {
case None => NotFound(otoroshi.views.html.oto.error("Route not found", env)).vfuture
case Some(route) if !route.plugins.hasPlugin[MultiAuthModule] =>
NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
case Some(route) if route.plugins.hasPlugin[MultiAuthModule] && route.id != env.backOfficeDescriptor.id =>
route.plugins.getPluginByClass[MultiAuthModule] match {
case Some(auth) =>
NgMultiAuthModuleConfig.format.reads(auth.config.raw) match {
case JsSuccess(config, _) =>
val auths = config.modules
.flatMap(module => env.proxyState.authModule(module))
.foldLeft(Json.obj("types" -> Json.obj())) { case (acc, auth) =>
(acc ++ Json.obj(auth.id -> auth.name)).deepMerge(
Json.obj(
"types" -> Json.obj(
auth.id -> auth.`type`
)
)
)
}
val redirect = ctx.request
.getQueryString("redirect")
.filter(redirect =>
ctx.request.getQueryString("hash").contains(env.sign(s"desc=${routeId}&redirect=${redirect}"))
)
.map(redirectBase64Encoded =>
new String(Base64.getUrlDecoder.decode(redirectBase64Encoded), StandardCharsets.UTF_8)
)
f(auths, route, redirect)
case JsError(errors) =>
logger.error(s"Failed to parse multi auth configuration, $errors")
NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
}
case None => NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
}
case _ => NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
}
}
}
def confidentialAppLoginPage() =
PrivateAppsAction.async { ctx =>
import otoroshi.utils.http.RequestImplicits._
implicit val req = ctx.request
(req.getQueryString("desc"), req.getQueryString("route")) match {
case (None, None) => NotFound(otoroshi.views.html.oto.error("Service not found", env)).vfuture
case (_, Some(route)) =>
env.proxyState.route(route).vfuture.flatMap {
case None => NotFound(otoroshi.views.html.oto.error("Route not found", env)).vfuture
case Some(route) if !route.plugins.hasPlugin[MultiAuthModule] =>
NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
case Some(route) if route.plugins.hasPlugin[MultiAuthModule] && route.id != env.backOfficeDescriptor.id => {
withMultiAuthConfig(route, req) { auth =>
val expectedCookieName = s"oto-papps-${auth.routeCookieSuffix(route)}"
req.cookies.find(c => c.name == expectedCookieName) match {
case Some(cookie) => {
env.extractPrivateSessionId(cookie) match {
case None =>
auth.authModule(ctx.globalConfig).paLoginPage(req, ctx.globalConfig, route.legacy, true)
case Some(sessionId) =>
env.datastores.privateAppsUserDataStore.findById(sessionId).flatMap {
case None =>
auth.authModule(ctx.globalConfig).paLoginPage(req, ctx.globalConfig, route.legacy, true)
case Some(user) =>
val sec = computeSec(user)
val secStr = if (auth.clientSideSessionEnabled) s"&sec=${sec}" else ""
req
.getQueryString("redirect")
.filter(redirect =>
req.getQueryString("hash").contains(env.sign(s"desc=${route.id}&redirect=${redirect}"))
)
.map(redirectBase64Encoded =>
new String(Base64.getUrlDecoder.decode(redirectBase64Encoded), StandardCharsets.UTF_8)
)
.getOrElse(s"${req.theProtocol}://${req.theHost}${req.relativeUri}") match {
case "urn:ietf:wg:oauth:2.0:oob" => {
val redirection =
s"${req.theProtocol}://${req.theHost}/.well-known/otoroshi/login?route=true&sessionId=${user.randomId}&redirectTo=urn:ietf:wg:oauth:2.0:oob&host=${req.theHost}&cp=${auth
.routeCookieSuffix(route)}&ma=${auth.sessionMaxAge}&httpOnly=${auth.sessionCookieValues.httpOnly}&secure=${auth.sessionCookieValues.secure}${secStr}"
val hash = env.sign(redirection)
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger.debug(
s"[session ${user.randomId}] redirection to '${redirection}' with hash '${hash}'"
)
}
FastFuture.successful(
Redirect(s"$redirection&hash=$hash")
.removingFromSession(
s"pa-redirect-after-login-${auth.routeCookieSuffix(route)}",
"desc"
)
.withCookies(
env.createPrivateSessionCookies(
req.theHost,
user.randomId,
route.legacy,
auth,
user.some
): _*
)
)
}
case redirectTo => {
// TODO - check if ref is needed
val encodedRedirectTo =
Base64.getUrlEncoder.encodeToString(redirectTo.getBytes(StandardCharsets.UTF_8))
val url =
new java.net.URL(
redirectTo
) //s"${req.theProtocol}://${req.theHost}${req.relativeUri}")
val host = url.getHost
val scheme = url.getProtocol
val setCookiesRedirect = url.getPort match {
case -1 =>
val redirection =
s"$scheme://$host/.well-known/otoroshi/login?route=true&sessionId=${user.randomId}&redirectTo=$encodedRedirectTo&host=$host&cp=${auth
.routeCookieSuffix(route)}&ma=${auth.sessionMaxAge}&httpOnly=${auth.sessionCookieValues.httpOnly}&secure=${auth.sessionCookieValues.secure}${secStr}"
val hash = env.sign(redirection)
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger.debug(
s"[session ${user.randomId}] redirection to '${redirection}' with hash '${hash}'"
)
}
s"$redirection&hash=$hash"
case port =>
val redirection =
s"$scheme://$host:$port/.well-known/otoroshi/login?route=true&sessionId=${user.randomId}&redirectTo=$encodedRedirectTo&host=$host&cp=${auth
.routeCookieSuffix(route)}&ma=${auth.sessionMaxAge}&httpOnly=${auth.sessionCookieValues.httpOnly}&secure=${auth.sessionCookieValues.secure}${secStr}"
val hash = env.sign(redirection)
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger.debug(
s"[session ${user.randomId}] redirection to '${redirection}' with hash '${hash}'"
)
}
s"$redirection&hash=$hash"
}
FastFuture.successful(
Redirect(setCookiesRedirect)
.removingFromSession(
s"pa-redirect-after-login-${auth.routeCookieSuffix(route)}",
"desc"
)
.withCookies(
env.createPrivateSessionCookies(
host,
user.randomId,
route.legacy,
auth,
user.some
): _*
)
)
}
}
}
}
}
case None =>
auth.authModule(ctx.globalConfig).paLoginPage(req, ctx.globalConfig, route.legacy, true)
}
}
}
case _ => NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
}
case (Some(serviceId), _) => {
env.datastores.serviceDescriptorDataStore.findOrRouteById(serviceId).flatMap {
case None => NotFound(otoroshi.views.html.oto.error("Service not found", env)).vfuture
case Some(descriptor) if !descriptor.privateApp =>
NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
case Some(descriptor) if descriptor.privateApp && descriptor.id != env.backOfficeDescriptor.id => {
withAuthConfig(descriptor, req) { auth =>
val expectedCookieName = s"oto-papps-${auth.cookieSuffix(descriptor)}"
req.cookies.find(c => c.name == expectedCookieName) match {
case Some(cookie) => {
env.extractPrivateSessionId(cookie) match {
case None =>
auth.authModule(ctx.globalConfig).paLoginPage(req, ctx.globalConfig, descriptor, false)
case Some(sessionId) => {
env.datastores.privateAppsUserDataStore.findById(sessionId).flatMap {
case None =>
auth.authModule(ctx.globalConfig).paLoginPage(req, ctx.globalConfig, descriptor, false)
case Some(user) =>
val sec = computeSec(user)
val secStr = if (auth.clientSideSessionEnabled) s"&sec=${sec}" else ""
req
.getQueryString("redirect")
.filter(redirect =>
req.getQueryString("hash").contains(env.sign(s"desc=${serviceId}&redirect=${redirect}"))
)
.map(redirectBase64Encoded =>
new String(Base64.getUrlDecoder.decode(redirectBase64Encoded), StandardCharsets.UTF_8)
)
.getOrElse(s"${req.theProtocol}://${req.theHost}${req.relativeUri}") match {
case "urn:ietf:wg:oauth:2.0:oob" => {
val redirection =
s"${req.theProtocol}://${req.theHost}/.well-known/otoroshi/login?sessionId=${user.randomId}&redirectTo=urn:ietf:wg:oauth:2.0:oob&host=${req.theHost}&cp=${auth
.cookieSuffix(descriptor)}&ma=${auth.sessionMaxAge}&httpOnly=${auth.sessionCookieValues.httpOnly}&secure=${auth.sessionCookieValues.secure}${secStr}"
val hash = env.sign(redirection)
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger.debug(
s"[session ${user.randomId}] redirection to '${redirection}' with hash '${hash}'"
)
}
FastFuture.successful(
Redirect(s"$redirection&hash=$hash")
.removingFromSession(
s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}",
"desc"
)
.withCookies(
env.createPrivateSessionCookies(
req.theHost,
user.randomId,
descriptor,
auth,
user.some
): _*
)
)
}
case redirectTo => {
val encodedRedirectTo =
Base64.getUrlEncoder.encodeToString(redirectTo.getBytes(StandardCharsets.UTF_8))
val url =
new java.net.URL(
redirectTo
) // new java.net.URL(s"${req.theProtocol}://${req.theHost}${req.relativeUri}")
val host = url.getHost
val scheme = url.getProtocol
val setCookiesRedirect = url.getPort match {
case -1 =>
val redirection =
s"$scheme://$host/.well-known/otoroshi/login?sessionId=${user.randomId}&redirectTo=$encodedRedirectTo&host=$host&cp=${auth
.cookieSuffix(descriptor)}&ma=${auth.sessionMaxAge}&httpOnly=${auth.sessionCookieValues.httpOnly}&secure=${auth.sessionCookieValues.secure}${secStr}"
val hash = env.sign(redirection)
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger.debug(
s"[session ${user.randomId}] redirection to '${redirection}' with hash '${hash}'"
)
}
s"$redirection&hash=$hash"
case port =>
val redirection =
s"$scheme://$host:$port/.well-known/otoroshi/login?sessionId=${user.randomId}&redirectTo=$encodedRedirectTo&host=$host&cp=${auth
.cookieSuffix(descriptor)}&ma=${auth.sessionMaxAge}&httpOnly=${auth.sessionCookieValues.httpOnly}&secure=${auth.sessionCookieValues.secure}${secStr}"
val hash = env.sign(redirection)
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger.debug(
s"[session ${user.randomId}] redirection to '${redirection}' with hash '${hash}'"
)
}
s"$redirection&hash=$hash"
}
FastFuture.successful(
Redirect(setCookiesRedirect)
.removingFromSession(
s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}",
"desc"
)
.withCookies(
env.createPrivateSessionCookies(
host,
user.randomId,
descriptor,
auth,
user.some
): _*
)
)
}
}
}
}
}
}
case None =>
auth.authModule(ctx.globalConfig).paLoginPage(req, ctx.globalConfig, descriptor, false)
}
}
}
case _ => NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
}
}
}
}
def confidentialAppLogout() =
PrivateAppsAction.async { ctx =>
implicit val req = ctx.request
val redirectToOpt: Option[String] = req.queryString.get("redirectTo").map(_.last)
val hostOpt: Option[String] = req.queryString.get("host").map(_.last)
val cookiePrefOpt: Option[String] = req.queryString.get("cp").map(_.last)
(redirectToOpt, hostOpt, cookiePrefOpt) match {
case (Some(redirectTo), Some(host), Some(cp)) =>
FastFuture.successful(
Redirect(redirectTo).discardingCookies(env.removePrivateSessionCookiesWithSuffix(host, cp): _*)
)
case _ =>
Errors.craftResponseResult(
"Missing parameters",
Results.BadRequest,
req,
None,
Some("errors.missing.parameters"),
attrs = TypedMap.empty
)
}
}
def confidentialAppCallback() =
PrivateAppsAction.async { ctx =>
import otoroshi.utils.http.RequestImplicits._
implicit val req = ctx.request
def saveUser(user: PrivateAppsUser, auth: AuthModuleConfig, descriptor: ServiceDescriptor, webauthn: Boolean)(
implicit req: RequestHeader
): Future[Result] = {
user
.save(Duration(auth.sessionMaxAge, TimeUnit.SECONDS))
.map { paUser =>
val sec = computeSec(paUser)
val secStr = if (auth.clientSideSessionEnabled) s"&sec=${sec}" else ""
if (logger.isDebugEnabled) logger.debug(s"Auth callback, creating session on the leader ${paUser.email}")
env.clusterAgent.createSession(paUser)
Alerts.send(
UserLoggedInAlert(env.snowflakeGenerator.nextIdStr(), env.env, paUser, ctx.from, ctx.ua, auth.id)
)
ctx.request.body.asFormUrlEncoded match {
case Some(body) if body.get("RelayState").exists(_.nonEmpty) =>
// val queryParams =
// body("RelayState").head.split("&").map { qParam => (qParam.split("=")(0), qParam.split("=")(1)) }
// val params = queryParams.groupBy(_._1).mapValues(_.map(_._2).head)
val params: Map[String, String] = {
try {
val decoded =
JWT.require(Algorithm.HMAC512(env.otoroshiSecret)).build().verify(body("RelayState").head)
decoded.getClaims.asScala.mapValues(_.asString()).filter(_._2 != null).toMap
} catch {
case t: Throwable =>
logger.error("error while verifying relay_state", t)
Map.empty[String, String]
}
}
val redirectTo = params.getOrElse(
"redirect_uri",
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
val url = new java.net.URL(redirectTo)
val host = url.getHost
Redirect(redirectTo)
.removingFromSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
.withCookies(
env.createPrivateSessionCookies(host, paUser.randomId, descriptor, auth, paUser.some): _*
)
case _ =>
ctx.request.session
.get(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}")
.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
) match {
case "urn:ietf:wg:oauth:2.0:oob" =>
val redirection =
s"${req.theProtocol}://${req.theHost}/.well-known/otoroshi/login?sessionId=${paUser.randomId}&redirectTo=urn:ietf:wg:oauth:2.0:oob&host=${req.theHost}&cp=${auth
.cookieSuffix(descriptor)}&ma=${auth.sessionMaxAge}&httpOnly=${auth.sessionCookieValues.httpOnly}&secure=${auth.sessionCookieValues.secure}${secStr}"
val hash = env.sign(redirection)
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger
.debug(s"[session ${user.randomId}] redirection to '${redirection}' with hash '${hash}'")
}
Redirect(
s"$redirection&hash=$hash"
).removingFromSession(s"pa-redirect-after-login-${auth.cookieSuffix(descriptor)}", "desc")
.withCookies(
env.createPrivateSessionCookies(req.theHost, user.randomId, descriptor, auth, user.some): _*
)
case redirectTo =>
val encodedRedirectTo =
Base64.getUrlEncoder.encodeToString(redirectTo.getBytes(StandardCharsets.UTF_8))
val url = new java.net.URL(redirectTo)
val host = url.getHost
val scheme = url.getProtocol
val setCookiesRedirect = url.getPort match {
case -1 =>
val redirection =
s"$scheme://$host/.well-known/otoroshi/login?sessionId=${paUser.randomId}&redirectTo=$encodedRedirectTo&host=$host&cp=${auth
.cookieSuffix(descriptor)}&ma=${auth.sessionMaxAge}&httpOnly=${auth.sessionCookieValues.httpOnly}&secure=${auth.sessionCookieValues.secure}${secStr}"
val hash = env.sign(redirection)
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger
.debug(s"[session ${user.randomId}] redirection to '${redirection}' with hash '${hash}'")
}
s"$redirection&hash=$hash"
case port =>
val redirection =
s"$scheme://$host:$port/.well-known/otoroshi/login?sessionId=${paUser.randomId}&redirectTo=$encodedRedirectTo&host=$host&cp=${auth
.cookieSuffix(descriptor)}&ma=${auth.sessionMaxAge}&httpOnly=${auth.sessionCookieValues.httpOnly}&secure=${auth.sessionCookieValues.secure}${secStr}"
val hash = env.sign(redirection)
if (otoroshi.controllers.AuthController.logger.isDebugEnabled) {
otoroshi.controllers.AuthController.logger
.debug(s"[session ${user.randomId}] redirection to '${redirection}' with hash '${hash}'")
}
s"$redirection&hash=$hash"
}
if (webauthn) {
Ok(Json.obj("location" -> setCookiesRedirect))
.removingFromSession(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")
.withCookies(
env.createPrivateSessionCookies(host, paUser.randomId, descriptor, auth, paUser.some): _*
)
}
}
}
}
}
var desc = ctx.request
.getQueryString("desc")
.orElse(ctx.request.session.get("desc"))
var isRoute = ctx.request
.getQueryString("route")
.orElse(ctx.request.session.get("route"))
.contains("true")
var refFromRelayState: Option[String] = ctx.request.session.get("ref")
ctx.request.body.asFormUrlEncoded match {
case Some(body) =>
if (body.get("RelayState").exists(_.nonEmpty)) {
// val queryParams =
// body("RelayState").head.split("&").map { qParam => (qParam.split("=")(0), qParam.split("=")(1)) }
// val params = queryParams.groupBy(_._1).mapValues(_.map(_._2).head)
val params: Map[String, String] = {
try {
val decoded = JWT.require(Algorithm.HMAC512(env.otoroshiSecret)).build().verify(body("RelayState").head)
decoded.getClaims.asScala.mapValues(_.asString()).filter(_._2 != null).toMap
} catch {
case t: Throwable =>
logger.error("error while verifying relay_state", t)
Map.empty[String, String]
}
}
if (!params.contains("hash") || !params.contains("redirect_uri") || !params.contains("desc"))
NotFound(otoroshi.views.html.oto.error("Service not found", env)).vfuture
else {
desc = params("desc").some
refFromRelayState = params.get("ref")
if (!isRoute)
isRoute = params.get("route").forall(_ == "true")
}
}
case None =>
}
def processService(serviceId: String) = {
if (logger.isDebugEnabled) logger.debug(s"redirect to service descriptor : $serviceId")
env.datastores.serviceDescriptorDataStore.findOrRouteById(serviceId).flatMap {
case None => NotFound(otoroshi.views.html.oto.error("Service not found", env)).vfuture
case Some(descriptor) if !descriptor.privateApp =>
NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
case Some(descriptor) if descriptor.privateApp && descriptor.id != env.backOfficeDescriptor.id => {
withAuthConfig(descriptor, ctx.request) { _auth =>
verifyHash(descriptor.id, _auth, ctx.request) {
case auth if auth.`type` == "basic" && auth.asInstanceOf[BasicAuthModuleConfig].webauthn => {
val authModule = auth.authModule(ctx.globalConfig).asInstanceOf[BasicAuthModule]
req.headers.get("WebAuthn-Login-Step") match {
case Some("start") => {
authModule.webAuthnLoginStart(ctx.request.body.asJson.get, descriptor).map {
case Left(error) => BadRequest(Json.obj("error" -> error))
case Right(reg) => Ok(reg)
}
}
case Some("finish") => {
authModule.webAuthnLoginFinish(ctx.request.body.asJson.get, descriptor).flatMap {
case Left(error) => {
logger.error(
s"login remote validation failed: ${error.display} - ${error.internal.map(_.stringify).getOrElse("")}"
)
BadRequest(Json.obj("error" -> error.display)).vfuture
}
case Right(user) => saveUser(user, auth, descriptor, true)(ctx.request)
}
}
case _ =>
BadRequest(
otoroshi.views.html.oto
.error(message = s"Missing step", _env = env, title = "Authorization error")
).vfuture
}
}
case auth => {
auth
.authModule(ctx.globalConfig)
.paCallback(ctx.request, ctx.globalConfig, descriptor)
.flatMap {
case Left(error) => {
BadRequest(
otoroshi.views.html.oto
.error(
message = s"You're not authorized here: ${error}",
_env = env,
title = "Authorization error"
)
).vfuture
}
case Right(user) => saveUser(user, auth, descriptor, false)(ctx.request)
}
}
}
}
}
case _ => NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
}
}
def processRoute(routeId: String) = {
if (logger.isDebugEnabled) logger.debug(s"redirect to route : $routeId")
env.proxyState.route(routeId).vfuture.flatMap {
case None => NotFound(otoroshi.views.html.oto.error("Route not found", env)).vfuture
case Some(route)
if route.plugins
.hasPlugin[otoroshi.next.plugins.AuthModule] && route.id != env.backOfficeDescriptor.id => {
withAuthConfig(route.serviceDescriptor, ctx.request) { _auth =>
verifyHash(route.id, _auth, ctx.request) {
case auth if auth.`type` == "basic" && auth.asInstanceOf[BasicAuthModuleConfig].webauthn => {
val authModule = auth.authModule(ctx.globalConfig).asInstanceOf[BasicAuthModule]
req.headers.get("WebAuthn-Login-Step") match {
case Some("start") => {
authModule.webAuthnLoginStart(ctx.request.body.asJson.get, route.legacy).map {
case Left(error) => BadRequest(Json.obj("error" -> error))
case Right(reg) => Ok(reg)
}
}
case Some("finish") => {
authModule.webAuthnLoginFinish(ctx.request.body.asJson.get, route.legacy).flatMap {
case Left(error) => {
logger.error(
s"login remote validation failed: ${error.display} - ${error.internal.map(_.stringify).getOrElse("")}"
)
BadRequest(Json.obj("error" -> error.display)).vfuture
}
case Right(user) => saveUser(user, auth, route.legacy, true)(ctx.request)
}
}
case _ =>
BadRequest(
otoroshi.views.html.oto
.error(message = s"Missing step", _env = env, title = "Authorization error")
).vfuture
}
}
case auth => {
auth
.authModule(ctx.globalConfig)
.paCallback(ctx.request, ctx.globalConfig, route.legacy)
.flatMap {
case Left(error) => {
BadRequest(
otoroshi.views.html.oto
.error(
message = s"You're not authorized here: ${error}",
_env = env,
title = "Authorization error"
)
).vfuture
}
case Right(user) => saveUser(user, auth, route.legacy, false)(ctx.request)
}
}
}
}
}
case Some(route) if route.plugins.hasPlugin[MultiAuthModule] && route.id != env.backOfficeDescriptor.id => {
withMultiAuthConfig(route, ctx.request, refFromRelayState) { _auth =>
verifyHash(route.id, _auth, ctx.request) {
case auth if auth.`type` == "basic" && auth.asInstanceOf[BasicAuthModuleConfig].webauthn => {
val authModule = auth.authModule(ctx.globalConfig).asInstanceOf[BasicAuthModule]
req.headers.get("WebAuthn-Login-Step") match {
case Some("start") => {
authModule.webAuthnLoginStart(ctx.request.body.asJson.get, route.legacy).map {
case Left(error) => BadRequest(Json.obj("error" -> error))
case Right(reg) => Ok(reg)
}
}
case Some("finish") => {
authModule.webAuthnLoginFinish(ctx.request.body.asJson.get, route.legacy).flatMap {
case Left(error) => {
logger.error(
s"login remote validation failed: ${error.display} - ${error.internal.map(_.stringify).getOrElse("")}"
)
BadRequest(Json.obj("error" -> error.display)).vfuture
}
case Right(user) => saveUser(user, auth, route.legacy, true)(ctx.request)
}
}
case _ =>
BadRequest(
otoroshi.views.html.oto
.error(message = s"Missing step", _env = env, title = "Authorization error")
).vfuture
}
}
case auth => {
auth
.authModule(ctx.globalConfig)
.paCallback(ctx.request, ctx.globalConfig, route.legacy)
.flatMap {
case Left(error) => {
BadRequest(
otoroshi.views.html.oto
.error(
message = s"You're not authorized here: ${error}",
_env = env,
title = "Authorization error"
)
).vfuture
}
case Right(user) => saveUser(user, auth, route.legacy, false)(ctx.request)
}
}
}
}
}
case _ => NotFound(otoroshi.views.html.oto.error("Private apps are not configured", env)).vfuture
}
}
val stt = ctx.request.getQueryString("state")
// val bod: Map[String, Seq[String]] = ctx.request.body.asFormUrlEncoded.getOrElse(Map.empty[String, Seq[String]])
// val context = Json.obj(
// "desc" -> desc.map(JsString.apply).getOrElse(JsNull).as[JsValue],
// "isRoute" -> isRoute,
// "refFromRelayState" -> refFromRelayState.map(JsString.apply).getOrElse(JsNull).as[JsValue],
// "state" -> stt.map(JsString.apply).getOrElse(JsNull).as[JsValue],
// "request" -> Json.obj(
// "query" -> req.queryString,
// "headers" -> req.headers.toMap,
// "session" -> req.session.data,
// "body" -> bod,
// )
// )
// logger.info(s"confidentialAppCallback context: ${context.prettify}")
((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"))
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 _ =>
NotFound(otoroshi.views.html.oto.error(s"${if (isRoute) "Route" else "service"} not found", env)).vfuture
}
case (_, _) =>
NotFound(otoroshi.views.html.oto.error(s"${if (isRoute) "Route" else "service"} not found", env)).vfuture
})
.recover {
case t: Throwable => {
val errorId = IdGenerator.uuid
logger.error(s"An error occurred during the authentication callback with error id: '${errorId}'", t)
InternalServerError(
otoroshi.views.html.oto
.error(
message =
s"An error occurred during the authentication callback. Please contact your administrator with error id: ${errorId}",
_env = env,
title = "Authorization error"
)
)
}
}
}
def auth0error(error: Option[String], error_description: Option[String]) =
BackOfficeAction { ctx =>
val errorId = IdGenerator.token(16)
logger.error(
s"[AUTH0 ERROR] error_id: $errorId => ${error.getOrElse("--")} : ${error_description.getOrElse("--")}"
)
Redirect(routes.BackOfficeController.error(Some(s"Auth0 error - logged with id: $errorId")))
}
def backOfficeLogin() =
BackOfficeAction.async { ctx =>
implicit val request = ctx.request
env.datastores.globalConfigDataStore.singleton().flatMap {
case config if !(config.u2fLoginOnly || config.backOfficeAuthRef.isEmpty) => {
config.backOfficeAuthRef match {
case None => FastFuture.successful(Redirect(otoroshi.controllers.routes.BackOfficeController.index))
case Some(aconf) => {
//env.datastores.authConfigsDataStore.findById(aconf).flatMap {
env.proxyState.authModuleAsync(aconf).flatMap {
case None =>
FastFuture
.successful(NotFound(otoroshi.views.html.oto.error("BackOffice Oauth is not configured", env)))
case Some(oauth) => oauth.authModule(config).boLoginPage(ctx.request, config)
}
}
}
}
case config if config.u2fLoginOnly || config.backOfficeAuthRef.isEmpty =>
FastFuture.successful(Redirect(otoroshi.controllers.routes.BackOfficeController.index))
}
}
def backOfficeLogout() =
BackOfficeActionAuth.async { ctx =>
import otoroshi.utils.http.RequestImplicits._
implicit val request = ctx.request
val redirect = request.getQueryString("redirect")
ctx.user.simpleLogin match {
case true =>
ctx.user.delete().map { _ =>
Alerts.send(AdminLoggedOutAlert(env.snowflakeGenerator.nextIdStr(), env.env, ctx.user, ctx.from, ctx.ua))
val userRedirect = redirect.getOrElse(routes.BackOfficeController.index.url)
Redirect(userRedirect).removingFromSession("bousr", "bo-redirect-after-login")
}
case false => {
env.datastores.globalConfigDataStore.singleton().flatMap { config =>
config.backOfficeAuthRef match {
case None => {
ctx.user.delete().map { _ =>
Alerts
.send(AdminLoggedOutAlert(env.snowflakeGenerator.nextIdStr(), env.env, ctx.user, ctx.from, ctx.ua))
val userRedirect = redirect.getOrElse(routes.BackOfficeController.index.url)
Redirect(userRedirect).removingFromSession("bousr", "bo-redirect-after-login")
}
}
case Some(aconf) => {
//env.datastores.authConfigsDataStore.findById(aconf).flatMap {
env.proxyState.authModuleAsync(aconf).flatMap {
case None =>
FastFuture.successful(
NotFound(otoroshi.views.html.oto.error("BackOffice auth is not configured", env))
.removingFromSession("bousr", "bo-redirect-after-login")
)
case Some(oauth) =>
oauth.authModule(config).boLogout(ctx.request, ctx.user, config).flatMap {
case Left(body) =>
ctx.user.delete().map { _ =>
Alerts.send(
AdminLoggedOutAlert(env.snowflakeGenerator.nextIdStr(), env.env, ctx.user, ctx.from, ctx.ua)
)
body.removingFromSession("bousr", "bo-redirect-after-login")
}
case Right(value) =>
value match {
case None =>
ctx.user.delete().map { _ =>
Alerts.send(
AdminLoggedOutAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
ctx.user,
ctx.from,
ctx.ua
)
)
val userRedirect = redirect.getOrElse(routes.BackOfficeController.index.url)
Redirect(userRedirect).removingFromSession("bousr", "bo-redirect-after-login")
}
case Some(logoutUrl) =>
val userRedirect = redirect.getOrElse(s"${request.theProtocol}://${request.theHost}/")
val actualRedirectUrl =
logoutUrl.replace("${redirect}", URLEncoder.encode(userRedirect, "UTF-8"))
ctx.user.delete().map { _ =>
Alerts.send(
AdminLoggedOutAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
ctx.user,
ctx.from,
ctx.ua
)
)
Redirect(actualRedirectUrl).removingFromSession("bousr", "bo-redirect-after-login")
}
}
}
}
}
}
}
}
}
}
def backOfficeCallback(error: Option[String] = None, error_description: Option[String] = None) = {
BackOfficeAction.async { ctx =>
implicit val request = ctx.request
def saveUser(user: BackOfficeUser, auth: AuthModuleConfig, webauthn: Boolean)(implicit req: RequestHeader) = {
user
.save(Duration(env.backOfficeSessionExp, TimeUnit.MILLISECONDS))
.map { boUser =>
env.datastores.backOfficeUserDataStore.hasAlreadyLoggedIn(user.email).map {
case false => {
env.datastores.backOfficeUserDataStore.alreadyLoggedIn(user.email)
Alerts.send(AdminFirstLogin(env.snowflakeGenerator.nextIdStr(), env.env, boUser, ctx.from, ctx.ua))
}
case true => {
Alerts
.send(
AdminLoggedInAlert(env.snowflakeGenerator.nextIdStr(), env.env, boUser, ctx.from, ctx.ua, auth.id)
)
}
}
Redirect(
request.session
.get("bo-redirect-after-login")
.getOrElse(
routes.BackOfficeController.index
.absoluteURL(env.exposedRootSchemeIsHttps)
)
).removingFromSession("bo-redirect-after-login")
.addingToSession("bousr" -> boUser.randomId)
}
}
error match {
case Some(e) => FastFuture.successful(BadRequest(otoroshi.views.html.backoffice.unauthorized(env)))
case None => {
env.datastores.globalConfigDataStore.singleton().flatMap {
case config if config.u2fLoginOnly || config.backOfficeAuthRef.isEmpty =>
FastFuture.successful(Redirect(otoroshi.controllers.routes.BackOfficeController.index))
case config if !(config.u2fLoginOnly || config.backOfficeAuthRef.isEmpty) => {
config.backOfficeAuthRef match {
case None =>
FastFuture
.successful(NotFound(otoroshi.views.html.oto.error("BackOffice OAuth is not configured", env)))
case Some(backOfficeAuth0Config) => {
// env.datastores.authConfigsDataStore.findById(backOfficeAuth0Config).flatMap {
env.proxyState.authModuleAsync(backOfficeAuth0Config).flatMap {
case None =>
FastFuture
.successful(NotFound(otoroshi.views.html.oto.error("BackOffice OAuth is not found", env)))
case Some(oauth) =>
verifyHash("backoffice", oauth, ctx.request) {
case auth if auth.`type` == "basic" && auth.asInstanceOf[BasicAuthModuleConfig].webauthn => {
val authModule = auth.authModule(config).asInstanceOf[BasicAuthModule]
request.headers.get("WebAuthn-Login-Step") match {
case Some("start") => {
authModule.webAuthnAdminLoginStart(ctx.request.body.asJson.get).map {
case Left(error) => BadRequest(Json.obj("error" -> error))
case Right(reg) => Ok(reg)
}
}
case Some("finish") => {
authModule.webAuthnAdminLoginFinish(ctx.request.body.asJson.get).flatMap {
case Left(error) => {
logger.error(
s"login remote validation failed: ${error.display} - ${error.internal.map(_.stringify).getOrElse("")}"
)
BadRequest(Json.obj("error" -> error.display)).future
}
case Right(user) => saveUser(user, auth, true)(ctx.request)
}
}
case _ =>
BadRequest(
otoroshi.views.html.oto
.error(message = s"Missing step", _env = env, title = "Authorization error")
).asFuture
}
}
case auth => {
oauth.authModule(config).boCallback(ctx.request, config).flatMap {
case Left(err) =>
FastFuture.successful(
BadRequest(
otoroshi.views.html.oto
.error(
message = s"You're not authorized here: ${err}",
_env = env,
title = "Authorization error"
)
)
)
case Right(user) =>
if (logger.isDebugEnabled) logger.debug(s"Login successful for user '${user.email}'")
saveUser(user, auth, false)(ctx.request)
}
}
}
}
}
}
}
}
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy