-
Notifications
You must be signed in to change notification settings - Fork 236
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
355 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package org.thp.cortex.services | ||
|
||
import scala.concurrent.{ ExecutionContext, Future } | ||
|
||
import play.api.http.Status | ||
import play.api.libs.json.{ JsObject, JsValue } | ||
import play.api.libs.ws.WSClient | ||
import play.api.mvc.RequestHeader | ||
import play.api.{ Configuration, Logger } | ||
|
||
import akka.stream.Materializer | ||
import javax.inject.{ Inject, Singleton } | ||
import org.thp.cortex.services.mappers.UserMapper | ||
|
||
import org.elastic4play.services.{ AuthContext, AuthSrv } | ||
import org.elastic4play.{ AuthenticationError, AuthorizationError, OAuth2Redirect } | ||
|
||
case class OAuth2Config( | ||
clientId: String, | ||
clientSecret: String, | ||
redirectUri: String, | ||
responseType: String, | ||
grantType: String, | ||
authorizationUrl: String, | ||
tokenUrl: String, | ||
userUrl: String, | ||
scope: String, | ||
autocreate: Boolean) | ||
|
||
object OAuth2Config { | ||
def apply(configuration: Configuration): Option[OAuth2Config] = { | ||
for { | ||
clientId ← configuration.getOptional[String]("auth.oauth2.clientId") | ||
clientSecret ← configuration.getOptional[String]("auth.oauth2.clientSecret") | ||
redirectUri ← configuration.getOptional[String]("auth.oauth2.redirectUri") | ||
responseType ← configuration.getOptional[String]("auth.oauth2.responseType") | ||
grantType ← configuration.getOptional[String]("auth.oauth2.grantType") | ||
authorizationUrl ← configuration.getOptional[String]("auth.oauth2.authorizationUrl") | ||
userUrl ← configuration.getOptional[String]("auth.oauth2.userUrl") | ||
tokenUrl ← configuration.getOptional[String]("auth.oauth2.tokenUrl") | ||
scope ← configuration.getOptional[String]("auth.oauth2.scope") | ||
autocreate = configuration.getOptional[Boolean]("auth.sso.autocreate").getOrElse(false) | ||
} yield OAuth2Config(clientId, clientSecret, redirectUri, responseType, grantType, authorizationUrl, tokenUrl, userUrl, scope, autocreate) | ||
} | ||
} | ||
|
||
@Singleton | ||
class OAuth2Srv( | ||
ws: WSClient, | ||
userSrv: UserSrv, | ||
ssoMapper: UserMapper, | ||
oauth2Config: Option[OAuth2Config], | ||
implicit val ec: ExecutionContext, | ||
implicit val mat: Materializer) | ||
extends AuthSrv { | ||
|
||
@Inject() def this( | ||
ws: WSClient, | ||
ssoMapper: UserMapper, | ||
userSrv: UserSrv, | ||
configuration: Configuration, | ||
ec: ExecutionContext, | ||
mat: Materializer) = this( | ||
ws, | ||
userSrv, | ||
ssoMapper, | ||
OAuth2Config(configuration), | ||
ec, | ||
mat) | ||
|
||
override val name: String = "oauth2" | ||
private val logger = Logger(getClass) | ||
|
||
val Oauth2TokenQueryString = "code" | ||
|
||
private def withOAuth2Config[A](body: OAuth2Config ⇒ Future[A]): Future[A] = { | ||
oauth2Config.fold[Future[A]](Future.failed(AuthenticationError("OAuth2 not configured properly")))(body) | ||
} | ||
|
||
override def authenticate()(implicit request: RequestHeader): Future[AuthContext] = { | ||
withOAuth2Config { cfg ⇒ | ||
request.queryString | ||
.get(Oauth2TokenQueryString) | ||
.flatMap(_.headOption) | ||
.fold(createOauth2Redirect(cfg.clientId)) { code ⇒ | ||
getAuthTokenAndAuthenticate(cfg.clientId, code) | ||
} | ||
} | ||
} | ||
|
||
private def getAuthTokenAndAuthenticate(clientId: String, code: String)(implicit request: RequestHeader): Future[AuthContext] = { | ||
logger.debug("Getting user token with the code from the response!") | ||
withOAuth2Config { cfg ⇒ | ||
ws.url(cfg.tokenUrl) | ||
.post(Map( | ||
"code" → code, | ||
"grant_type" → cfg.grantType, | ||
"client_secret" → cfg.clientSecret, | ||
"redirect_uri" → cfg.redirectUri, | ||
"client_id" → clientId)) | ||
.recoverWith { | ||
case error ⇒ | ||
logger.error(s"Token verification failure", error) | ||
Future.failed(AuthenticationError("Token verification failure")) | ||
} | ||
.flatMap { r ⇒ | ||
r.status match { | ||
case Status.OK ⇒ | ||
val accessToken = (r.json \ "access_token").asOpt[String].getOrElse("") | ||
val authHeader = "Authorization" → s"bearer $accessToken" | ||
ws.url(cfg.userUrl) | ||
.addHttpHeaders(authHeader) | ||
.get().flatMap { userResponse ⇒ | ||
if (userResponse.status != Status.OK) { | ||
Future.failed(AuthenticationError(s"unexpected response from server: ${userResponse.status} ${userResponse.body}")) | ||
} | ||
else { | ||
val response = userResponse.json.asInstanceOf[JsObject] | ||
getOrCreateUser(response, authHeader) | ||
} | ||
} | ||
case _ ⇒ | ||
logger.error(s"unexpected response from server: ${r.status} ${r.body}") | ||
Future.failed(AuthenticationError("unexpected response from server")) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private def getOrCreateUser(response: JsValue, authHeader: (String, String))(implicit request: RequestHeader): Future[AuthContext] = { | ||
withOAuth2Config { cfg ⇒ | ||
ssoMapper.getUserFields(response, Some(authHeader)).flatMap { | ||
userFields ⇒ | ||
val userId = userFields.getString("login").getOrElse("") | ||
userSrv.get(userId).flatMap(user ⇒ { | ||
userSrv.getFromUser(request, user, name) | ||
}).recoverWith { | ||
case authErr: AuthorizationError ⇒ Future.failed(authErr) | ||
case _ if cfg.autocreate ⇒ | ||
userSrv.inInitAuthContext { implicit authContext ⇒ | ||
userSrv.create(userFields).flatMap(user ⇒ { | ||
userSrv.getFromUser(request, user, name) | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
private def createOauth2Redirect(clientId: String): Future[AuthContext] = { | ||
withOAuth2Config { cfg ⇒ | ||
val queryStringParams = Map[String, Seq[String]]( | ||
"scope" → Seq(cfg.scope), | ||
"response_type" → Seq(cfg.responseType), | ||
"redirect_uri" → Seq(cfg.redirectUri), | ||
"client_id" → Seq(clientId)) | ||
Future.failed(OAuth2Redirect(cfg.authorizationUrl, queryStringParams)) | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package org.thp.cortex.services.mappers | ||
|
||
import scala.concurrent.{ ExecutionContext, Future } | ||
|
||
import play.api.Configuration | ||
import play.api.libs.json._ | ||
import play.api.libs.ws.WSClient | ||
|
||
import javax.inject.Inject | ||
|
||
import org.elastic4play.AuthenticationError | ||
import org.elastic4play.controllers.Fields | ||
|
||
class GroupUserMapper( | ||
loginAttrName: String, | ||
nameAttrName: String, | ||
rolesAttrName: Option[String], | ||
groupAttrName: String, | ||
defaultRoles: Seq[String], | ||
groupsUrl: String, | ||
mappings: Map[String, Seq[String]], | ||
ws: WSClient, | ||
implicit val ec: ExecutionContext) extends UserMapper { | ||
|
||
@Inject() def this( | ||
|
||
configuration: Configuration, | ||
ws: WSClient, | ||
ec: ExecutionContext) = this( | ||
configuration.getOptional[String]("auth.sso.attributes.login").getOrElse("name"), | ||
configuration.getOptional[String]("auth.sso.attributes.name").getOrElse("username"), | ||
configuration.getOptional[String]("auth.sso.attributes.roles"), | ||
configuration.getOptional[String]("auth.sso.attributes.groups").getOrElse(""), | ||
configuration.getOptional[Seq[String]]("auth.sso.defaultRoles").getOrElse(Seq()), | ||
configuration.getOptional[String]("auth.sso.groups.url").getOrElse(""), | ||
configuration.getOptional[Map[String, Seq[String]]]("auth.sso.groups.mappings").getOrElse(Map()), | ||
ws, | ||
ec) | ||
|
||
override val name: String = "group" | ||
|
||
override def getUserFields(jsValue: JsValue, authHeader: Option[(String, String)]): Future[Fields] = { | ||
|
||
val apiCall = authHeader.fold(ws.url(groupsUrl))(headers ⇒ ws.url(groupsUrl).addHttpHeaders(headers)) | ||
apiCall.get.flatMap { r ⇒ | ||
val jsonGroups = (r.json \ groupAttrName).as[Seq[String]] | ||
val mappedRoles = jsonGroups.flatMap(mappings.get).maxBy(_.length) | ||
val roles = if (mappedRoles.nonEmpty) mappedRoles else defaultRoles | ||
|
||
val fields = for { | ||
login ← (jsValue \ loginAttrName).validate[String] | ||
name ← (jsValue \ nameAttrName).validate[String] | ||
} yield Fields(Json.obj( | ||
"login" → login, | ||
"name" → name, | ||
"roles" → roles)) | ||
fields match { | ||
case JsSuccess(f, _) ⇒ Future.successful(f) | ||
case JsError(errors) ⇒ Future.failed(AuthenticationError(s"User info fails: ${errors.map(_._1).mkString}")) | ||
} | ||
} | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
app/org/thp/cortex/services/mappers/MultiUserMapperSrv.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package org.thp.cortex.services.mappers | ||
|
||
import scala.collection.immutable | ||
import scala.concurrent.Future | ||
|
||
import play.api.Configuration | ||
import play.api.libs.json.JsValue | ||
|
||
import javax.inject.{ Inject, Singleton } | ||
|
||
import org.elastic4play.controllers.Fields | ||
|
||
object MultiUserMapperSrv { | ||
def getMapper(configuration: Configuration, ssoMapperModules: immutable.Set[UserMapper]): UserMapper = { | ||
val name = configuration.getOptional[String]("auth.sso.mapper").getOrElse("simple") | ||
ssoMapperModules.find(_.name == name).get | ||
} | ||
} | ||
|
||
@Singleton | ||
class MultiUserMapperSrv @Inject() ( | ||
configuration: Configuration, | ||
ssoMapperModules: immutable.Set[UserMapper]) extends UserMapper { | ||
|
||
override val name: String = "usermapper" | ||
private lazy val mapper: UserMapper = MultiUserMapperSrv.getMapper(configuration, ssoMapperModules) | ||
|
||
override def getUserFields(jsValue: JsValue, authHeader: Option[(String, String)]): Future[Fields] = { | ||
mapper.getUserFields(jsValue, authHeader) | ||
} | ||
|
||
} |
Oops, something went wrong.