Skip to content

Instantly share code, notes, and snippets.

@yan130
Created March 13, 2018 21:20
Show Gist options
  • Save yan130/47eb8e5ad7784a62230a5a35c66db4d9 to your computer and use it in GitHub Desktop.
Save yan130/47eb8e5ad7784a62230a5a35c66db4d9 to your computer and use it in GitHub Desktop.
LDAPProvider for silhouette authenticate
package utils.auth
import com.mohiva.play.silhouette.impl.providers._
import com.mohiva.play.silhouette.api._
import com.mohiva.play.silhouette.impl.exceptions.ProfileRetrievalException
import com.mohiva.play.silhouette.api.util.{ ExecutionContextProvider, ExtractableRequest, HTTPLayer }
import com.mohiva.play.silhouette.impl.exceptions.{ AccessDeniedException, UnexpectedResponseException }
import com.mohiva.play.silhouette.impl.providers.state.UserStateItemHandler
import com.unboundid.ldap.sdk._
import com.unboundid.util.ssl.{JVMDefaultTrustManager, SSLUtil, TrustAllTrustManager}
import play.api.Logger
import play.api.mvc._
import scala.concurrent.Future
import java.net.URI
import javax.net.ssl.SSLSocketFactory
import play.api.libs.json._
import play.api.libs.ws.WSResponse
case class LDAPSettings(
hostname: String,
group: String,
port: Int,
baseDN: String,
userDN: String,
groupDN: String,
objectClass: String,
trustAllCertificates: Boolean
)
case class LDAPInfo(
code: String,
username: String,
password: String
) extends AuthInfo
class LDAPProfileParser
extends SocialProfileParser[SearchResultEntry, CommonSocialProfile, LDAPInfo] {
val ID = "LDAP"
/**
* Parses the social profile.
*
* @param json The content returned from the provider.
* @return The social profile from given result.
*/
override def parse(searchEntry: SearchResultEntry, authInfo: LDAPInfo) = Future.successful{
CommonSocialProfile(
loginInfo = LoginInfo(ID, searchEntry.getAttributeValue("uid")),
firstName = Some(searchEntry.getAttributeValue("givenName")),
lastName = Some(searchEntry.getAttributeValue("sn")),
fullName = Some(searchEntry.getAttributeValue("givenName") + " "+ searchEntry.getAttributeValue("sn")),
email = Some(searchEntry.getAttributeValue("mail")),
avatarURL = None
)
}
}
class LDAPProvider (protected val httpLayer: HTTPLayer, val settings: LDAPSettings)
extends SocialProvider with CommonSocialProfileBuilder{
/**
* The type of the auth info.
*/
override type A = LDAPInfo
/**
* The settings type.
*/
override type Settings = LDAPSettings
/**
* The content type to parse a profile from.
*/
override type Content = SearchResultEntry
/**
* The provider ID.
*/
override val id = "LDAP"
/**
* The type of this class.
*/
type Self = LDAPProvider
/**
* The profile parser.
*/
val profileParser = new LDAPProfileParser
/**
* Gets a provider initialized with a new settings object.
*
* @param f A function which gets the settings passed and returns different settings.
* @return An instance of the provider initialized with new settings.
*/
def withSettings(f: (Settings) => Settings) = {
new LDAPProvider(httpLayer, f(settings))
}
/**
* Defines the URLs that are needed to retrieve the profile data.
* this is not used in LDAP
*/
override protected val urls = Map("hostname" -> settings.hostname)
val SpecifiedProfileError = "[Silhouette][%s] Error retrieving profile information. Error code: %s, message: %s"
val AuthorizationError = "[Silhouette][%s] Authorization server returned error: %s"
/**
* Builds the social profile.
*
* @param authInfo The auth info received from the provider.
* @return On success the build social profile, otherwise a failure.
*/
override protected def buildProfile(authInfo: LDAPInfo): Future[Profile] = {
val username = authInfo.username
val password = authInfo.password
val baseUserNamespace = settings.userDN + "," + settings.baseDN
val baseGroupNamespace = settings.groupDN + "," + settings.baseDN
val trustManager = if (settings.trustAllCertificates) new TrustAllTrustManager() else JVMDefaultTrustManager.getInstance()
val sslUtil: SSLUtil = new SSLUtil(trustManager)
val socketFactory: SSLSocketFactory = sslUtil.createSSLSocketFactory()
var ldapConnection: LDAPConnection = null
try {
// Create LDAP connection
ldapConnection = new LDAPConnection(socketFactory, settings.hostname, settings.port)
// Bind user to the connection.
// This will throw an exception if the user credentials do not match any LDAP entry.
// This exception is later caught to refuse access to the user.
val dn = "uid=" + username + "," + baseUserNamespace
ldapConnection.bind(dn, password)
// Filter to search the user's membership in the specified group
val searchFilter: com.unboundid.ldap.sdk.Filter = com.unboundid.ldap.sdk.Filter.create("(&(objectClass=" +
settings.objectClass + ")(memberOf=cn=" + settings.group + "," + baseGroupNamespace + ")(uid=" + username + "))")
// Perform group membership search
val searchResult: SearchResult = ldapConnection.search(settings.baseDN, SearchScope.SUB, searchFilter)
// User is part of the specified group
if (searchResult.getEntryCount == 1) {
// Logger.debug("LDAP search result: " + searchResult.getSearchEntry(dn))
val searchEntry = searchResult.getSearchEntry(dn)
profileParser.parse(searchEntry, authInfo)
}
// User is not part of the specified group
else {
throw new ProfileRetrievalException(SpecifiedProfileError.format(id, 403, "user not in the group"))
}
} catch {
case e: Exception => {
// TODO: change error code.
throw new ProfileRetrievalException(SpecifiedProfileError.format(id, 403, e.getMessage))
}
}
finally {
// Close connection
if (ldapConnection != null)
ldapConnection.close()
}
}
def authenticate[B]()(implicit request: ExtractableRequest[B]): Future[Either[Result, LDAPInfo]] = {
request.extractString("error").map {
// TODO: may remove this part is not used.
// refer to https://github.com/mohiva/play-silhouette/blob/master/silhouette/app/com/mohiva/play/silhouette/impl/providers/OAuth2Provider.scala
case e @ "access_denied" => new AccessDeniedException(AuthorizationError.format(id, e))
case e => new UnexpectedResponseException(AuthorizationError.format(id, e))
} match {
case Some(throwable) => Future.failed(throwable)
case None => request.extractString("code") match {
// We're being redirected back from the authorization server with the access code and the state
case Some(code) => {
val authInfo = LDAPInfo(code = request.extractString("code").getOrElse(""),
username=request.extractString("username").getOrElse(""),
password=request.extractString("password").getOrElse(""))
Future.successful(Right(authInfo))
}
// There's no code in the request, this is the first step in the OAuth flow, i.e. the webpage to fill in username
// & password.
case None => {
Future.successful(Left(Results.Redirect("ldapinput")))
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment