Skip to content

Commit

Permalink
#25 Add API key authentication type
Browse files Browse the repository at this point in the history
  • Loading branch information
To-om committed Sep 4, 2017
1 parent f32fb0c commit 0529d84
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 58 deletions.
33 changes: 24 additions & 9 deletions app/org/elastic4play/controllers/Authenticated.scala
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ class Authenticated(
* Retrieve authentication information from API key
*/
def getFromApiKey(request: RequestHeader): Future[AuthContext] =
for {
auth request
.headers
.get(HeaderNames.AUTHORIZATION)
.fold(Future.failed[String](AuthenticationError("Authentication header not found")))(Future.successful)
_ if (!auth.startsWith("Bearer ")) Future.failed(AuthenticationError("Only bearer authentication is supported")) else Future.successful(())
key = auth.substring(7)
authContext authSrv.authenticate(key)(request)
} yield authContext

def getFromBasicAuth(request: RequestHeader): Future[AuthContext] =
for {
auth request
.headers
Expand All @@ -119,15 +130,19 @@ class Authenticated(
case getFromSessionError
getFromApiKey(request).recoverWith {
case getFromApiKeyError
userSrv.getInitialUser(request).recoverWith {
case getInitialUserError
logger.error(
s"""Authentication error:
| From session: ${getFromSessionError.getClass.getSimpleName} ${getFromSessionError.getMessage}
| From api key: ${getFromApiKeyError.getClass.getSimpleName} ${getFromApiKeyError.getMessage}
| Initial user: ${getInitialUserError.getClass.getSimpleName} ${getInitialUserError.getMessage}
""".stripMargin)
Future.failed(AuthenticationError("Not authenticated"))
getFromBasicAuth(request).recoverWith {
case getFromBasicAuthError
userSrv.getInitialUser(request).recoverWith {
case getInitialUserError
logger.error(
s"""Authentication error:
| From session : ${getFromSessionError.getClass.getSimpleName} ${getFromSessionError.getMessage}
| From api key : ${getFromApiKeyError.getClass.getSimpleName} ${getFromApiKeyError.getMessage}
| From basic auth: ${getFromBasicAuthError.getClass.getSimpleName} ${getFromBasicAuthError.getMessage}
| Initial user : ${getInitialUserError.getClass.getSimpleName} ${getInitialUserError.getMessage}
""".stripMargin)
Future.failed(AuthenticationError("Not authenticated"))
}
}
}
}
Expand Down
25 changes: 20 additions & 5 deletions app/org/elastic4play/services/UserSrv.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
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

import org.elastic4play.{ AuthenticationError, AuthorizationError }

abstract class Role(val name: String)

trait AuthContext {
Expand Down Expand Up @@ -35,15 +39,26 @@ trait User {

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

trait AuthSrv {
protected final def generateKey(): String = {
val bytes = Array.ofDim[Byte](24)
Random.nextBytes(bytes)
Base64.getEncoder.encodeToString(bytes)
}
val name: String
def capabilities: Set[AuthCapability.Type]
def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext]
def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit]
def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit]
val capabilities = Set.empty[AuthCapability.Type]

def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = Future.failed(AuthenticationError("Operation not supported"))
def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = Future.failed(AuthenticationError("Operation not supported"))
def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = Future.failed(AuthorizationError("Operation not supported"))
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
Expand Down
8 changes: 3 additions & 5 deletions app/org/elastic4play/services/auth/ADAuthSrv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class ADAuthSrvFactory @Inject() (

private[ADAuthSrv] lazy val logger = Logger(getClass)
val name: String = factory.name
val capabilities: Set[AuthCapability.Value] = Set(AuthCapability.changePassword)
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://"
Expand Down Expand Up @@ -65,7 +65,7 @@ class ADAuthSrvFactory @Inject() (
}
}

def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = {
override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = {
(for {
_ Future.fromTry(connect(domainName + "\\" + username, password)(identity))
u userSrv.get(username)
Expand All @@ -78,7 +78,7 @@ class ADAuthSrvFactory @Inject() (
}
}

def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = {
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
Expand All @@ -98,7 +98,5 @@ class ADAuthSrvFactory @Inject() (
Future.failed(AuthorizationError("Change password failure"))
}
}

def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = Future.failed(AuthorizationError("Operation not supported"))
}
}
131 changes: 96 additions & 35 deletions app/org/elastic4play/services/auth/LdapAuthSrv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,31 @@ import javax.naming.Context
import javax.naming.directory._

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

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

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

@Singleton
class LdapAuthSrvFactory @Inject() (
configuration: Configuration,
userSrv: UserSrv,
ec: ExecutionContext) extends AuthSrvFactory { factory
ec: ExecutionContext) extends AuthSrvFactory {
val name = "ldap"

def getAuthSrv: AuthSrv = new LdapAuthSrv(
configuration.get[String]("auth.ldap.serverName"),
configuration.getOptional[Boolean]("auth.ldap.useSSL").getOrElse(false),
configuration.get[String]("auth.ldap.bindDN"),
configuration.get[String]("auth.ldap.bindPW"),
configuration.get[String]("auth.ldap.baseDN"),
configuration.get[String]("auth.ldap.filter"),
configuration.getOptional[String]("auth.ldap.keyAttribute"),
configuration.getOptional[String]("auth.ldap.keyFilter"),
configuration.getOptional[String]("auth.ldap.loginAttribute").getOrElse("uid"),
userSrv,
ec)

Expand All @@ -37,28 +41,20 @@ class LdapAuthSrvFactory @Inject() (
bindPW: String,
baseDN: String,
filter: String,
keyAttribute: Option[String],
keyFilter: Option[String],
loginAttribute: String,
userSrv: UserSrv,
implicit val ec: ExecutionContext) extends AuthSrv {

private[LdapAuthSrv] lazy val logger = Logger(getClass)
val name = "ldap"
val capabilities = Set(AuthCapability.changePassword)
override val capabilities: Set[AuthCapability.Value] = keyAttribute match {
case Some(_) Set(AuthCapability.changePassword, AuthCapability.renewKey)
case None Set(AuthCapability.changePassword)
}

@Inject() def this(
configuration: Configuration,
userSrv: UserSrv,
ec: ExecutionContext) =
this(
configuration.get[String]("auth.ldap.serverName"),
configuration.getOptional[Boolean]("auth.ldap.useSSL").getOrElse(false),
configuration.get[String]("auth.ldap.bindDN"),
configuration.get[String]("auth.ldap.bindPW"),
configuration.get[String]("auth.ldap.baseDN"),
configuration.get[String]("auth.ldap.filter"),
userSrv,
ec)

private[auth] def connect[A](username: String, password: String)(f: InitialDirContext A): Try[A] = {
private[auth] def connect[A](username: String, password: String)(f: InitialDirContext Try[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")
Expand All @@ -71,6 +67,7 @@ class LdapAuthSrvFactory @Inject() (
try f(ctx)
finally ctx.close()
}
.flatten
}

private[auth] def getUserDN(ctx: InitialDirContext, username: String): Try[String] = {
Expand All @@ -84,46 +81,110 @@ class LdapAuthSrvFactory @Inject() (
}
}

def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = {
private[auth] def getUserNameFromKey(ctx: InitialDirContext, key: String): Try[String] = {
Try {
val controls = new SearchControls()
controls.setSearchScope(SearchControls.SUBTREE_SCOPE)
controls.setCountLimit(1)
val searchResult = ctx.search(baseDN, keyFilter.getOrElse(throw AuthenticationError("Authentication by key is not possible as auth.ldap.keyFilter is not configured")), Array[Object](key), controls)
if (searchResult.hasMore) searchResult.next().getAttributes.get(loginAttribute).get().toString
else throw AuthenticationError("User not found in LDAP server")
}
}

override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = {
connect(bindDN, bindPW) { ctx
getUserDN(ctx, username)
}
.flatten
.flatMap { userDN
connect(userDN, password) { _
userSrv.get(username)
.flatMap { u userSrv.getFromUser(request, u) }
}
.flatMap { userDN connect(userDN, password)(_ Success(())) }
.map { _
userSrv.get(username)
.flatMap { u userSrv.getFromUser(request, u) }
}
.recover { case t Future.failed(t) }
.get
.fold[Future[AuthContext]](Future.failed, identity)
.recoverWith {
case t
logger.error("LDAP authentication failure", t)
Future.failed(AuthenticationError("Authentication failure"))
}
}

def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = {
val changeTry = connect(bindDN, bindPW) { ctx
override def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = {
keyFilter
.map { _
connect(bindDN, bindPW) { ctx
getUserNameFromKey(ctx, key)
}
.map(username userSrv.getFromId(request, username))
.fold(Future.failed, identity)
.recoverWith {
case t
logger.error("LDAP authentication failure", t)
Future.failed(AuthenticationError("Authentication failure"))
}
}
.getOrElse(Future.failed(AuthorizationError("ldap authenticator doesn't support api key authentication")))
}

override def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = {
connect(bindDN, bindPW) { ctx
getUserDN(ctx, username)
}
.flatten
.flatMap { userDN
connect(userDN, oldPassword) { ctx
val mods = Array(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("userPassword", newPassword)))
ctx.modifyAttributes(userDN, mods)
Try(ctx.modifyAttributes(userDN, mods))
}
}
Future
.fromTry(changeTry)
.fold(Future.failed, Future.successful)
.recoverWith {
case t
logger.error("LDAP change password failure", t)
Future.failed(AuthorizationError("Change password failure"))
}
}

def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = Future.failed(AuthorizationError("Operation not supported"))
override def renewKey(username: String)(implicit authContext: AuthContext): Future[String] = {
keyAttribute.map { ka
connect(bindDN, bindPW) { ctx
getUserDN(ctx, username).flatMap { userDN
val newKey = generateKey()
val mods = Array(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(ka, newKey)))
Try(ctx.modifyAttributes(userDN, mods)).map(_ newKey)
}
}
.fold(Future.failed, Future.successful)
.recoverWith {
case t
logger.error("LDAP renew key failure", t)
Future.failed(AuthorizationError("Renew key failure"))
}
}
.getOrElse(Future.failed(AuthorizationError("Operation not supported")))
}

override def getKey(username: String)(implicit authContext: AuthContext): Future[String] = {
keyAttribute.map { ka
connect(bindDN, bindPW) { ctx
Try {
val controls = new SearchControls()
controls.setSearchScope(SearchControls.SUBTREE_SCOPE)
controls.setCountLimit(1)
val searchResult = ctx.search(baseDN, filter, Array[Object](username), controls)
if (searchResult.hasMore) {
searchResult.next().getAttributes.get(ka).get().toString
}
else throw AuthenticationError("User not found in LDAP server")
}
}
.fold(Future.failed, Future.successful)
.recoverWith {
case t
logger.error("LDAP renew key failure", t)
Future.failed(AuthorizationError("Renew key failure"))
}
}
.getOrElse(Future.failed(AuthorizationError("Operation not supported")))
}
}
}
19 changes: 15 additions & 4 deletions app/org/elastic4play/services/auth/MultiAuthSrv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,32 @@ class MultiAuthSrv(
ec)

val name = "multi"
def capabilities: Set[Type] = authProviders.flatMap(_.capabilities).toSet
override val capabilities: Set[Type] = authProviders.flatMap(_.capabilities).toSet

private[auth] def forAllAuthProvider[A](body: AuthSrv Future[A]) = {
authProviders.foldLeft(Future.failed[A](new Exception("no authentication provider found"))) {
(f, a) f.recoverWith { case _ body(a) }
}
}

def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] =
override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] =
forAllAuthProvider(_.authenticate(username, password))
.recoverWith { case _ Future.failed(AuthenticationError("Authentication failure")) }

def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] =
override def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] =
forAllAuthProvider(_.authenticate(key))
.recoverWith { case _ Future.failed(AuthenticationError("Authentication failure")) }

override def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] =
forAllAuthProvider(_.changePassword(username, oldPassword, newPassword))

def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] =
override def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] =
forAllAuthProvider(_.setPassword(username, newPassword))

override def renewKey(username: String)(implicit authContext: AuthContext): Future[String] =
forAllAuthProvider(_.renewKey(username))

override def getKey(username: String)(implicit authContext: AuthContext): Future[String] =
forAllAuthProvider(_.getKey(username))

}

0 comments on commit 0529d84

Please sign in to comment.