Skip to content

Commit

Permalink
Hawtio and Keycloak: Upgrade to Keycloak 2.2.0.Final
Browse files Browse the repository at this point in the history
  • Loading branch information
mposolda committed Sep 16, 2016
1 parent 834e69a commit 6c1ba79
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 157 deletions.
Expand Up @@ -22,7 +22,7 @@ public class RedirectFilter implements Filter {

private static final transient Logger LOG = LoggerFactory.getLogger(RedirectFilter.class);

private static final String knownServlets[] = {"jolokia", "auth", "upload", "javadoc", "proxy", "springBatch", "user", "plugin", "exportContext", "contextFormatter", "refresh"};
private static final String knownServlets[] = {"jolokia", "auth", "upload", "javadoc", "proxy", "springBatch", "user", "plugin", "exportContext", "contextFormatter", "refresh", "keycloak" };

private ServletContext context;

Expand Down
2 changes: 1 addition & 1 deletion hawtio-web/bower.json
Expand Up @@ -28,6 +28,6 @@
"Font-Awesome": "3.2.1",
"elastic.js": "1.1.1",
"underscore": "1.7.0",
"keycloak": "1.9.0"
"keycloak": "2.2.0"
}
}
4 changes: 4 additions & 0 deletions hawtio-web/src/main/webapp/app/core/js/coreInterfaces.ts
Expand Up @@ -22,6 +22,10 @@ module Core {
keycloak: KeycloakModule.IKeycloak;
}

export interface KeycloakPostLoginTasks {
bootstrapIfNeeded: Function;
}

/**
* Typescript interface that represents the options needed to connect to another JVM
*/
Expand Down
3 changes: 3 additions & 0 deletions hawtio-web/src/main/webapp/app/core/js/corePlugin.ts
Expand Up @@ -91,6 +91,7 @@ module Core {
"$location",
"ConnectOptions",
"locationChangeStartTasks",
"keycloakPostLoginTasks",
"$http",
"$route",
($rootScope,
Expand All @@ -112,6 +113,7 @@ module Core {
$location:ng.ILocationService,
ConnectOptions:Core.ConnectOptions,
locationChangeStartTasks:Core.ParameterizedTasks,
keycloakPostLoginTasks: KeycloakPostLoginTasks,
$http:ng.IHttpService,
$route) => {

Expand All @@ -125,6 +127,7 @@ module Core {
checkInjectorLoaded();
postLogoutTasks.reset();
});
keycloakPostLoginTasks.bootstrapIfNeeded();

preLogoutTasks.addTask("ResetPostLoginTasks", () => {
checkInjectorLoaded();
Expand Down
184 changes: 83 additions & 101 deletions hawtio-web/src/main/webapp/app/core/js/keycloakLogin.ts
@@ -1,7 +1,4 @@
/**
* @module Core
*/
/// <reference path="corePlugin.ts"/>
/// <reference path="./coreHelpers.ts"/>
module Core {

var log = Logger.get('Keycloak');
Expand Down Expand Up @@ -69,9 +66,9 @@ module Core {
log.debug("Keycloak authenticated with Subject " + keycloakUsername + ". Validating subject matches");

validateSubjectMatches(keycloakUsername, function() {
log.debug("Keycloak authentication finished! Continue next task");
log.debug("validateSubjectMatches finished! Continue next task");
// Continue next registered task and bootstrap Angular
nextTask();
keycloakJaasLogin(keycloak, nextTask);
});
}).error(function () {
log.warn("Keycloak authentication failed!");
Expand Down Expand Up @@ -106,103 +103,17 @@ module Core {
});
}

/**
* Prebootstrap task, which handles Keycloak OAuth flow. It will first check if keycloak is enabled and then possibly init keycloak.
* It will continue with Angular bootstrap just when Keycloak authentication is successfully finished
*/
hawtioPluginLoader.registerPreBootstrapTask(function (nextTask) {
log.debug('Prebootstrap task executed');

checkKeycloakEnabled(function(keycloakContext) {
initKeycloakIfNeeded(keycloakContext, nextTask);
});
});

// This is used to track if we already processed loginController for this window. Because hawtio may redirect to "/login" more times and we don't want to trigger controller every time this happens
var loginControllerProcessed: boolean = false;

/**
* Method is called from LoginController when '/login' URL is opened and we have keycloak integration enabled.
* It registers needed logout tasks and send request for JAAS login with keycloak authToken attached as password
*/
export var keycloakLoginController = function($scope, jolokia, userDetails:Core.UserDetails, jolokiaUrl, workspace, localStorage, keycloakContext: KeycloakContext, postLogoutTasks) {
if (loginControllerProcessed) {
log.debug('Skip processing login controller as it was already processed this request!');
return;
}

// Now switch to true and allow controller to be processed again after 30 seconds
loginControllerProcessed = true;
setTimeout(function() {
loginControllerProcessed = false;
}, 30000);

log.debug("keycloakLoginController triggered");
var keycloakAuth: KeycloakModule.IKeycloak = keycloakContext.keycloak;

// Handle logout triggered from hawtio. Maybe not best to add it here but should work as tasks are tracked by name
postLogoutTasks.addTask('KeycloakLogout', function () {
if (keycloakAuth.authenticated) {
log.debug("postLogoutTask: Going to trigger keycloak logout");
keycloakAuth.logout();

// We redirected to keycloak logout. Skip execution of onComplete callback
return false;
} else {
log.debug("postLogoutTask: Keycloak not authenticated. Skip calling keycloak logout");
return true;
}
});

// Detect keycloak logout based on iframe. We need to trigger hawtio logout too to ensure single-sign-out
keycloakAuth.onAuthLogout = function() {
log.debug('keycloakAuth.onAuthLogout triggered!');
Core.logout(jolokiaUrl, userDetails, localStorage, $scope);
};

// Handle periodic refreshing of keycloak token. Token validity is checked each 5 seconds and token is refreshed if it is going to expire
// Periodic refreshment is stopped once we detect that we are not logged anymore to keycloak
var setPeriodicTokenRefresh = function() {
if (keycloakAuth.authenticated) {
setTimeout(function() {
keycloakAuth.updateToken(10).success(function(refreshed) {
if (refreshed) {
log.debug('Keycloak token refreshed. Set new value to userDetails');
userDetails.password = keycloakAuth.token;
}
}).error(function() {
log.warn('Failed to refresh keycloak token!');
});

// Setup timeout again, so it is checked again next 5 seconds
setPeriodicTokenRefresh();
}, 5000);
} else {
log.debug('Keycloak not authenticated any more. Skip period for token refreshing');
}
}

// triggers JAAS request with keycloak accessToken as password. This will finish hawtio authentication
var doKeycloakJaasLogin = function() {
if (jolokiaUrl) {
var keycloakJaasLogin = function(keycloak: KeycloakModule.IKeycloak, callback: Function) {
var url = "auth/login/";

if (keycloakAuth.token && keycloakAuth.token != '') {
if (keycloak.token && keycloak.token != '') {
log.debug('Keycloak authentication token found! Going to trigger JAAS');
$.ajax(url, {
$.ajax(url, <JQueryAjaxSettings> {
type: "POST",
success: (response) => {
log.debug('Callback from JAAS login!');
userDetails.username = keycloakAuth.tokenParsed.preferred_username;
userDetails.password = keycloakAuth.token;
userDetails.loginDetails = response;

setPeriodicTokenRefresh();

jolokia.start();
workspace.loadTree();
Core.executePostLoginTasks();
Core.$apply($scope);
log.debug("Got response for keycloakJaasLogin: ", response);
callback();
},
error: (xhr, textStatus, error) => {
switch (xhr.status) {
Expand All @@ -216,19 +127,90 @@ module Core {
notification('error', 'Failed to log in, ' + error);
break;
}
Core.$apply($scope);
},
beforeSend: (xhr) => {
xhr.setRequestHeader('Authorization', Core.getBasicAuthHeader(keycloakAuth.tokenParsed.preferred_username, keycloakAuth.token));
xhr.setRequestHeader('Authorization', Core.getBasicAuthHeader(keycloak.tokenParsed.preferred_username, keycloak.token));
}
});
} else {
notification('error', 'Keycloak auth token not found.');
}

};

/**
* Prebootstrap task, which handles Keycloak OAuth flow. It will first check if keycloak is enabled and then possibly init keycloak.
* It will continue with Angular bootstrap just when Keycloak authentication is successfully finished
*/
hawtioPluginLoader.registerPreBootstrapTask(function (nextTask) {
log.debug('Prebootstrap task executed');

checkKeycloakEnabled(function(keycloakContext) {
initKeycloakIfNeeded(keycloakContext, nextTask);
});
});


/**
* Method is called from corePlugins. This is at the stage where Keycloak authentication is always finished.
*/
_module.factory('keycloakPostLoginTasks', ["$rootScope", "userDetails", "jolokiaUrl", "localStorage", "keycloakContext", "postLogoutTasks", ($rootScope, userDetails:Core.UserDetails, jolokiaUrl, localStorage, keycloakContext: KeycloakContext, postLogoutTasks) => {

var bootstrapIfNeeded1 = function() {
if (keycloakContext.enabled) {
log.debug("keycloakPostLoginTasks triggered");
var keycloakAuth: KeycloakModule.IKeycloak = keycloakContext.keycloak;

// Handle logout triggered from hawtio.
postLogoutTasks.addTask('KeycloakLogout', function () {
if (keycloakAuth.authenticated) {
log.debug("postLogoutTask: Going to trigger keycloak logout");
keycloakAuth.logout();

// We redirected to keycloak logout. Skip execution of onComplete callback
return false;
} else {
log.debug("postLogoutTask: Keycloak not authenticated. Skip calling keycloak logout");
return true;
}
});

// Detect keycloak logout based on iframe. We need to trigger hawtio logout too to ensure single-sign-out
keycloakAuth.onAuthLogout = function() {
log.debug('keycloakAuth.onAuthLogout triggered!');
Core.logout(jolokiaUrl, userDetails, localStorage, $rootScope);
};

// Handle periodic refreshing of keycloak token. Token validity is checked each 5 seconds and token is refreshed if it is going to expire
// Periodic refreshment is stopped once we detect that we are not logged anymore to keycloak
var setPeriodicTokenRefresh = function() {
if (keycloakAuth.authenticated) {
setTimeout(function() {
keycloakAuth.updateToken(10).success(function(refreshed) {
if (refreshed) {
log.debug('Keycloak token refreshed. Set new value to userDetails');
userDetails.password = keycloakAuth.token;
}
}).error(function() {
log.warn('Failed to refresh keycloak token!');
Core.logout(jolokiaUrl, userDetails, localStorage, $rootScope);
});

// Setup timeout again, so it is checked again next 5 seconds
setPeriodicTokenRefresh();
}, 5000);
} else {
log.debug('Keycloak not authenticated any more. Skip period for token refreshing');
}
}
setPeriodicTokenRefresh();
}
};
var answer = <KeycloakPostLoginTasks> {
bootstrapIfNeeded: bootstrapIfNeeded1
};

doKeycloakJaasLogin();
};
return answer;
}]);

}
4 changes: 1 addition & 3 deletions hawtio-web/src/main/webapp/app/core/js/login.ts
Expand Up @@ -24,9 +24,7 @@ module Core {

$scope.keycloakEnabled = keycloakContext.enabled;

if ($scope.keycloakEnabled) {
keycloakLoginController($scope, jolokia, userDetails, jolokiaUrl, workspace, localStorage, keycloakContext, postLogoutTasks);
} else {
if (!$scope.keycloakEnabled) {
loginController($scope, jolokia, jolokiaStatus, userDetails, jolokiaUrl, workspace, localStorage, branding, postLoginTasks);
}
}]);
Expand Down
31 changes: 7 additions & 24 deletions sample-keycloak-integration/README.md
Expand Up @@ -10,13 +10,13 @@ Prepare Keycloak server

**1)** Download file [demorealm.json](demorealm.json) with Keycloak sample metadata about `hawtio-demo` realm. It's assumed you downloaded it to directory `/downloads` on your laptop.

**2)** Download keycloak appliance with wildfly from [http://downloads.jboss.org/keycloak/1.9.1.Final/keycloak-1.9.1.Final.zip](http://downloads.jboss.org/keycloak/1.9.1.Final/keycloak-1.9.1.Final.zip) .
**2)** Download keycloak server from [http://www.keycloak.org](http://www.keycloak.org) and download version 2.2.0.Final .
Then unpack and run keycloak server on localhost:8081 . You also need to import downloaded `demorealm.json` file into your Keycloak. Import can be done either via Keycloak admin console or by
using `keycloak.import` system property:

```
unzip -q /downloads/keycloak-1.9.1.Final.zip
cd keycloak-1.9.1.Final/bin/
unzip -q /downloads/keycloak-2.2.0.Final.zip
cd keycloak-2.2.0.Final/bin/
./standalone.sh -Djboss.http.port=8081 -Dkeycloak.import=/downloads/demorealm.json
```

Expand All @@ -35,7 +35,7 @@ There are also 3 users:
Hawtio and Keycloak integration on JBoss Fuse or Karaf
------------------------------------------------------

This was tested with JBoss Fuse 6.1.0-redhat379 and Apache Karaf 2.4 . Steps are almost same on both. Assuming `$FUSE_HOME` is the root directory of your fuse/karaf
This was tested with JBoss Fuse jboss-fuse-6.3.0.redhat-067 and Apache Karaf 2.4 . Steps are almost same on both. Assuming `$FUSE_HOME` is the root directory of your fuse/karaf

* Add this into the end of file `$FUSE_HOME/etc/system.properties` :

Expand Down Expand Up @@ -63,7 +63,7 @@ cd $FUSE_HOME/bin

Replace with `./karaf` if you are on plain Apache Karaf

* If you are on JBoss Fuse 6.1, you need to first uninstall old hawtio (This step is not needed on plain Apache karaf as it hasn't hawtio installed by default).
* If you are on JBoss Fuse 6.3, you need to first uninstall old hawtio (This step is not needed on plain Apache karaf as it hasn't hawtio installed by default).
So in opened karaf terminal do this:

```
Expand All @@ -77,37 +77,20 @@ features:removeurl mvn:io.hawt/hawtio-karaf/1.2-redhat-379/xml/features
* Install new hawtio with keycloak integration (Replace with the correct version where is Keycloak integration available. It should be 1.4.47 or newer)

```
features:chooseurl hawtio 1.4.47
features:chooseurl hawtio 1.4.66
features:install hawtio
```

* Install keycloak OSGI bundling into Fuse/Karaf . It contains few jars with Keycloak adapter and also configuration of `keycloak` JAAS realm

```
features:addurl mvn:org.keycloak/keycloak-osgi-features/1.9.0.Final/xml/features
features:addurl mvn:org.keycloak/keycloak-osgi-features/2.2.0.Final/xml/features
features:install keycloak-jaas
```

* Go to [http://localhost:8181/hawtio](http://localhost:8181/hawtio) and login in keycloak as `root` or `john` to see hawtio admin console.
If you login as `mary`, you should receive 'forbidden' error in hawtio

#### Additional step on Karaf 2.4

From Karaf 2.4 there is more fine-grained security for JMX. Since Keycloak integration is currently using custom principal class `org.keycloak.adapters.jaas.RolePrincipal`
there is a need to add prefix with this class to the `etc/jmx.acl.*.cfg` files . Otherwise users root and john, who are logged via Keycloak, will be able to login
to Hawtio, but they won't have permission to do much here.

This is likely going to be improved in the future, however currently
you may need to edit this in file `$KARAF_HOME/etc/jmx.acl.cfg` (and maybe also other `jmx.acl.*.cfg` files according to permission you want):

```
list* = org.keycloak.adapters.jaas.RolePrincipal:viewer
get* = org.keycloak.adapters.jaas.RolePrincipal:viewer
is* = org.keycloak.adapters.jaas.RolePrincipal:viewer
set* = org.keycloak.adapters.jaas.RolePrincipal:admin
* = org.keycloak.adapters.jaas.RolePrincipal:admin
```


Hawtio and Keycloak integration on Jetty
----------------------------------------
Expand Down

0 comments on commit 6c1ba79

Please sign in to comment.