Skip to content

Commit

Permalink
chore/keycloak-config-auto-discovery (#274)
Browse files Browse the repository at this point in the history
  • Loading branch information
zingmane authored Oct 20, 2023
1 parent ec75c45 commit 0d75f59
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 29 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,25 @@ gradlew -v

### Setup

At first you need to setup your database and create a new `conf.json` based on `conf-example.json`.
At first you need to setup your database and create a new `conf.json` based on `./conf-example.json`.
After that you can need to call `POST /system/reset` once to initialize system tables. If you wish you can fill in the demo data with `POST /system/resetDemo`.

### Update DB schema (optionally)

If you upgrade from an older schema version you need to call `POST /system/update` before that. Schema will be upgraded automatically.

## Auth

There are three different auth modes:

- 1. no auth (legacy)
- 2. manual auth with bearer token validation (JWT)
- 3. automatic keycloak auth discovery (JWT) - preferred

Auth modes 2. and 3. of Tableaux are secured by a JWT based authentication. The JWT (signed with a private key) is verified by the public key of the auth service. In manual auth mode 2. the public key is configured in the conf file (see `./conf-example.json`), in automatic auth mode 3. the public key is discovered via the auth service also configured in the conf file (see `./conf-example-auto-discovery.json`)

The auth mode 1. is a legacy mode for testing or for running the service behind a different auth service. In this mode the incoming request is not verified. The user (e.g. for history entries) must be set via cookie `userName`. Legacy mode is activated, if `auth` key in config is missing.

## Build & Test

Tableaux uses gradle to build a so called **fat jar** which contains all runtime dependencies. You can find it in `build/libs/tableaux-fat.jar`. The gradle task `build` needs a running PostgreSQL and the `conf-test.json` must be configured correct. Requests in auth tests must contain an accessToken. For simplicity this accessToken is generated within a test helper with a hardcoded key pair. For the accessToken to match the pub key, the auth configuration for testing must always be the same as configured in `conf-test-example.json`.
Expand Down
103 changes: 75 additions & 28 deletions src/main/scala/com/campudus/tableaux/router/RouterRegistry.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.campudus.tableaux.router.auth.permission.RoleModel

import io.vertx.lang.scala.VertxExecutionContext
import io.vertx.scala.core.Vertx
import io.vertx.scala.ext.auth.oauth2.OAuth2ClientOptions
import io.vertx.scala.ext.auth.oauth2.providers.KeycloakAuth
import io.vertx.scala.ext.web.{Router, RoutingContext}
import io.vertx.scala.ext.web.handler.CookieHandler
Expand All @@ -24,30 +25,13 @@ object RouterRegistry extends LazyLogging {

val vertx: Vertx = tableauxConfig.vertx

val isAuthorization: Boolean = !tableauxConfig.authConfig.isEmpty
val isAuth: Boolean = !tableauxConfig.authConfig.isEmpty
val isAutoDiscovery: Boolean = tableauxConfig.authConfig.getBoolean("isAutoDiscovery", false)

implicit val roleModel: RoleModel = RoleModel(tableauxConfig.rolePermissions, isAuthorization)
implicit val roleModel: RoleModel = RoleModel(tableauxConfig.rolePermissions, isAuth)

val mainRouter: Router = Router.router(vertx)

// needed cookies will be extracted by route handlers and stored in `TableauxUser` instances
mainRouter.route().handler(CookieHandler.create())

if (isAuthorization) {
val keycloakAuthProvider = KeycloakAuth.create(vertx, tableauxConfig.authConfig)
val keycloakAuthHandler = OAuth2AuthHandler.create(keycloakAuthProvider)
mainRouter.route().handler(keycloakAuthHandler)

val tableauxKeycloakAuthHandler = new KeycloakAuthHandler(vertx, tableauxConfig)
mainRouter.route().handler(tableauxKeycloakAuthHandler)

} else {
logger.warn(
"Started WITHOUT access token verification. The API is completely publicly available and NOT secured! " +
"This is for development and/or testing purposes ONLY."
)
}

val systemModel = SystemModel(dbConnection)
val structureModel = StructureModel(dbConnection)
val tableauxModel = TableauxModel(dbConnection, structureModel)
Expand All @@ -67,16 +51,79 @@ object RouterRegistry extends LazyLogging {
val structureRouter = StructureRouter(tableauxConfig, StructureController(_, structureModel, roleModel))
val documentationRouter = DocumentationRouter(tableauxConfig)

mainRouter.mountSubRouter("/system", systemRouter.route)
mainRouter.mountSubRouter("/", structureRouter.route)
mainRouter.mountSubRouter("/", tableauxRouter.route)
mainRouter.mountSubRouter("/", mediaRouter.route)
mainRouter.mountSubRouter("/docs", documentationRouter.route)
mainRouter.route().handler(CookieHandler.create())

def registerCommonRoutes(router: Router) = {
router.mountSubRouter("/system", systemRouter.route)
router.mountSubRouter("/", structureRouter.route)
router.mountSubRouter("/", tableauxRouter.route)
router.mountSubRouter("/", mediaRouter.route)
router.mountSubRouter("/docs", documentationRouter.route)

router.get("/").handler(systemRouter.defaultRoute)
router.get("/index.html").handler(systemRouter.defaultRoute)

router.route().handler(systemRouter.noRouteMatched)
}

def initManualAuth() = {
val keycloakAuthProvider = KeycloakAuth.create(vertx, tableauxConfig.authConfig)
val keycloakAuthHandler = OAuth2AuthHandler.create(keycloakAuthProvider)
mainRouter.route().handler(keycloakAuthHandler)

mainRouter.get("/").handler(systemRouter.defaultRoute)
mainRouter.get("/index.html").handler(systemRouter.defaultRoute)
val tableauxKeycloakAuthHandler = new KeycloakAuthHandler(vertx, tableauxConfig)
mainRouter.route().handler(tableauxKeycloakAuthHandler)

registerCommonRoutes(mainRouter)
}

def initAutoDiscoverAuth() = {
val clientOptions: OAuth2ClientOptions = OAuth2ClientOptions()
.setSite(tableauxConfig.authConfig.getString("issuer"))
.setClientID(tableauxConfig.authConfig.getString("resource"))

val tableauxKeycloakAuthHandler = new KeycloakAuthHandler(vertx, tableauxConfig)

mainRouter.route().handler(systemRouter.noRouteMatched)
KeycloakAuth.discover(
vertx,
clientOptions,
handler => {
if (handler.succeeded()) {
val keycloakAuthProvider = handler.result()
val keycloakAuthHandler = OAuth2AuthHandler.create(keycloakAuthProvider)
mainRouter.route().handler(keycloakAuthHandler)
mainRouter.route().handler(tableauxKeycloakAuthHandler)

registerCommonRoutes(mainRouter)
} else {
logger.error(
"Could not configure Keycloak integration via OpenID Connect " +
"Discovery Endpoint because of: " + handler.cause().getMessage
)
}
}
)
}

if (isAuth) {
if (isAutoDiscovery) {
initAutoDiscoverAuth()
} else {
logger.info(
"Started with manual auth configuration! To use auto discovery " +
"set 'auth.isAutoDiscovery' to true in your config and remove all other " +
"auth.* configuration options but `issuer` and `resource`."
)
initManualAuth()
}

} else {
logger.warn(
"Started WITHOUT access token verification. The API is completely publicly available and NOT secured! " +
"This is for development and/or testing purposes ONLY."
)
registerCommonRoutes(mainRouter)
}

mainRouter
}
Expand Down

0 comments on commit 0d75f59

Please sign in to comment.