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

keycloak compatible for SaaS and self-hosted #14

Merged
merged 2 commits into from
Jun 15, 2023
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
2 changes: 1 addition & 1 deletion admin-web/src/api/infoAPI.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import http from "./http";
import {KeycloakConfig} from "keycloak-js";

type AuthTypeResponse = { type: 'ST' } | (KeycloakConfig & { type: 'Bearer' })
type AuthTypeResponse = { type: 'ST' } | (KeycloakConfig & { type: 'Bearer', saas: boolean })
export function getAppInfo() {
return http.get<AuthTypeResponse>('/info')
}
2 changes: 1 addition & 1 deletion admin-web/src/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function AppLayout() {
return (
<Layout style={{height: 'calc(100vh)'}}>
<Header style={{textAlign: 'center'}}>
<span style={{fontWeight: 'bold', fontSize: '20px'}}>ForNet Admin</span>
<span style={{fontWeight: 'bold', fontSize: '20px'}}>ForNet Manager (BETA)</span>
</Header>
<Content style={{padding: '0 50px'}}>
<AppBreadcrumb/>
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ auth {
# authServerUrl: "http://keycloak-dev.fornetcode.com",
# frontClientId : "fornet",
# # the user who has admin role can login in admin web, if undefined, anyone in the keycloak of realm can login
# # when server.saas enabled, this is useless
# adminRole: "admin",
# # the user who has client role can login in client, if undefined, anyone in the keycloak of realm can login
# # when server.saas enabled, this is useless
# clientRole: "client",
#}
#simple {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,26 @@ package com.timzaak.fornet.controller
import com.google.common.net.InetAddresses
import com.timzaak.fornet.config.AppConfig
import com.timzaak.fornet.controller.auth.AppAuthSupport
import com.timzaak.fornet.dao.{DB, Network, NetworkDao, NetworkProtocol, NetworkSetting}
import com.timzaak.fornet.dao.*
import com.timzaak.fornet.pubsub.NodeChangeNotifyService
import com.typesafe.config.Config
import org.hashids.Hashids
import very.util.security.IntID

import java.util.Base64
//import org.json4s.Formats
import com.timzaak.fornet.dao.NetworkStatus
import io.getquill.*
import org.scalatra.*
import org.scalatra.i18n.I18nSupport
import org.scalatra.json.*
import org.scalatra.*
import very.util.security.IntID.toIntID
import very.util.web.Controller
import very.util.web.validate.ValidationExtra
import very.util.security.IntID.toIntID
import zio.json.{ DeriveJsonDecoder, JsonDecoder }

import java.time.OffsetDateTime

case class CreateNetworkReq(name: String, addressRange: String, protocol:NetworkProtocol)
case class CreateNetworkReq(name: String, addressRange: String, protocol: NetworkProtocol)
given JsonDecoder[CreateNetworkReq] = DeriveJsonDecoder.gen
case class UpdateNetworkReq(
name: String,
Expand Down Expand Up @@ -55,8 +54,8 @@ trait NetworkController(
.filter(_.groupId == lift(groupId))
)(_.sortBy(_.id)(Ord.desc))
case _ =>
val r= pageWithCount(
query[Network].filter(_.status == lift(NetworkStatus.Normal))
val r = pageWithCount(
query[Network].filter(_.status == lift(NetworkStatus.Normal)).filter(_.groupId == lift(groupId))
)(_.sortBy(_.id)(Ord.desc))
import zio.json.*
val j = r.toJson
Expand Down Expand Up @@ -132,7 +131,7 @@ trait NetworkController(
)
}
}
nodeChangeNotifyService.networkSettingChange(oldNetwork, networkDao.findById(id).get)
nodeChangeNotifyService.networkSettingChange(oldNetwork, networkDao.findById(id).get)
case _ =>
}
Accepted()
Expand Down
36 changes: 22 additions & 14 deletions backend/src/main/scala/com/timzaak/fornet/di/DI.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.timzaak.fornet.di

import com.timzaak.fornet.config.{AppConfig, AppConfigImpl}
import com.timzaak.fornet.config.{ AppConfig, AppConfigImpl }
import com.timzaak.fornet.controller.*
import com.timzaak.fornet.grpc.AuthGRPCController
import com.timzaak.fornet.keycloak.{ KeycloakJWTSaaSAuthStrategy, KeycloakJWTSaaSCompatAuthStrategy }
import com.timzaak.fornet.mqtt.MqttCallbackController
import com.timzaak.fornet.mqtt.api.RMqttApiClient
import com.timzaak.fornet.pubsub.{MqttConnectionManager, NodeChangeNotifyService}
import com.timzaak.fornet.pubsub.{ MqttConnectionManager, NodeChangeNotifyService }
import com.timzaak.fornet.service.*
import very.util.keycloak.{JWKPublicKeyLocator, JWKTokenVerifier, KeycloakJWTAuthStrategy}
import very.util.web.auth.{AuthStrategy, AuthStrategyProvider, SingleUserAuthStrategy}
import very.util.keycloak.{ JWKPublicKeyLocator, JWKTokenVerifier }
import very.util.web.auth.{ AuthStrategy, AuthStrategyProvider, SingleUserAuthStrategy }
object DI extends DaoDI { di =>

object appConfig extends AppConfigImpl(config)
Expand Down Expand Up @@ -43,17 +44,24 @@ object DI extends DaoDI { di =>
// init keycloak,( keycloak server must start, this would get information from keycloak server)
val keycloakUrl = config.get[String]("auth.keycloak.authServerUrl")
val realm = config.get[String]("auth.keycloak.realm")
val publicKeyLocator = JWKPublicKeyLocator.init(
keycloakUrl,
realm,
)
List(
KeycloakJWTAuthStrategy(
JWKTokenVerifier(publicKeyLocator.get, keycloakUrl, realm),
config.getOptional[String]("auth.keycloak.adminRole"),
config.getOptional[String]("auth.keycloak.clientRole"),
val publicKeyLocator = JWKPublicKeyLocator
.init(
keycloakUrl,
realm,
)
)
.get
val verifier = JWKTokenVerifier(publicKeyLocator, keycloakUrl, realm)
if (appConfig.enableSAAS) {
List(KeycloakJWTSaaSAuthStrategy(verifier))
} else {
List(
KeycloakJWTSaaSCompatAuthStrategy(
verifier,
config.getOptional[String]("auth.keycloak.adminRole"),
config.getOptional[String]("auth.keycloak.clientRole"),
)
)
}
} else {
List(
SingleUserAuthStrategy(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.timzaak.fornet.keycloak

import org.scalatra.auth.ScentrySupport
import org.scalatra.auth.strategy.BasicAuthSupport
import com.typesafe.scalalogging.LazyLogging
import org.keycloak.TokenVerifier
import com.typesafe.scalalogging.Logger
import very.util.keycloak.{ JWKTokenVerifier, KeycloakJWTAuthStrategy }
import very.util.web.auth.AuthStrategy

import scala.util.{ Failure, Success }

class KeycloakJWTSaaSAuthStrategy(
jwkTokenVerifier: JWKTokenVerifier,
) extends AuthStrategy[String]
with LazyLogging {

def name: String = KeycloakJWTAuthStrategy.name

def adminAuth(token: String): Option[String] = {
jwkTokenVerifier.verify(token) match {
case Success(accessToken) =>
Some(accessToken.getSubject)
case Failure(exception) =>
logger.debug(s"bad token:$token", exception)
None
}
}

// SaaS do not support Client SSO Login
def clientAuth(token: String): Option[String] = {
None
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.timzaak.fornet.keycloak

import org.scalatra.auth.ScentrySupport
import org.scalatra.auth.strategy.BasicAuthSupport
import com.typesafe.scalalogging.LazyLogging
import org.keycloak.TokenVerifier
import com.typesafe.scalalogging.Logger
import very.util.keycloak.{ JWKTokenVerifier, KeycloakJWTAuthStrategy }
import very.util.web.auth.AuthStrategy

import scala.util.{ Failure, Success }

class KeycloakJWTSaaSCompatAuthStrategy(
jwkTokenVerifier: JWKTokenVerifier,
adminRole: Option[String],
clientRole: Option[String]
) extends AuthStrategy[String]
with LazyLogging {
// JWT
def name: String = KeycloakJWTAuthStrategy.name

def adminAuth(token: String): Option[String] = {
jwkTokenVerifier.verify(token) match {
case Success(accessToken) =>
if (adminRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) {
Some(adminRole.getOrElse("admin"))
} else {
logger.info(
s"the user:${accessToken.getSubject} could not pass admin auth"
)
None
}
case Failure(exception) =>
logger.debug(s"bad token:$token", exception)
None
}
}

def clientAuth(token: String): Option[String] = {
jwkTokenVerifier.verify(token) match {
case Success(accessToken) =>
if (clientRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) {
Some(accessToken.getSubject)
} else {
logger.info(
s"the user:${accessToken.getSubject} could not pass client auth"
)
None
}
case Failure(exception) =>
logger.debug(s"bad token:$token", exception)
None
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@ import very.util.web.auth.AuthStrategy

import scala.util.{ Success, Failure }

class KeycloakJWTAuthStrategy(jwkTokenVerifier: JWKTokenVerifier, adminRole: Option[String], clientRole:Option[String])
extends AuthStrategy[String] {
def logger: Logger = com.typesafe.scalalogging.Logger(getClass.getName)

class KeycloakJWTAuthStrategy(jwkTokenVerifier: JWKTokenVerifier, adminRole: Option[String], clientRole: Option[String])
extends AuthStrategy[String] with LazyLogging {
// JWT
def name: String = KeycloakJWTAuthStrategy.name

def adminAuth(token: String): Option[String] = {
jwkTokenVerifier.verify(token) match {
case Success(accessToken) =>
if (adminRole.isEmpty || accessToken.getRealmAccess.getRoles.contains(adminRole.get)) {
if (adminRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) {
Some(accessToken.getSubject)
} else {
logger.info(
Expand All @@ -33,10 +31,10 @@ class KeycloakJWTAuthStrategy(jwkTokenVerifier: JWKTokenVerifier, adminRole: Opt
}
}

def clientAuth(token:String):Option[String] = {
def clientAuth(token: String): Option[String] = {
jwkTokenVerifier.verify(token) match {
case Success(accessToken) =>
if (clientRole.isEmpty||accessToken.getRealmAccess.getRoles.contains(clientRole.get)) {
if (clientRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) {
Some(accessToken.getSubject)
} else {
logger.info(
Expand All @@ -53,5 +51,5 @@ class KeycloakJWTAuthStrategy(jwkTokenVerifier: JWKTokenVerifier, adminRole: Opt
}

object KeycloakJWTAuthStrategy {
val name:String = "Bearer"
val name: String = "Bearer"
}