Skip to content

Commit

Permalink
keycloak compatible for SaaS and self-hosted (#14)
Browse files Browse the repository at this point in the history
fix #3
  • Loading branch information
timzaak authored Jun 15, 2023
1 parent a71938e commit 0a7c28e
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 32 deletions.
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"
}

0 comments on commit 0a7c28e

Please sign in to comment.