@@ -3,11 +3,19 @@ package otoroshi.next.plugins
33import akka .http .scaladsl .model .Uri
44import akka .stream .Materializer
55import akka .util .ByteString
6+ import com .github .blemale .scaffeine .{Cache , Scaffeine }
7+ import com .google .common .base .Charsets
68import org .mindrot .jbcrypt .BCrypt
9+ import otoroshi .auth .{AuthModuleConfig , BasicAuthModule , BasicAuthModuleConfig , LdapAuthModule , LdapAuthModuleConfig }
10+ import otoroshi .auth .implicits .ResultWithPrivateAppSession
11+ import otoroshi .controllers .routes
712import otoroshi .env .Env
813import 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
1017import otoroshi .next .plugins .api ._
18+ import otoroshi .security .OtoroshiClaim
1119import otoroshi .utils .http .RequestImplicits ._
1220import otoroshi .utils .syntax .implicits ._
1321import play .api .Logger
@@ -18,6 +26,7 @@ import play.api.mvc.{Result, Results}
1826import java .net .URLEncoder
1927import java .nio .charset .StandardCharsets
2028import java .util .Base64
29+ import scala .concurrent .duration .DurationInt
2130import scala .concurrent .{ExecutionContext , Future }
2231import 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