Skip to content

Commit

Permalink
#25 Authentication service refactoring
Browse files Browse the repository at this point in the history
Remove useless AuthSrvFactory class
Remove authentication by key from LDAPAuthSrv
Add authentication capability "authByKey"
  • Loading branch information
To-om committed Sep 6, 2017
1 parent e0e0ae6 commit acc6bb5
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 281 deletions.
15 changes: 2 additions & 13 deletions app/org/elastic4play/services/UserSrv.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package org.elastic4play.services

import java.util.Base64
import java.util.concurrent.atomic.AtomicBoolean

import scala.concurrent.Future
import scala.util.Random

import play.api.libs.json.JsObject
import play.api.mvc.RequestHeader
Expand Down Expand Up @@ -39,15 +37,10 @@ trait User {

object AuthCapability extends Enumeration {
type Type = Value
val changePassword, setPassword, renewKey = Value
val changePassword, setPassword, authByKey = Value
}

trait AuthSrv {
protected final def generateKey(): String = {
val bytes = Array.ofDim[Byte](24)
Random.nextBytes(bytes)
Base64.getEncoder.encodeToString(bytes)
}
val name: String
val capabilities = Set.empty[AuthCapability.Type]

Expand All @@ -57,9 +50,5 @@ trait AuthSrv {
def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = Future.failed(AuthorizationError("Operation not supported"))
def renewKey(username: String)(implicit authContext: AuthContext): Future[String] = Future.failed(AuthorizationError("Operation not supported"))
def getKey(username: String)(implicit authContext: AuthContext): Future[String] = Future.failed(AuthorizationError("Operation not supported"))
}

trait AuthSrvFactory {
val name: String
def getAuthSrv: AuthSrv
def removeKey(username: String)(implicit authContext: AuthContext): Future[Unit] = Future.failed(AuthorizationError("Operation not supported"))
}
186 changes: 111 additions & 75 deletions app/org/elastic4play/services/auth/ADAuthSrv.scala
Original file line number Diff line number Diff line change
@@ -1,102 +1,138 @@
package org.elastic4play.services.auth

import java.net.ConnectException
import java.util
import javax.inject.{ Inject, Singleton }
import javax.naming.Context
import javax.naming.directory._

import scala.concurrent.{ ExecutionContext, Future }
import scala.util.Try
import scala.util.{ Failure, Success, Try }

import play.api.mvc.RequestHeader
import play.api.{ Configuration, Logger }

import org.elastic4play.services._
import org.elastic4play.{ AuthenticationError, AuthorizationError }

case class ADConnection(
domainFQDN: String,
domainName: String,
serverNames: Seq[String],
useSSL: Boolean) {

private[ADConnection] lazy val logger = Logger(classOf[ADAuthSrv])

private val noADServerAvailableException = AuthenticationError("No LDAP server found")

private def isFatal(t: Throwable): Boolean = t match {
case null true
case `noADServerAvailableException` false
case _: ConnectException false
case _ isFatal(t.getCause)
}

private def connect[A](username: String, password: String)(f: InitialDirContext Try[A]): Try[A] = {
serverNames.foldLeft[Try[A]](Failure(noADServerAvailableException)) {
case (Failure(e), serverName) if !isFatal(e)
val protocol = if (useSSL) "ldaps://" else "ldap://"
val env = new util.Hashtable[Any, Any]
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
env.put(Context.PROVIDER_URL, protocol + serverName)
env.put(Context.SECURITY_AUTHENTICATION, "simple")
env.put(Context.SECURITY_PRINCIPAL, username)
env.put(Context.SECURITY_CREDENTIALS, password)
Try {
val ctx = new InitialDirContext(env)
try f(ctx)
finally ctx.close()
}
.flatten
case (failure @ Failure(e), _)
logger.debug("LDAP connect error", e)
failure
case (r, _) r
}
}

private def getUserDN(ctx: InitialDirContext, username: String): Try[String] = {
Try {
val controls = new SearchControls()
controls.setSearchScope(SearchControls.SUBTREE_SCOPE)
controls.setCountLimit(1)
val domainDN = domainFQDN.split("\\.").mkString("dc=", ",dc=", "")
val searchResult = ctx.search(domainDN, "(sAMAccountName={0})", Array[Object](username), controls)
if (searchResult.hasMore) searchResult.next().getNameInNamespace
else throw AuthenticationError("User not found in Active Directory")
}
}

def authenticate(username: String, password: String)(implicit request: RequestHeader): Try[Unit] = {
connect(domainName + "\\" + username, password)(_ Success(()))
}

def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Try[Unit] = {
val unicodeOldPassword = ("\"" + oldPassword + "\"").getBytes("UTF-16LE")
val unicodeNewPassword = ("\"" + newPassword + "\"").getBytes("UTF-16LE")
connect(domainName + "\\" + username, oldPassword) { ctx
getUserDN(ctx, username).map { userDN
val mods = Array(
new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute("unicodePwd", unicodeOldPassword)),
new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute("unicodePwd", unicodeNewPassword)))
ctx.modifyAttributes(userDN, mods)
}
}
}
}

object ADConnection {
def apply(configuration: Configuration): ADConnection =
(for {
domainFQDN configuration.getOptional[String]("auth.ad.domainFQDN")
domainName configuration.getOptional[String]("auth.ad.domainName")
serverNames = configuration.getOptional[Seq[String]]("auth.ad.serverNames").getOrElse(Seq(domainFQDN))
useSSL = configuration.getOptional[Boolean]("auth.ad.useSSL").getOrElse(false)
} yield ADConnection(domainFQDN, domainName, serverNames, useSSL))
.getOrElse(ADConnection("", "", Nil, useSSL = false))
}

@Singleton
class ADAuthSrvFactory @Inject() (
class ADAuthSrv(
adConnection: ADConnection,
userSrv: UserSrv,
implicit val ec: ExecutionContext) extends AuthSrv {

@Inject() def this(
configuration: Configuration,
userSrv: UserSrv,
ec: ExecutionContext) extends AuthSrvFactory { factory
val name = "ad"
def getAuthSrv: AuthSrv = new ADAuthSrv(
configuration.get[String]("auth.ad.domainFQDN"),
configuration.get[String]("auth.ad.domainName"),
configuration.getOptional[Boolean]("auth.ad.useSSL").getOrElse(false),
ec: ExecutionContext) = this(
ADConnection(configuration),
userSrv,
ec)

private class ADAuthSrv(
DomainFQDN: String,
domainName: String,
useSSL: Boolean,
userSrv: UserSrv,
implicit val ec: ExecutionContext) extends AuthSrv {

private[ADAuthSrv] lazy val logger = Logger(getClass)
val name: String = factory.name
override val capabilities: Set[AuthCapability.Value] = Set(AuthCapability.changePassword)

private[auth] def connect[A](username: String, password: String)(f: InitialDirContext A): Try[A] = {
val protocol = if (useSSL) "ldaps://" else "ldap://"
val env = new util.Hashtable[Any, Any]
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
env.put(Context.PROVIDER_URL, protocol + DomainFQDN)
env.put(Context.SECURITY_AUTHENTICATION, "simple")
env.put(Context.SECURITY_PRINCIPAL, username)
env.put(Context.SECURITY_CREDENTIALS, password)
Try {
val ctx = new InitialDirContext(env)
try f(ctx)
finally ctx.close()
}
}
private[ADAuthSrv] lazy val logger = Logger(getClass)
val name: String = "ad"
override val capabilities: Set[AuthCapability.Value] = Set(AuthCapability.changePassword)

private[auth] def getUserDN(ctx: InitialDirContext, username: String): Try[String] = {
Try {
val controls = new SearchControls()
controls.setSearchScope(SearchControls.SUBTREE_SCOPE)
controls.setCountLimit(1)
val domainDN = DomainFQDN.split("\\.").mkString("dc=", ",dc=", "")
val searchResult = ctx.search(domainDN, "(sAMAccountName={0})", Array[Object](username), controls)
if (searchResult.hasMore) searchResult.next().getNameInNamespace
else throw AuthenticationError("User not found in Active Directory")
override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = {
(for {
_ Future.fromTry(adConnection.authenticate(username, password))
authContext userSrv.getFromId(request, username)
} yield authContext)
.recoverWith {
case t
logger.error("AD authentication failure", t)
Future.failed(AuthenticationError("Authentication failure"))
}
}

override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = {
(for {
_ Future.fromTry(connect(domainName + "\\" + username, password)(identity))
u userSrv.get(username)
authContext userSrv.getFromUser(request, u)
} yield authContext)
.recoverWith {
case t
logger.error("AD authentication failure", t)
Future.failed(AuthenticationError("Authentication failure"))
}
}
}

override def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = {
val unicodeOldPassword = ("\"" + oldPassword + "\"").getBytes("UTF-16LE")
val unicodeNewPassword = ("\"" + newPassword + "\"").getBytes("UTF-16LE")
val changeTry = connect(domainName + "\\" + username, oldPassword) { ctx
getUserDN(ctx, username).map { userDN
val mods = Array(
new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute("unicodePwd", unicodeOldPassword)),
new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute("unicodePwd", unicodeNewPassword)))
ctx.modifyAttributes(userDN, mods)
}
override def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = {
Future
.fromTry(adConnection.changePassword(username, oldPassword, newPassword))
.recoverWith {
case t
logger.error("AD change password failure", t)
Future.failed(AuthorizationError("Change password failure"))
}
.flatMap(identity)
Future
.fromTry(changeTry)
.recoverWith {
case t
logger.error("LDAP change password failure", t)
Future.failed(AuthorizationError("Change password failure"))
}
}
}
}
Loading

0 comments on commit acc6bb5

Please sign in to comment.