Skip to content

Commit 96bb736

Browse files
fix #2203, fix #2204
1 parent edefd3d commit 96bb736

2 files changed

Lines changed: 195 additions & 2 deletions

File tree

otoroshi/app/auth/basic.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,6 @@ case class BasicAuthModule(authConfig: BasicAuthModuleConfig) extends AuthModule
244244
def this() = this(BasicAuthModule.defaultConfig)
245245

246246
def decodeBase64(encoded: String): String = new String(OtoroshiClaim.decoder.decode(encoded), Charsets.UTF_8)
247-
248247
def extractUsernamePassword(header: String): Option[(String, String)] = {
249248
val base64 = header.replace("Basic ", "").replace("basic ", "")
250249
Option(base64)
@@ -255,6 +254,7 @@ case class BasicAuthModule(authConfig: BasicAuthModuleConfig) extends AuthModule
255254

256255
}
257256

257+
258258
def bindUser(username: String, password: String, descriptor: ServiceDescriptor)(implicit
259259
env: Env,
260260
ec: ExecutionContext

otoroshi/app/next/plugins/auth.scala

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ package otoroshi.next.plugins
33
import akka.http.scaladsl.model.Uri
44
import akka.stream.Materializer
55
import akka.util.ByteString
6+
import com.github.blemale.scaffeine.{Cache, Scaffeine}
7+
import com.google.common.base.Charsets
68
import org.mindrot.jbcrypt.BCrypt
9+
import otoroshi.auth.{AuthModuleConfig, BasicAuthModule, BasicAuthModuleConfig, LdapAuthModule, LdapAuthModuleConfig}
10+
import otoroshi.auth.implicits.ResultWithPrivateAppSession
11+
import otoroshi.controllers.routes
712
import otoroshi.env.Env
813
import otoroshi.gateway.Errors
9-
import otoroshi.models.PrivateAppsUserHelper
14+
import otoroshi.models.{PrivateAppsUser, PrivateAppsUserHelper}
15+
import otoroshi.next.plugins.BasicAuthWithAuthModule.alreadyLoggedIn
16+
import otoroshi.next.plugins.api.NgAccess.NgAllowed
1017
import otoroshi.next.plugins.api._
18+
import otoroshi.security.OtoroshiClaim
1119
import otoroshi.utils.http.RequestImplicits._
1220
import otoroshi.utils.syntax.implicits._
1321
import play.api.Logger
@@ -18,6 +26,7 @@ import play.api.mvc.{Result, Results}
1826
import java.net.URLEncoder
1927
import java.nio.charset.StandardCharsets
2028
import java.util.Base64
29+
import scala.concurrent.duration.DurationInt
2130
import scala.concurrent.{ExecutionContext, Future}
2231
import scala.util.{Failure, Success, Try}
2332

@@ -745,3 +754,187 @@ class SimpleBasicAuth extends NgAccessValidator {
745754
}
746755
}
747756
}
757+
758+
class NgExpectedConsumer extends NgAccessValidator {
759+
760+
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
761+
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication)
762+
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
763+
764+
override def isAccessAsync: Boolean = true
765+
override def multiInstance: Boolean = true
766+
override def core: Boolean = true
767+
override def name: String = "Expected consumer"
768+
override def description: Option[String] = "This plugin expect that a user or an apikey made the call".some
769+
override def defaultConfigObject: Option[NgPluginConfig] = None
770+
override def noJsForm: Boolean = true
771+
772+
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
773+
774+
def error(status: Results.Status, msg: String, code: String): Future[NgAccess] = {
775+
Errors
776+
.craftResponseResult(
777+
msg,
778+
status,
779+
ctx.request,
780+
None,
781+
code.some,
782+
attrs = ctx.attrs,
783+
maybeRoute = ctx.route.some
784+
)
785+
.map(NgAccess.NgDenied.apply)
786+
}
787+
788+
ctx.attrs.get(otoroshi.plugins.Keys.UserKey) match {
789+
case None => ctx.attrs.get(otoroshi.plugins.Keys.ApiKeyKey) match {
790+
case None => error(Results.Unauthorized, "You're not authorized here !", "errors.auth.unauthorized")
791+
case Some(_) => NgAccess.NgAllowed.vfuture
792+
}
793+
case Some(_) => NgAccess.NgAllowed.vfuture
794+
}
795+
}
796+
}
797+
798+
case class BasicAuthWithAuthModuleConfig(ref: String = "", addAuthenticateHeader: Boolean = true) extends NgPluginConfig {
799+
def json: JsValue = BasicAuthWithAuthModuleConfig.format.writes(this)
800+
}
801+
802+
object BasicAuthWithAuthModuleConfig {
803+
val format = new Format[BasicAuthWithAuthModuleConfig] {
804+
805+
override def reads(json: JsValue): JsResult[BasicAuthWithAuthModuleConfig] = Try {
806+
BasicAuthWithAuthModuleConfig(
807+
ref = json.select("ref").asString,
808+
addAuthenticateHeader = json.select("add_authenticate_header").asOptBoolean.getOrElse(true)
809+
)
810+
} match {
811+
case Failure(e) => JsError(e.getMessage)
812+
case Success(e) => JsSuccess(e)
813+
}
814+
815+
override def writes(o: BasicAuthWithAuthModuleConfig): JsValue = Json.obj(
816+
"ref" -> o.ref,
817+
"add_authenticate_header" -> o.addAuthenticateHeader,
818+
)
819+
}
820+
val configFlow: Seq[String] = Seq("ref", "add_authenticate_header")
821+
val configSchema: Option[JsObject] = Some(
822+
Json.obj(
823+
"add_authenticate_header" -> Json.obj(
824+
"type" -> "boolean",
825+
"label" -> "Add WWW-Authenticate header"
826+
),
827+
"ref" -> Json.obj(
828+
"type" -> "select",
829+
"label" -> "Auth. module",
830+
"props" -> Json.obj(
831+
"optionsFrom" -> "/bo/api/proxy/api/auths",
832+
"optionsTransformer" -> Json.obj(
833+
"label" -> "name",
834+
"value" -> "id",
835+
)
836+
)
837+
)
838+
)
839+
)
840+
}
841+
842+
object BasicAuthWithAuthModule {
843+
844+
val cache: Cache[String, PrivateAppsUser] = Scaffeine().expireAfterWrite(1.minutes).build()
845+
846+
def alreadyLoggedIn(username: String, config: AuthModuleConfig): Option[PrivateAppsUser] = {
847+
cache.getIfPresent(s"${config.id}:${username.sha256}")
848+
}
849+
850+
def addLoggedIn(username: String, user: PrivateAppsUser, config: AuthModuleConfig): Unit = {
851+
cache.put(s"${config.id}:${username.sha256}", user)
852+
}
853+
}
854+
855+
class BasicAuthWithAuthModule extends NgAccessValidator {
856+
857+
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
858+
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication)
859+
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
860+
861+
override def isAccessAsync: Boolean = true
862+
override def multiInstance: Boolean = true
863+
override def core: Boolean = true
864+
override def name: String = "Basic auth. from auth. module"
865+
override def description: Option[String] = "This plugin enforces basic auth. authentication with users coming from LDAP and In-memory auth. modules".some
866+
override def defaultConfigObject: Option[NgPluginConfig] = BasicAuthWithAuthModuleConfig().some
867+
override def noJsForm: Boolean = true
868+
override def configFlow: Seq[String] = BasicAuthWithAuthModuleConfig.configFlow
869+
override def configSchema: Option[JsObject] = BasicAuthWithAuthModuleConfig.configSchema
870+
871+
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
872+
873+
val config = ctx.cachedConfig(internalName)(BasicAuthWithAuthModuleConfig.format).getOrElse(BasicAuthWithAuthModuleConfig())
874+
val descriptor = ctx.route.legacy
875+
876+
def unauthorized(opt: Option[AuthModuleConfig]): Result = {
877+
Results
878+
.Unauthorized(Json.obj("error" -> "forbidden"))
879+
.applyOnWithOpt(opt) {
880+
case (r, authConfig) if config.addAuthenticateHeader => r.withHeaders("WWW-Authenticate" -> s"""Basic realm="bawam-${authConfig.cookieSuffix(descriptor)}"""")
881+
case (r, _) => r
882+
}
883+
}
884+
885+
def decodeBase64(encoded: String): String = new String(OtoroshiClaim.decoder.decode(encoded), Charsets.UTF_8)
886+
887+
def extractUsernamePassword(header: String): Option[(String, String)] = {
888+
val base64 = header.replace("Basic ", "").replace("basic ", "")
889+
Option(base64)
890+
.map(decodeBase64)
891+
.map(_.split(":").toSeq)
892+
.filter(v => v.nonEmpty && v.length > 1)
893+
.flatMap(a => a.headOption.map(head => (head, a.tail.mkString(":"))))
894+
895+
}
896+
897+
val authModuleOpt = env.proxyState.authModule(config.ref)
898+
899+
ctx.request.headers.get("Authorization") match {
900+
case Some(auth) if auth.startsWith("Basic ") =>
901+
extractUsernamePassword(auth) match {
902+
case None => NgAccess.NgDenied(unauthorized(authModuleOpt)).vfuture
903+
case Some((username, password)) => {
904+
authModuleOpt match {
905+
case None => NgAccess.NgDenied(Results.InternalServerError(Json.obj("error" -> "internal_server_error", "error_description" -> "auth. module not found"))).vfuture
906+
case Some(authModule: BasicAuthModuleConfig) => {
907+
BasicAuthModule(authModule).bindUser(username, password, descriptor).map {
908+
case Left(_) => NgAccess.NgDenied(unauthorized(authModuleOpt))
909+
case Right(user) => {
910+
ctx.attrs.put(otoroshi.plugins.Keys.UserKey -> user)
911+
NgAllowed
912+
}
913+
}
914+
}
915+
case Some(authModule: LdapAuthModuleConfig) => {
916+
BasicAuthWithAuthModule.alreadyLoggedIn(username, authModule) match {
917+
case Some(user) => {
918+
ctx.attrs.put(otoroshi.plugins.Keys.UserKey -> user)
919+
NgAllowed.vfuture
920+
}
921+
case None => {
922+
LdapAuthModule(authModule).bindUser(username, password, descriptor).map {
923+
case Left(_) => NgAccess.NgDenied(unauthorized(authModuleOpt))
924+
case Right(user) => {
925+
BasicAuthWithAuthModule.addLoggedIn(username, user, authModule)
926+
ctx.attrs.put(otoroshi.plugins.Keys.UserKey -> user)
927+
NgAllowed
928+
}
929+
}
930+
}
931+
}
932+
}
933+
case _ => NgAccess.NgDenied(Results.InternalServerError(Json.obj("error" -> "internal_server_error", "error_description" -> "unsupported auth. module kind"))).vfuture
934+
}
935+
}
936+
}
937+
case _ => NgAccess.NgDenied(unauthorized(authModuleOpt)).vfuture
938+
}
939+
}
940+
}

0 commit comments

Comments
 (0)