next.plugins.auth.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.next.plugins
import akka.http.scaladsl.model.Uri
import akka.stream.Materializer
import akka.util.ByteString
import org.mindrot.jbcrypt.BCrypt
import otoroshi.env.Env
import otoroshi.gateway.Errors
import otoroshi.models.PrivateAppsUserHelper
import otoroshi.next.plugins.api._
import otoroshi.utils.http.RequestImplicits._
import otoroshi.utils.syntax.implicits._
import play.api.Logger
import play.api.libs.json._
import play.api.mvc.Results.BadRequest
import play.api.mvc.{Result, Results}
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.Base64
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
case class NgLegacyAuthModuleCallConfig(
publicPatterns: Seq[String] = Seq.empty,
privatePatterns: Seq[String] = Seq.empty,
config: NgAuthModuleConfig
) extends NgPluginConfig {
override def json: JsValue = NgLegacyAuthModuleCallConfig.format.writes(this)
}
object NgLegacyAuthModuleCallConfig {
val default = NgLegacyAuthModuleCallConfig(Seq.empty, Seq.empty, NgAuthModuleConfig())
val format = new Format[NgLegacyAuthModuleCallConfig] {
override def writes(o: NgLegacyAuthModuleCallConfig): JsValue = Json.obj(
"public_patterns" -> o.publicPatterns,
"private_patterns" -> o.privatePatterns
) ++ o.config.json.asObject
override def reads(json: JsValue): JsResult[NgLegacyAuthModuleCallConfig] = Try {
NgLegacyAuthModuleCallConfig(
publicPatterns = json.select("public_patterns").asOpt[Seq[String]].getOrElse(Seq.empty),
privatePatterns = json.select("private_patterns").asOpt[Seq[String]].getOrElse(Seq.empty),
config = NgAuthModuleConfig.format.reads(json).asOpt.getOrElse(NgAuthModuleConfig())
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(value) => JsSuccess(value)
}
}
}
class NgLegacyAuthModuleCall extends NgAccessValidator {
private val configReads: Reads[NgLegacyAuthModuleCallConfig] = NgLegacyAuthModuleCallConfig.format
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication, NgPluginCategory.Classic)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def multiInstance: Boolean = true
override def core: Boolean = true
override def name: String = "Legacy Authentication"
override def description: Option[String] =
"This plugin applies an authentication module the same way service descriptor does".some
override def defaultConfigObject: Option[NgPluginConfig] = NgLegacyAuthModuleCallConfig.default.some
override def isAccessAsync: Boolean = true
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
val authPlugin = env.scriptManager
.getAnyScript[NgAccessValidator](NgPluginHelper.pluginId[AuthModule])(env.otoroshiExecutionContext)
.right
.get
val apikeyPlugin = env.scriptManager
.getAnyScript[NgAccessValidator](NgPluginHelper.pluginId[NgLegacyApikeyCall])(env.otoroshiExecutionContext)
.right
.get
val config = ctx.cachedConfig(internalName)(configReads).getOrElse(NgLegacyAuthModuleCallConfig.default)
val apikeyConfig = NgLegacyApikeyCallConfig(
config.publicPatterns,
config.privatePatterns,
NgApikeyCallsConfig.fromLegacy(ctx.route.legacy.apiKeyConstraints)
)
val descriptor =
ctx.route.legacy.copy(publicPatterns = config.publicPatterns, privatePatterns = config.privatePatterns)
val req = ctx.request
if (descriptor.isUriPublic(req.path)) {
authPlugin.access(ctx)(env, ec)
} else {
PrivateAppsUserHelper.isPrivateAppsSessionValid(req, descriptor, ctx.attrs).flatMap {
case Some(_) if descriptor.strictlyPrivate => apikeyPlugin.access(ctx.copy(config = apikeyConfig.json))
case Some(user) =>
ctx.attrs.put(otoroshi.plugins.Keys.UserKey -> user)
NgAccess.NgAllowed.vfuture
case None =>
apikeyPlugin.access(ctx.copy(config = apikeyConfig.json))
}
}
}
}
case class NgAuthModuleConfig(module: Option[String] = None, passWithApikey: Boolean = false) extends NgPluginConfig {
def json: JsValue = NgAuthModuleConfig.format.writes(this)
}
object NgAuthModuleConfig {
val format = new Format[NgAuthModuleConfig] {
override def reads(json: JsValue): JsResult[NgAuthModuleConfig] = Try {
NgAuthModuleConfig(
module =
json.select("auth_module").asOpt[String].orElse(json.select("module").asOpt[String]).filter(_.nonEmpty),
passWithApikey = json.select("pass_with_apikey").asOpt[Boolean].getOrElse(false)
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(c) => JsSuccess(c)
}
override def writes(o: NgAuthModuleConfig): JsValue = Json.obj(
"pass_with_apikey" -> o.passWithApikey,
"auth_module" -> o.module.map(JsString.apply).getOrElse(JsNull).as[JsValue]
)
}
}
case class NgMultiAuthModuleConfig(
modules: Seq[String] = Seq.empty,
passWithApikey: Boolean = false,
useEmailPrompt: Boolean = false,
usersGroups: JsObject = Json.obj()
) extends NgPluginConfig {
def json: JsValue = NgMultiAuthModuleConfig.format.writes(this)
}
object NgMultiAuthModuleConfig {
val format = new Format[NgMultiAuthModuleConfig] {
override def reads(json: JsValue): JsResult[NgMultiAuthModuleConfig] = Try {
NgMultiAuthModuleConfig(
modules = json
.select("auth_modules")
.asOpt[Seq[String]]
.orElse(json.select("modules").asOpt[Seq[String]])
.getOrElse(Seq.empty[String])
.filter(_.nonEmpty),
passWithApikey = json
.select("pass_with_apikey")
.asOpt[Boolean]
.getOrElse(false),
useEmailPrompt = json
.select("use_email_prompt")
.asOpt[Boolean]
.getOrElse(false),
usersGroups = json
.select("users_groups")
.asOpt[JsObject]
.getOrElse(Json.obj())
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(c) => JsSuccess(c)
}
override def writes(o: NgMultiAuthModuleConfig): JsValue = Json.obj(
"pass_with_apikey" -> o.passWithApikey,
"auth_modules" -> o.modules
)
}
}
class MultiAuthModule extends NgAccessValidator {
private val logger = Logger("otoroshi-next-plugins-multi-auth-module")
private val configReads: Reads[NgMultiAuthModuleConfig] = NgMultiAuthModuleConfig.format
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def multiInstance: Boolean = true
override def core: Boolean = true
override def name: String = "Multi Authentication"
override def description: Option[String] =
"This plugin applies an authentication module from a list of selected modules".some
override def defaultConfigObject: Option[NgPluginConfig] = NgMultiAuthModuleConfig().some
override def isAccessAsync: Boolean = true
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
val NgMultiAuthModuleConfig(modules, passWithApikey, useEmailPrompt, usersGroups) =
ctx.cachedConfig(internalName)(configReads).getOrElse(NgMultiAuthModuleConfig())
val maybeApikey = ctx.attrs.get(otoroshi.plugins.Keys.ApiKeyKey)
val pass = passWithApikey match {
case true => maybeApikey.isDefined
case false => false
}
ctx.attrs.get(otoroshi.plugins.Keys.UserKey) match {
case None if !pass =>
if (modules.isEmpty) {
Errors
.craftResponseResult(
"Auth. config. refs not found on the descriptor",
Results.InternalServerError,
ctx.request,
None,
"errors.auth.config.refs.not.found".some,
attrs = ctx.attrs,
maybeRoute = ctx.route.some
)
.map(NgAccess.NgDenied.apply)
} else if (modules.length == 1) {
passWithAuthModule(modules.head, ctx)
} else {
ctx.request.cookies.filter(cookie => cookie.name.startsWith("oto-papps")) match {
case cookies if cookies.nonEmpty =>
modules
.flatMap(module => env.proxyState.authModule(module))
.find(module =>
cookies.exists(cookie => cookie.name == s"oto-papps-${module.routeCookieSuffix(ctx.route)}")
) match {
case Some(module) => passWithAuthModule(module.id, ctx)
case None => redirectToAuthModule(ctx, useEmailPrompt)
}
case _ => redirectToAuthModule(ctx, useEmailPrompt)
}
}
case _ => NgAccess.NgAllowed.vfuture
}
}
private def redirectToAuthModule(ctx: NgAccessContext, useEmailPrompt: Boolean)(implicit env: Env) = {
val redirect = ctx.request
.getQueryString("redirect")
.getOrElse(s"${ctx.request.theProtocol}://${ctx.request.theHost}${ctx.request.relativeUri}")
if (useEmailPrompt) {
NgAccess
.NgDenied(
Results.Redirect(
s"${env.rootScheme + env.privateAppsHost + env.privateAppsPort}/privateapps/generic/simple-login?route=${ctx.route.id}&redirect=${redirect}"
)
)
.vfuture
} else {
NgAccess
.NgDenied(
Results.Redirect(
s"${env.rootScheme + env.privateAppsHost + env.privateAppsPort}/privateapps/generic/choose-provider?route=${ctx.route.id}&redirect=${redirect}"
)
)
.vfuture
}
}
private def passWithAuthModule(authModuleId: String, ctx: NgAccessContext)(implicit
env: Env,
ec: ExecutionContext
): Future[NgAccess] = {
env.proxyState.authModule(authModuleId) match {
case None =>
Errors
.craftResponseResult(
"Auth. config. not found on the descriptor",
Results.InternalServerError,
ctx.request,
None,
"errors.auth.config.not.found".some,
attrs = ctx.attrs,
maybeRoute = ctx.route.some
)
.map(NgAccess.NgDenied.apply)
case Some(auth) => {
// here there is a datastore access (by key) to get the user session
PrivateAppsUserHelper.isPrivateAppsSessionValidWithAuth(ctx.request, ctx.route.legacy, auth).flatMap {
case Some(paUsr) =>
ctx.attrs.put(otoroshi.plugins.Keys.UserKey -> paUsr)
NgAccess.NgAllowed.vfuture
case None => {
val req = ctx.request
val baseRedirect = s"${req.theProtocol}://${req.theHost}${req.relativeUri}"
val redirect =
if (env.allowRedirectQueryParamOnLogin) req.getQueryString("redirect").getOrElse(baseRedirect)
else baseRedirect
val encodedRedirect = Base64.getUrlEncoder.encodeToString(redirect.getBytes(StandardCharsets.UTF_8))
val descriptorId = ctx.route.legacy.id
val hash = env.sign(s"desc=${descriptorId}&redirect=${encodedRedirect}")
val redirectTo =
env.rootScheme + env.privateAppsHost + env.privateAppsPort + otoroshi.controllers.routes.AuthController
.confidentialAppLoginPage()
.url + s"?desc=${descriptorId}&redirect=${encodedRedirect}&hash=${hash}"
if (logger.isTraceEnabled) logger.trace("should redirect to " + redirectTo)
NgAccess
.NgDenied(
Results
.Redirect(redirectTo)
.discardingCookies(
env.removePrivateSessionCookies(
ctx.request.theDomain,
ctx.route.legacy,
auth
): _*
)
)
.vfuture
}
}
}
}
}
}
class AuthModule extends NgAccessValidator {
private val logger = Logger("otoroshi-next-plugins-auth-module")
private val configReads: Reads[NgAuthModuleConfig] = NgAuthModuleConfig.format
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def multiInstance: Boolean = true
override def core: Boolean = true
override def name: String = "Authentication"
override def description: Option[String] = "This plugin applies an authentication module".some
override def defaultConfigObject: Option[NgPluginConfig] = NgAuthModuleConfig().some
override def isAccessAsync: Boolean = true
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
val NgAuthModuleConfig(module, passWithApikey) =
ctx.cachedConfig(internalName)(configReads).getOrElse(NgAuthModuleConfig())
val req = ctx.request
val descriptor = ctx.route.serviceDescriptor
val maybeApikey = ctx.attrs.get(otoroshi.plugins.Keys.ApiKeyKey)
val pass = passWithApikey match {
case true => maybeApikey.isDefined
case false => false
}
ctx.attrs.get(otoroshi.plugins.Keys.UserKey) match {
case None if !pass => {
module match {
case None =>
Errors
.craftResponseResult(
"Auth. config. ref not found on the descriptor",
Results.InternalServerError,
ctx.request,
None,
"errors.auth.config.ref.not.found".some,
attrs = ctx.attrs,
maybeRoute = ctx.route.some
)
.map(NgAccess.NgDenied.apply)
case Some(ref) => {
// env.datastores.authConfigsDataStore.findById(ref).flatMap {
env.proxyState.authModule(ref) match {
case None =>
Errors
.craftResponseResult(
"Auth. config. not found on the descriptor",
Results.InternalServerError,
ctx.request,
None,
"errors.auth.config.not.found".some,
attrs = ctx.attrs,
maybeRoute = ctx.route.some
)
.map(NgAccess.NgDenied.apply)
case Some(auth) => {
// here there is a datastore access (by key) to get the user session
PrivateAppsUserHelper.isPrivateAppsSessionValidWithAuth(ctx.request, descriptor, auth).flatMap {
case Some(paUsr) =>
ctx.attrs.put(otoroshi.plugins.Keys.UserKey -> paUsr)
NgAccess.NgAllowed.vfuture
case None => {
val baseRedirect = s"${req.theProtocol}://${req.theHost}${req.relativeUri}"
val redirect =
if (env.allowRedirectQueryParamOnLogin) req.getQueryString("redirect").getOrElse(baseRedirect)
else baseRedirect
val encodedRedirect = Base64.getUrlEncoder.encodeToString(redirect.getBytes(StandardCharsets.UTF_8))
val descriptorId = descriptor.id
val hash = env.sign(s"desc=${descriptorId}&redirect=${encodedRedirect}")
val redirectTo =
env.rootScheme + env.privateAppsHost + env.privateAppsPort + otoroshi.controllers.routes.AuthController
.confidentialAppLoginPage()
.url + s"?desc=${descriptorId}&redirect=${encodedRedirect}&hash=${hash}"
if (logger.isTraceEnabled) logger.trace("should redirect to " + redirectTo)
NgAccess
.NgDenied(
Results
.Redirect(redirectTo)
.discardingCookies(
env.removePrivateSessionCookies(
req.theDomain,
descriptor,
auth
): _*
)
)
.vfuture
}
}
}
}
}
}
}
case _ => NgAccess.NgAllowed.vfuture
}
}
}
case class NgAuthModuleUserExtractorConfig(module: Option[String] = None) extends NgPluginConfig {
def json: JsValue = NgAuthModuleUserExtractorConfig.format.writes(this)
}
object NgAuthModuleUserExtractorConfig {
val format = new Format[NgAuthModuleUserExtractorConfig] {
override def reads(json: JsValue): JsResult[NgAuthModuleUserExtractorConfig] = Try {
NgAuthModuleUserExtractorConfig(
module = json.select("auth_module").asOpt[String].orElse(json.select("module").asOpt[String]).filter(_.nonEmpty)
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(c) => JsSuccess(c)
}
override def writes(o: NgAuthModuleUserExtractorConfig): JsValue = Json.obj(
"auth_module" -> o.module.map(JsString.apply).getOrElse(JsNull).as[JsValue]
)
}
}
class NgAuthModuleUserExtractor extends NgAccessValidator {
private val configReads: Reads[NgAuthModuleUserExtractorConfig] = NgAuthModuleUserExtractorConfig.format
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def isAccessAsync: Boolean = true
override def multiInstance: Boolean = true
override def core: Boolean = true
override def name: String = "User extraction from auth. module"
override def description: Option[String] =
"This plugin extracts users from an authentication module without enforcing login".some
override def defaultConfigObject: Option[NgAuthModuleUserExtractorConfig] = NgAuthModuleUserExtractorConfig().some
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
def error(status: Results.Status, msg: String, code: String): Future[NgAccess] = {
Errors
.craftResponseResult(
msg,
status,
ctx.request,
None,
code.some,
attrs = ctx.attrs,
maybeRoute = ctx.route.some
)
.map(NgAccess.NgDenied.apply)
}
val NgAuthModuleUserExtractorConfig(module) =
ctx.cachedConfig(internalName)(configReads).getOrElse(NgAuthModuleUserExtractorConfig())
val descriptor = ctx.route.serviceDescriptor
ctx.attrs.get(otoroshi.plugins.Keys.UserKey) match {
case None => {
module match {
case None =>
error(
Results.InternalServerError,
"Auth. config. ref not found on the descriptor",
"errors.auth.config.ref.not.found"
)
case Some(ref) => {
env.proxyState.authModule(ref) match {
case None =>
error(
Results.InternalServerError,
"Auth. config. not found on the descriptor",
"errors.auth.config.not.found"
)
case Some(auth) => {
// here there is a datastore access (by key) to get the user session
PrivateAppsUserHelper.isPrivateAppsSessionValidWithAuth(ctx.request, descriptor, auth).flatMap {
case Some(paUsr) =>
ctx.attrs.put(otoroshi.plugins.Keys.UserKey -> paUsr)
NgAccess.NgAllowed.vfuture
case None => {
NgAccess.NgAllowed.vfuture
}
}
}
}
}
}
}
case _ => NgAccess.NgAllowed.vfuture
}
}
}
case class NgAuthModuleExpectedUserConfig(onlyFrom: Seq[String] = Seq.empty) extends NgPluginConfig {
def json: JsValue = NgAuthModuleExpectedUserConfig.format.writes(this)
}
object NgAuthModuleExpectedUserConfig {
val format = new Format[NgAuthModuleExpectedUserConfig] {
override def reads(json: JsValue): JsResult[NgAuthModuleExpectedUserConfig] = Try {
NgAuthModuleExpectedUserConfig(
onlyFrom = json.select("only_from").asOpt[Seq[String]].getOrElse(Seq.empty)
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(c) => JsSuccess(c)
}
override def writes(o: NgAuthModuleExpectedUserConfig): JsValue = Json.obj(
"only_from" -> JsArray(o.onlyFrom.map(JsString.apply))
)
}
}
class NgAuthModuleExpectedUser extends NgAccessValidator {
private val configReads: Reads[NgAuthModuleExpectedUserConfig] = NgAuthModuleExpectedUserConfig.format
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def isAccessAsync: Boolean = true
override def multiInstance: Boolean = true
override def core: Boolean = true
override def name: String = "User logged in expected"
override def description: Option[String] = "This plugin enforce that a user from any auth. module is logged in".some
override def defaultConfigObject: Option[NgAuthModuleExpectedUserConfig] = NgAuthModuleExpectedUserConfig().some
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
def error(status: Results.Status, msg: String, code: String): Future[NgAccess] = {
Errors
.craftResponseResult(
msg,
status,
ctx.request,
None,
code.some,
attrs = ctx.attrs,
maybeRoute = ctx.route.some
)
.map(NgAccess.NgDenied.apply)
}
val NgAuthModuleExpectedUserConfig(onlyFrom) =
ctx.cachedConfig(internalName)(configReads).getOrElse(NgAuthModuleExpectedUserConfig())
ctx.attrs.get(otoroshi.plugins.Keys.UserKey) match {
case None => error(Results.Unauthorized, "You're not authorized here !", "errors.auth.unauthorized")
case Some(user) if onlyFrom.nonEmpty => {
if (onlyFrom.contains(user.authConfigId)) {
NgAccess.NgAllowed.vfuture
} else {
error(Results.Unauthorized, "You're not authorized here !", "errors.auth.unauthorized")
}
}
case Some(_) => NgAccess.NgAllowed.vfuture
}
}
}
case class BasicAuthCallerConfig(
username: Option[String] = None,
password: Option[String] = None,
headerName: String = "Authorization",
headerValueFormat: String = "Basic %s"
) extends NgPluginConfig {
override def json: JsValue = BasicAuthCallerConfig.format.writes(this)
}
object BasicAuthCallerConfig {
val format: Format[BasicAuthCallerConfig] = new Format[BasicAuthCallerConfig] {
override def writes(o: BasicAuthCallerConfig): JsValue = Json.obj(
"username" -> o.username,
"passaword" -> o.password,
"headerName" -> o.headerName,
"headerValueFormat" -> o.headerValueFormat
)
override def reads(json: JsValue): JsResult[BasicAuthCallerConfig] = Try {
BasicAuthCallerConfig(
username = json.select("username").asOpt[String].filter(_.nonEmpty),
password = json.select("password").asOpt[String].filter(_.nonEmpty),
headerName = json.select("headerName").asOpt[String].getOrElse("Authorization"),
headerValueFormat = json.select("headerValueFormat").asOpt[String].getOrElse("Basic %s")
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(c) => JsSuccess(c)
}
}
}
class BasicAuthCaller extends NgRequestTransformer {
override def steps: Seq[NgStep] = Seq(NgStep.TransformRequest)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def multiInstance: Boolean = true
override def core: Boolean = true
override def usesCallbacks: Boolean = false
override def transformsRequest: Boolean = true
override def transformsResponse: Boolean = false
override def transformsError: Boolean = false
override def isTransformRequestAsync: Boolean = false
override def isTransformResponseAsync: Boolean = false
override def name: String = "Basic Auth. caller"
override def description: Option[String] =
"This plugin can be used to call api that are authenticated using basic auth.".some
override def defaultConfigObject: Option[NgPluginConfig] = BasicAuthCallerConfig().some
override def transformRequestSync(
ctx: NgTransformerRequestContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, NgPluginHttpRequest] = {
val config = ctx.cachedConfig(internalName)(BasicAuthCallerConfig.format.reads).getOrElse(BasicAuthCallerConfig())
(config.username, config.password) match {
case (Some(username), Some(password)) if username.nonEmpty && password.nonEmpty =>
val token: String = ByteString(s"$username:$password").encodeBase64.utf8String
Right(
ctx.otoroshiRequest.copy(headers =
ctx.otoroshiRequest.headers + (config.headerName -> config.headerValueFormat.format(token))
)
)
case _ => BadRequest(Json.obj("error" -> "Bad configuration")).left
}
}
}
case class SimpleBasicAuthConfig(realm: String = "authentication", users: Map[String, String] = Map.empty)
extends NgPluginConfig {
override def json: JsValue = SimpleBasicAuthConfig.format.writes(this)
}
object SimpleBasicAuthConfig {
val format = new Format[SimpleBasicAuthConfig] {
override def reads(json: JsValue): JsResult[SimpleBasicAuthConfig] = Try {
SimpleBasicAuthConfig(
realm = json.select("realm").asOptString.getOrElse("authentication"),
users = json.select("users").asOpt[Map[String, String]].getOrElse(Map.empty)
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(e) => JsSuccess(e)
}
override def writes(o: SimpleBasicAuthConfig): JsValue = Json.obj(
"realm" -> o.realm,
"users" -> o.users
)
}
val configFlow: Seq[String] = Seq("realm", "users")
def configSchema: Option[JsObject] = Some(
Json.obj(
"realm" -> Json.obj(
"type" -> "string",
"label" -> s"Realm",
"help" -> "A unique realm name to avoid weird browser behaviors",
"props" -> Json.obj(
"placeholder" -> "A unique realm name to avoid weird browser behaviors",
"help" -> "A unique realm name to avoid weird browser behaviors"
)
),
"users" -> Json.obj(
"type" -> "object",
"label" -> "Users",
"props" -> Json.obj(
"bcryptable" -> true
)
)
)
)
}
class SimpleBasicAuth extends NgAccessValidator {
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def multiInstance: Boolean = true
override def core: Boolean = true
override def noJsForm: Boolean = true
override def name: String = "Basic Auth"
override def description: Option[String] =
"This plugin can be used to protect a route with basic auth. You can use clear text passwords (not recommended for production usage) or Bcryt hashed password as password values".some
override def defaultConfigObject: Option[NgPluginConfig] = SimpleBasicAuthConfig().some
override def configFlow: Seq[String] = SimpleBasicAuthConfig.configFlow
override def configSchema: Option[JsObject] = SimpleBasicAuthConfig.configSchema
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
val config = ctx.cachedConfig(internalName)(SimpleBasicAuthConfig.format.reads).getOrElse(SimpleBasicAuthConfig())
val globalUsers = env.datastores.globalConfigDataStore
.latest()
.plugins
.config
.select("simple_basic_auth_global_users")
.asOpt[Map[String, String]]
.getOrElse(Map.empty)
val authorization: String = ctx.request.headers
.get("Authorization")
.filter(_.startsWith("Basic "))
.map(_.replace("Basic ", ""))
.map(v => new String(Base64.getDecoder.decode(v), StandardCharsets.UTF_8))
.getOrElse("")
val parts = authorization.split(":").toSeq
if (authorization.contains(":") && parts.length > 1) {
val username = parts.head
val password = parts.tail.mkString(":")
(config.users ++ globalUsers).get(username) match {
case Some(pwd) if password == pwd => NgAccess.NgAllowed.vfuture
case Some(pwd) if BCrypt.checkpw(password, pwd) => NgAccess.NgAllowed.vfuture
case _ => {
NgAccess
.NgDenied(
Results
.Unauthorized("")
.withHeaders("WWW-Authenticate" -> s"""Basic realm="${config.realm}"""")
)
.vfuture
}
}
} else {
NgAccess
.NgDenied(
Results
.Unauthorized("")
.withHeaders("WWW-Authenticate" -> s"""Basic realm="${config.realm}"""")
)
.vfuture
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy