Skip to content

Commit 0a7c28e

Browse files
authored
keycloak compatible for SaaS and self-hosted (#14)
fix #3
1 parent a71938e commit 0a7c28e

File tree

8 files changed

+130
-32
lines changed

8 files changed

+130
-32
lines changed

admin-web/src/api/infoAPI.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import http from "./http";
22
import {KeycloakConfig} from "keycloak-js";
33

4-
type AuthTypeResponse = { type: 'ST' } | (KeycloakConfig & { type: 'Bearer' })
4+
type AuthTypeResponse = { type: 'ST' } | (KeycloakConfig & { type: 'Bearer', saas: boolean })
55
export function getAppInfo() {
66
return http.get<AuthTypeResponse>('/info')
77
}

admin-web/src/layout/AppLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default function AppLayout() {
1414
return (
1515
<Layout style={{height: 'calc(100vh)'}}>
1616
<Header style={{textAlign: 'center'}}>
17-
<span style={{fontWeight: 'bold', fontSize: '20px'}}>ForNet Admin</span>
17+
<span style={{fontWeight: 'bold', fontSize: '20px'}}>ForNet Manager (BETA)</span>
1818
</Header>
1919
<Content style={{padding: '0 50px'}}>
2020
<AppBreadcrumb/>

backend/src/main/resources/application.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ auth {
3939
# authServerUrl: "http://keycloak-dev.fornetcode.com",
4040
# frontClientId : "fornet",
4141
# # the user who has admin role can login in admin web, if undefined, anyone in the keycloak of realm can login
42+
# # when server.saas enabled, this is useless
4243
# adminRole: "admin",
4344
# # the user who has client role can login in client, if undefined, anyone in the keycloak of realm can login
45+
# # when server.saas enabled, this is useless
4446
# clientRole: "client",
4547
#}
4648
#simple {

backend/src/main/scala/com/timzaak/fornet/controller/NetworkController.scala

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,26 @@ package com.timzaak.fornet.controller
33
import com.google.common.net.InetAddresses
44
import com.timzaak.fornet.config.AppConfig
55
import com.timzaak.fornet.controller.auth.AppAuthSupport
6-
import com.timzaak.fornet.dao.{DB, Network, NetworkDao, NetworkProtocol, NetworkSetting}
6+
import com.timzaak.fornet.dao.*
77
import com.timzaak.fornet.pubsub.NodeChangeNotifyService
88
import com.typesafe.config.Config
99
import org.hashids.Hashids
1010
import very.util.security.IntID
1111

1212
import java.util.Base64
13-
//import org.json4s.Formats
1413
import com.timzaak.fornet.dao.NetworkStatus
1514
import io.getquill.*
15+
import org.scalatra.*
1616
import org.scalatra.i18n.I18nSupport
1717
import org.scalatra.json.*
18-
import org.scalatra.*
18+
import very.util.security.IntID.toIntID
1919
import very.util.web.Controller
2020
import very.util.web.validate.ValidationExtra
21-
import very.util.security.IntID.toIntID
2221
import zio.json.{ DeriveJsonDecoder, JsonDecoder }
2322

2423
import java.time.OffsetDateTime
2524

26-
case class CreateNetworkReq(name: String, addressRange: String, protocol:NetworkProtocol)
25+
case class CreateNetworkReq(name: String, addressRange: String, protocol: NetworkProtocol)
2726
given JsonDecoder[CreateNetworkReq] = DeriveJsonDecoder.gen
2827
case class UpdateNetworkReq(
2928
name: String,
@@ -55,8 +54,8 @@ trait NetworkController(
5554
.filter(_.groupId == lift(groupId))
5655
)(_.sortBy(_.id)(Ord.desc))
5756
case _ =>
58-
val r= pageWithCount(
59-
query[Network].filter(_.status == lift(NetworkStatus.Normal))
57+
val r = pageWithCount(
58+
query[Network].filter(_.status == lift(NetworkStatus.Normal)).filter(_.groupId == lift(groupId))
6059
)(_.sortBy(_.id)(Ord.desc))
6160
import zio.json.*
6261
val j = r.toJson
@@ -132,7 +131,7 @@ trait NetworkController(
132131
)
133132
}
134133
}
135-
nodeChangeNotifyService.networkSettingChange(oldNetwork, networkDao.findById(id).get)
134+
nodeChangeNotifyService.networkSettingChange(oldNetwork, networkDao.findById(id).get)
136135
case _ =>
137136
}
138137
Accepted()

backend/src/main/scala/com/timzaak/fornet/di/DI.scala

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package com.timzaak.fornet.di
22

3-
import com.timzaak.fornet.config.{AppConfig, AppConfigImpl}
3+
import com.timzaak.fornet.config.{ AppConfig, AppConfigImpl }
44
import com.timzaak.fornet.controller.*
55
import com.timzaak.fornet.grpc.AuthGRPCController
6+
import com.timzaak.fornet.keycloak.{ KeycloakJWTSaaSAuthStrategy, KeycloakJWTSaaSCompatAuthStrategy }
67
import com.timzaak.fornet.mqtt.MqttCallbackController
78
import com.timzaak.fornet.mqtt.api.RMqttApiClient
8-
import com.timzaak.fornet.pubsub.{MqttConnectionManager, NodeChangeNotifyService}
9+
import com.timzaak.fornet.pubsub.{ MqttConnectionManager, NodeChangeNotifyService }
910
import com.timzaak.fornet.service.*
10-
import very.util.keycloak.{JWKPublicKeyLocator, JWKTokenVerifier, KeycloakJWTAuthStrategy}
11-
import very.util.web.auth.{AuthStrategy, AuthStrategyProvider, SingleUserAuthStrategy}
11+
import very.util.keycloak.{ JWKPublicKeyLocator, JWKTokenVerifier }
12+
import very.util.web.auth.{ AuthStrategy, AuthStrategyProvider, SingleUserAuthStrategy }
1213
object DI extends DaoDI { di =>
1314

1415
object appConfig extends AppConfigImpl(config)
@@ -43,17 +44,24 @@ object DI extends DaoDI { di =>
4344
// init keycloak,( keycloak server must start, this would get information from keycloak server)
4445
val keycloakUrl = config.get[String]("auth.keycloak.authServerUrl")
4546
val realm = config.get[String]("auth.keycloak.realm")
46-
val publicKeyLocator = JWKPublicKeyLocator.init(
47-
keycloakUrl,
48-
realm,
49-
)
50-
List(
51-
KeycloakJWTAuthStrategy(
52-
JWKTokenVerifier(publicKeyLocator.get, keycloakUrl, realm),
53-
config.getOptional[String]("auth.keycloak.adminRole"),
54-
config.getOptional[String]("auth.keycloak.clientRole"),
47+
val publicKeyLocator = JWKPublicKeyLocator
48+
.init(
49+
keycloakUrl,
50+
realm,
5551
)
56-
)
52+
.get
53+
val verifier = JWKTokenVerifier(publicKeyLocator, keycloakUrl, realm)
54+
if (appConfig.enableSAAS) {
55+
List(KeycloakJWTSaaSAuthStrategy(verifier))
56+
} else {
57+
List(
58+
KeycloakJWTSaaSCompatAuthStrategy(
59+
verifier,
60+
config.getOptional[String]("auth.keycloak.adminRole"),
61+
config.getOptional[String]("auth.keycloak.clientRole"),
62+
)
63+
)
64+
}
5765
} else {
5866
List(
5967
SingleUserAuthStrategy(
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.timzaak.fornet.keycloak
2+
3+
import org.scalatra.auth.ScentrySupport
4+
import org.scalatra.auth.strategy.BasicAuthSupport
5+
import com.typesafe.scalalogging.LazyLogging
6+
import org.keycloak.TokenVerifier
7+
import com.typesafe.scalalogging.Logger
8+
import very.util.keycloak.{ JWKTokenVerifier, KeycloakJWTAuthStrategy }
9+
import very.util.web.auth.AuthStrategy
10+
11+
import scala.util.{ Failure, Success }
12+
13+
class KeycloakJWTSaaSAuthStrategy(
14+
jwkTokenVerifier: JWKTokenVerifier,
15+
) extends AuthStrategy[String]
16+
with LazyLogging {
17+
18+
def name: String = KeycloakJWTAuthStrategy.name
19+
20+
def adminAuth(token: String): Option[String] = {
21+
jwkTokenVerifier.verify(token) match {
22+
case Success(accessToken) =>
23+
Some(accessToken.getSubject)
24+
case Failure(exception) =>
25+
logger.debug(s"bad token:$token", exception)
26+
None
27+
}
28+
}
29+
30+
// SaaS do not support Client SSO Login
31+
def clientAuth(token: String): Option[String] = {
32+
None
33+
}
34+
35+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.timzaak.fornet.keycloak
2+
3+
import org.scalatra.auth.ScentrySupport
4+
import org.scalatra.auth.strategy.BasicAuthSupport
5+
import com.typesafe.scalalogging.LazyLogging
6+
import org.keycloak.TokenVerifier
7+
import com.typesafe.scalalogging.Logger
8+
import very.util.keycloak.{ JWKTokenVerifier, KeycloakJWTAuthStrategy }
9+
import very.util.web.auth.AuthStrategy
10+
11+
import scala.util.{ Failure, Success }
12+
13+
class KeycloakJWTSaaSCompatAuthStrategy(
14+
jwkTokenVerifier: JWKTokenVerifier,
15+
adminRole: Option[String],
16+
clientRole: Option[String]
17+
) extends AuthStrategy[String]
18+
with LazyLogging {
19+
// JWT
20+
def name: String = KeycloakJWTAuthStrategy.name
21+
22+
def adminAuth(token: String): Option[String] = {
23+
jwkTokenVerifier.verify(token) match {
24+
case Success(accessToken) =>
25+
if (adminRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) {
26+
Some(adminRole.getOrElse("admin"))
27+
} else {
28+
logger.info(
29+
s"the user:${accessToken.getSubject} could not pass admin auth"
30+
)
31+
None
32+
}
33+
case Failure(exception) =>
34+
logger.debug(s"bad token:$token", exception)
35+
None
36+
}
37+
}
38+
39+
def clientAuth(token: String): Option[String] = {
40+
jwkTokenVerifier.verify(token) match {
41+
case Success(accessToken) =>
42+
if (clientRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) {
43+
Some(accessToken.getSubject)
44+
} else {
45+
logger.info(
46+
s"the user:${accessToken.getSubject} could not pass client auth"
47+
)
48+
None
49+
}
50+
case Failure(exception) =>
51+
logger.debug(s"bad token:$token", exception)
52+
None
53+
}
54+
}
55+
56+
}

backend/src/main/scala/very/util/keycloak/KeycloakJWTAuthStrategy.scala

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,15 @@ import very.util.web.auth.AuthStrategy
99

1010
import scala.util.{ Success, Failure }
1111

12-
class KeycloakJWTAuthStrategy(jwkTokenVerifier: JWKTokenVerifier, adminRole: Option[String], clientRole:Option[String])
13-
extends AuthStrategy[String] {
14-
def logger: Logger = com.typesafe.scalalogging.Logger(getClass.getName)
15-
12+
class KeycloakJWTAuthStrategy(jwkTokenVerifier: JWKTokenVerifier, adminRole: Option[String], clientRole: Option[String])
13+
extends AuthStrategy[String] with LazyLogging {
1614
// JWT
1715
def name: String = KeycloakJWTAuthStrategy.name
1816

1917
def adminAuth(token: String): Option[String] = {
2018
jwkTokenVerifier.verify(token) match {
2119
case Success(accessToken) =>
22-
if (adminRole.isEmpty || accessToken.getRealmAccess.getRoles.contains(adminRole.get)) {
20+
if (adminRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) {
2321
Some(accessToken.getSubject)
2422
} else {
2523
logger.info(
@@ -33,10 +31,10 @@ class KeycloakJWTAuthStrategy(jwkTokenVerifier: JWKTokenVerifier, adminRole: Opt
3331
}
3432
}
3533

36-
def clientAuth(token:String):Option[String] = {
34+
def clientAuth(token: String): Option[String] = {
3735
jwkTokenVerifier.verify(token) match {
3836
case Success(accessToken) =>
39-
if (clientRole.isEmpty||accessToken.getRealmAccess.getRoles.contains(clientRole.get)) {
37+
if (clientRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) {
4038
Some(accessToken.getSubject)
4139
} else {
4240
logger.info(
@@ -53,5 +51,5 @@ class KeycloakJWTAuthStrategy(jwkTokenVerifier: JWKTokenVerifier, adminRole: Opt
5351
}
5452

5553
object KeycloakJWTAuthStrategy {
56-
val name:String = "Bearer"
54+
val name: String = "Bearer"
5755
}

0 commit comments

Comments
 (0)