Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import com.neu.api.*
import com.neu.service.Utils
import com.neu.service.authentication.AuthService
import org.apache.pekko.http.scaladsl.server.Route
import org.apache.pekko.http.scaladsl.model.headers.HttpCookie

import java.nio.charset.StandardCharsets
import java.util.Base64
import java.util.UUID

//noinspection UnstableApiUsage
class OpenIdAuthApi(
Expand All @@ -19,13 +24,29 @@ class OpenIdAuthApi(
optionalHeaderValueByName("Host") { host =>
parameter(Symbol("serverName").?) { serverName =>
Utils.respondWithWebServerHeaders() {
authService.getResources(
code,
state,
ip.toString(),
host,
serverName
)
if (code.isEmpty && state.isEmpty) {
// Initial request: generate nonce and set cookie
val nonce = java.util.UUID.randomUUID().toString
val encodedNonce =
Base64.getEncoder.encodeToString(nonce.getBytes(StandardCharsets.UTF_8))
setCookie(HttpCookie("temp", encodedNonce)) {
authService.getResources(code, state, ip.toString(), host, serverName, nonce)
}
} else {
// OAuth callback: read nonce from existing cookie, do not overwrite it
optionalCookie("temp") { tempCookie =>
val nonce = tempCookie
.flatMap(c =>
scala.util
.Try(
new String(Base64.getDecoder.decode(c.value), StandardCharsets.UTF_8)
)
.toOption
)
.getOrElse("")
authService.getResources(code, state, ip.toString(), host, serverName, nonce)
}
}
}
}
}
Expand All @@ -34,8 +55,17 @@ class OpenIdAuthApi(
} ~
(patch & path(openId)) {
extractClientIP { ip =>
Utils.respondWithWebServerHeaders() {
authService.validateToken(None, Some(ip))
optionalCookie("temp") { tempCookie =>
val nonce = tempCookie.flatMap(c =>
scala.util
.Try(new String(Base64.getDecoder.decode(c.value), StandardCharsets.UTF_8))
.toOption
)
Utils.respondWithWebServerHeaders() {
deleteCookie("temp") {
authService.validateToken(None, Some(ip), nonce)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,56 @@ class SamlAuthApi(
private val saml = "token_auth_server"
private val samlslo = "token_auth_server_slo"
private val rootPath = "/"
private val samlKey = "samlSso"

val route: Route =
(get & path(saml)) {
extractClientIP { _ =>
optionalHeaderValueByName("Host") { host =>
parameter(Symbol("serverName").?) { serverName =>
Utils.respondWithWebServerHeaders() {
authService.getResources(None, None, "", host, serverName)
val nonce = java.util.UUID.randomUUID().toString
val encodedNonce =
Base64.getEncoder.encodeToString(nonce.getBytes(StandardCharsets.UTF_8))
setCookie(HttpCookie("temp", encodedNonce)) {
Utils.respondWithWebServerHeaders() {
authService.getResources(None, None, "", host, serverName, nonce)
}
}
}
}
}
} ~
(patch & path(saml)) {
extractClientIP { _ =>
Utils.respondWithWebServerHeaders() {
authService.validateToken(None, None)
optionalCookie("temp") { tempCookie =>
val nonce = tempCookie.flatMap { c =>
scala.util
.Try(
new String(Base64.getDecoder.decode(c.value), StandardCharsets.UTF_8)
)
.toOption
}
deleteCookie("temp") {
Utils.respondWithWebServerHeaders() {
authService.validateToken(None, None, nonce)
}
}
}
}
} ~
(post & path(saml)) {
extractClientIP { ip =>
optionalHeaderValueByName("Host") {
case Some(host) =>
val text = Base64.getEncoder.encodeToString(samlKey.getBytes(StandardCharsets.UTF_8))

setCookie(HttpCookie("temp", text)) {
optionalCookie("temp") { tempCookie =>
val nonce = tempCookie
.flatMap(c =>
scala.util
.Try(new String(Base64.getDecoder.decode(c.value), StandardCharsets.UTF_8))
.toOption
)
.getOrElse("")
extractRequestContext { ctx =>
authService.login(ip, host, ctx)
authService.login(ip, host, ctx, nonce)
}
}
case None =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class SuseAuthApi(
extractClientIP { ip =>
extractRequestContext { ctx =>
Utils.respondWithWebServerHeaders() {
authService.login(ip, "", ctx)
authService.login(ip, "", ctx, "")
}
}
}
Expand All @@ -34,7 +34,7 @@ class SuseAuthApi(
pathPrefix("heartbeat") {
patch {
Utils.respondWithWebServerHeaders() {
authService.validateToken(Some(tokenId), None)
authService.validateToken(Some(tokenId), None, None)
}
}
} ~
Expand Down
22 changes: 22 additions & 0 deletions admin/src/main/scala/com/neu/core/AuthenticationManager.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ object AuthenticationManager extends LazyLogging {
private val tokenMap: mutable.Map[String, UserTokenNew] =
scala.collection.mutable.Map[String, UserTokenNew]()

// Per-attempt SSO tokens keyed by nonce; entries expire after 5 minutes
private val ssoTtlMs: Long = 5L * 60 * 1000
private val pendingSsoMap: mutable.Map[String, (UserTokenNew, Long)] = mutable.Map.empty

def putSsoToken(nonce: String, userToken: UserTokenNew): Unit = {
cleanExpiredSsoTokens()
pendingSsoMap += nonce -> (userToken, System.currentTimeMillis())
}

def validateSsoToken(nonce: String): Option[UserTokenNew] =
pendingSsoMap.get(nonce).collect {
case (t, ts) if System.currentTimeMillis() - ts <= ssoTtlMs => t
}

def invalidateSsoToken(nonce: String): Unit =
pendingSsoMap -= nonce

private def cleanExpiredSsoTokens(): Unit = {
val now = System.currentTimeMillis()
pendingSsoMap.filterInPlace { case (_, (_, ts)) => now - ts <= ssoTtlMs }
}

private val tokenClusterMap: mutable.Map[String, String] =
scala.collection.mutable.Map[String, String]()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ object AuthTokenJsonProtocol extends DefaultJsonProtocol {

given groupMappedRoleFormat: RootJsonFormat[GroupMappedRole] = jsonFormat3(GroupMappedRole.apply)
given ldapServerFormat: RootJsonFormat[LdapServer] = jsonFormat13(LdapServer.apply)
given samlServerFormat: RootJsonFormat[SamlServer] = jsonFormat13(SamlServer.apply)
given samlServerFormat: RootJsonFormat[SamlServer] = jsonFormat14(SamlServer.apply)
given ssoServerFormat: RootJsonFormat[SsoServer] = jsonFormat3(SsoServer.apply)
given samlTokenFormat: RootJsonFormat[SamlToken] = jsonFormat3(SamlToken.apply)
given samlTokenFormat: RootJsonFormat[SamlToken] = jsonFormat4(SamlToken.apply)
given samlResponseFormat: RootJsonFormat[SamlResponse] = jsonFormat3(SamlResponse.apply)

given serverOIDCFormat: RootJsonFormat[ServerOIDC] = jsonFormat11(ServerOIDC.apply)
Expand All @@ -58,8 +58,8 @@ object AuthTokenJsonProtocol extends DefaultJsonProtocol {
given openIdServerConfigFormat: RootJsonFormat[OpenIdServerConfig] = jsonFormat2(
OpenIdServerConfig.apply
)
given redirectURLFormat: RootJsonFormat[RedirectURL] = jsonFormat1(RedirectURL.apply)
given samlRedirectURLFormat: RootJsonFormat[SamlRedirectURL] = jsonFormat2(SamlRedirectURL.apply)
given redirectURLFormat: RootJsonFormat[RedirectURL] = jsonFormat2(RedirectURL.apply)
given samlRedirectURLFormat: RootJsonFormat[SamlRedirectURL] = jsonFormat3(SamlRedirectURL.apply)

given ldapSettingFormat: RootJsonFormat[LdapSetting] = jsonFormat4(LdapSetting.apply)
given ldapSettingWrapFormat: RootJsonFormat[LdapSettingWrap] = jsonFormat1(LdapSettingWrap.apply)
Expand Down
2 changes: 1 addition & 1 deletion admin/src/main/scala/com/neu/model/OpenIdServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ case class OpenIdServerConfig(name: String, oidc: OpenIdServer)
* @param redirect_endpoint
* the redirect url
*/
case class RedirectURL(redirect_endpoint: String)
case class RedirectURL(redirect_endpoint: String, sso_nonce: Option[String] = None)
12 changes: 9 additions & 3 deletions admin/src/main/scala/com/neu/model/SamlServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ case class SamlServer(
slo_enabled: Option[Boolean],
slo_url: Option[String],
signing_cert: Option[String],
signing_key: Option[String]
signing_key: Option[String],
audience_uri: Option[String]
)

case class SamlConfig(name: String, saml: SamlServer)
Expand All @@ -25,12 +26,17 @@ case class SamlConfigWrap(config: SamlConfig)

case class SsoServer(server_name: String, server_type: String, redirect_url: String)

case class SamlRedirectURL(redirect_endpoint: String, issuer: String)
case class SamlRedirectURL(
redirect_endpoint: String,
issuer: String,
sso_nonce: Option[String] = None
)

case class SamlToken(
token: String,
state: Option[String] = None,
redirect_endpoint: Option[String] = None
redirect_endpoint: Option[String] = None,
sso_nonce: Option[String] = None
)

case class SamlResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ trait AuthService extends BaseService with DefaultJsonFormats with LazyLogging {
state: Option[String],
ip: String,
host: Option[String],
serverName: Option[String]
serverName: Option[String],
nonce: String
): Route

def validateToken(tokenId: Option[String], ip: Option[RemoteAddress]): Route
def validateToken(
tokenId: Option[String],
ip: Option[RemoteAddress],
nonce: Option[String]
): Route

def login(ip: RemoteAddress, host: String, ctx: RequestContext): Route
def login(ip: RemoteAddress, host: String, ctx: RequestContext, nonce: String): Route

def logout(host: Option[String], tokenId: String): Route

Expand Down
Loading
Loading