Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: OpenID based authentication for SMUI #62

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions app/controllers/OpenidController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package controllers

import javax.inject.Inject
import play.api.libs.json.Json
import play.api.mvc._
import play.api.http.HttpErrorHandler
import play.api.{Configuration, Logging}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
import scalaj.http.{Http, HttpOptions}
import pdi.jwt.{Jwt, JwtOptions, JwtAlgorithm, JwtClaim, JwtJson}



class OpenidController @Inject()(override val controllerComponents: ControllerComponents, errorHandler: HttpErrorHandler, appConfig: Configuration)(implicit ec: ExecutionContext) extends AbstractController(controllerComponents) with Logging {

private val JWT_COOKIE = getValueFromConfigWithFallback("smui.JWTOpenIdAuthenticatedAction.cookie.name", "jwt")

private def redirectToHomePage(): Future[Result] = {
Future {
Results.Redirect("http://localhost:9000/")
}
}

private def getValueFromConfigWithFallback(key: String, default: String): String = {
appConfig.getOptional[String](key) match {
case Some(value: String) => value
case None =>
logger.warn(s":: No value for $key found. Setting pass to super-default.")
default
}
}

def callback() = Action { implicit request: Request[AnyContent] =>
logger.warn("Here is the authorization code: " + request.getQueryString("code"))


val code: Option[String] = request getQueryString "code"
val upper = code map { _.trim } filter { _.length != 0 }


logger.warn("We now have a Authorization Code, and now we need to convert it to a Access Token.")

val result = Http("http://keycloak:9080/auth/realms/smui/protocol/openid-connect/token").postForm
.param("grant_type", "authorization_code")
.param("client_id", "smui")
.param("redirect_uri","http://localhost:9000/auth/openid/callback")
.param("code", upper getOrElse "")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Charset", "UTF-8")
.option(HttpOptions.readTimeout(10000)).asString

logger.warn(s"Result is $result" )

val responseJson = Json.parse(result.body)

val accessToken : String = responseJson("access_token").as[String]

val decodedAccessToken = Jwt
.decodeRawAll(
accessToken,
JwtOptions(signature = false, expiration = false, notBefore = false)
)


logger.warn("Decoded access token: " + decodedAccessToken)

// This should come from the decodedAccessToken, not from the responseJson ;-(
val scope : String = responseJson("scope").as[String]


logger.warn("Scope is " + scope)



Results.Redirect("http://localhost:9000/health").withCookies(Cookie(JWT_COOKIE, accessToken))
}
}
109 changes: 109 additions & 0 deletions app/controllers/auth/JWTOpenIdAuthenticatedAction.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package controllers.auth

import com.jayway.jsonpath.JsonPath
import net.minidev.json.JSONArray
import pdi.jwt.{Jwt, JwtOptions, JwtAlgorithm, JwtClaim, JwtJson}
import play.api.mvc._
import pdi.jwt._
import play.api.{Configuration, Logging}
import scalaj.http.{Http, HttpOptions}
import play.api.libs.json.Json
import java.util.Base64
import scala.util.Success

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

class JWTOpenIdAuthenticatedAction(parser: BodyParsers.Default, appConfig: Configuration)(implicit ec: ExecutionContext)
extends ActionBuilderImpl(parser) with Logging {

logger.warn("In JWTOpenIdAuthenticatedAction")

private val JWT_LOGIN_URL = getValueFromConfigWithFallback("smui.JWTOpenIdAuthenticatedAction.login.url", "")
private val JWT_COOKIE = getValueFromConfigWithFallback("smui.JWTOpenIdAuthenticatedAction.cookie.name", "jwt")
private val JWT_AUTHORIZED_ROLES = getValueFromConfigWithFallback("smui.JWTOpenIdAuthenticatedAction.authorization.roles", "admin")

private lazy val authorizedRoles = JWT_AUTHORIZED_ROLES.replaceAll("\\s", "").split(",").toSeq

private def getValueFromConfigWithFallback(key: String, default: String): String = {
appConfig.getOptional[String](key) match {
case Some(value: String) => value
case None =>
logger.warn(s":: No value for $key found. Setting pass to super-default.")
default
}
}



def decodeRawAll(jwt: String): Try[(String, String, String)] = {
Jwt
.decodeRawAll(
jwt,
JwtOptions(signature = false, expiration = false, notBefore = false)
)
}

//private def isAuthenticated(jwt: String): Option[JwtClaim] = {
private def isAuthenticated(jwt: String): Option[Boolean] = {
epugh marked this conversation as resolved.
Show resolved Hide resolved
logger.warn(s"\n Trying to deal with $jwt")


logger.warn("We should validate the token")

val result = for {
(header, claim, signature) <-
decodeRawAll(jwt)
} yield claim


logger.warn("claim: " + claim)



Some(true)
}

private def isAuthorized(token: String): Boolean = {
val rolesInToken = token.split(" ").toSeq
logger.warn("Here are the rolesinToken:" + rolesInToken)
rolesInToken.forall(authorizedRoles.contains)
}

private def redirectToLoginPage(): Future[Result] = {
Future {
Results.Redirect(JWT_LOGIN_URL)
}
}

private def getJwtCookie[A](request: Request[A]): Option[Cookie] = {
request.cookies.get(JWT_COOKIE)
}



override def invokeBlock[A](request: Request[A], block: Request[A] => Future[Result]): Future[Result] = {

logger.warn(s":: invokeBlock :: request.path = ${request.path}")

if (request.path == "/auth/openid/callback"){
// https://www.appsdeveloperblog.com/keycloak-authorization-code-grant-example/
logger.warn("We got a callback!!!!");

}
getJwtCookie(request) match {
case Some(cookie) =>
isAuthenticated(cookie.value) match {
case Some(token) if isAuthorized("smui:searchandizder") => block(request)
case _ => redirectToLoginPage()
}
case None => redirectToLoginPage()
}

//if (isAuthorized("smui:searchandizer")) {
// block(request)
//} else {
// redirectToLoginPage
//}
}
}
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ version := "3.12.1"

scalaVersion := "2.12.11"


lazy val root = (project in file("."))
.enablePlugins(PlayScala)
.enablePlugins(BuildInfoPlugin)
Expand Down Expand Up @@ -49,6 +50,7 @@ libraryDependencies ++= {
"org.playframework.anorm" %% "anorm" % "2.6.4",
"com.typesafe.play" %% "play-json" % "2.6.12",
"com.pauldijou" %% "jwt-play" % "4.1.0",
"org.scalaj" %% "scalaj-http" % "2.3.0",
"org.scalatestplus.play" %% "scalatestplus-play" % "3.1.0" % Test,
"org.mockito" % "mockito-all" % "1.10.19" % Test,
"com.pauldijou" %% "jwt-play" % "4.1.0",
Expand Down
2 changes: 2 additions & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
GET / controllers.FrontendController.index()
GET /health controllers.HealthController.health

GET /auth/openid/callback controllers.OpenidController.callback

# serve the API v1 Specification
# TODO search-input URL path partially "behind" solrIndexId path component and partially not
GET /api/v1/featureToggles controllers.ApiController.getFeatureToggles
Expand Down