You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/07/19 08:51:10 UTC

[isis] 01/02: ISIS-2793: rewrites keycloak, draft documentation

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-2803-keycloak-rewrite
in repository https://gitbox.apache.org/repos/asf/isis.git

commit 5e72ac4d94b872a480a9e76c4384bfeab8ade326
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Jul 17 16:45:01 2021 +0100

    ISIS-2793: rewrites keycloak, draft documentation
    
    this also has a commented out LoginSuccessHandlerUNUSED that is unused and can probably be removed
---
 .../apache/isis/core/config/IsisConfiguration.java |  45 ++++-
 .../login/LoginSuccessHandlerUNUSED.java           |  36 ++--
 mavendeps/webapp/pom.xml                           |   9 +-
 security/keycloak/pom.xml                          |  34 +++-
 .../adoc/modules/keycloak/images/account-mgmt.png  | Bin 0 -> 324215 bytes
 .../modules/keycloak/images/add-realm-prompt.png   | Bin 0 -> 51244 bytes
 .../images/add-sven-to-regular-user-role.png       | Bin 0 -> 181297 bytes
 .../keycloak/images/add-sven-user-prompt.png       | Bin 0 -> 127568 bytes
 .../modules/keycloak/images/client-app-config.png  | Bin 0 -> 192250 bytes
 .../adoc/modules/keycloak/images/client-secret.png | Bin 0 -> 101319 bytes
 .../keycloak/images/create-regular-user-role.png   | Bin 0 -> 54651 bytes
 .../keycloak/images/create-simpleapp-client.png    | Bin 0 -> 123580 bytes
 .../keycloak/images/define-simpleapp-realm.png     | Bin 0 -> 60911 bytes
 .../modules/keycloak/images/logged-in-as-sven.png  | Bin 0 -> 329658 bytes
 .../images/login-to-admin-console-prompt.png       | Bin 0 -> 54678 bytes
 .../keycloak/images/login-to-admin-console.png     | Bin 0 -> 100389 bytes
 .../modules/keycloak/images/sven-credentials.png   | Bin 0 -> 98429 bytes
 .../modules/keycloak/images/test-sven-login.png    | Bin 0 -> 86866 bytes
 .../main/adoc/modules/keycloak/pages/about.adoc    | 182 +++++++++++++++++++--
 .../keycloak/IsisModuleSecurityKeycloak.java       | 127 +++++++++++++-
 .../authentication/AuthenticatorKeycloak.java      |  61 -------
 .../keycloak/handler/LogoutHandlerForKeycloak.java |  60 +++++++
 .../services/KeycloakOauth2UserService.java        | 103 ++++++++++++
 .../keycloak/webmodule/KeycloakFilter.java         | 101 ------------
 .../keycloak/webmodule/WebModuleKeycloak.java      |  73 ---------
 .../viewer/IsisModuleViewerWicketViewer.java       |   2 +
 .../services/ImpersonatedUserHolderForWicket.java  |  77 +++++++++
 27 files changed, 618 insertions(+), 292 deletions(-)

diff --git a/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java b/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java
index aed0f79..63c069b 100644
--- a/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java
+++ b/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java
@@ -42,10 +42,6 @@ import javax.validation.Payload;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.validation.annotation.Validated;
-
 import org.apache.isis.applib.IsisModuleApplib;
 import org.apache.isis.applib.annotation.ActionLayout;
 import org.apache.isis.applib.annotation.DomainObject;
@@ -60,7 +56,6 @@ import org.apache.isis.applib.services.userreg.EmailNotificationService;
 import org.apache.isis.applib.services.userreg.UserRegistrationService;
 import org.apache.isis.applib.services.userui.UserMenu;
 import org.apache.isis.commons.internal.context._Context;
-import org.apache.isis.core.config.IsisConfiguration.Viewer;
 import org.apache.isis.core.config.metamodel.facets.DefaultViewConfiguration;
 import org.apache.isis.core.config.metamodel.facets.EditingObjectsConfiguration;
 import org.apache.isis.core.config.metamodel.facets.PublishingPolicies.ActionPublishingPolicy;
@@ -69,13 +64,15 @@ import org.apache.isis.core.config.metamodel.facets.PublishingPolicies.PropertyP
 import org.apache.isis.core.config.metamodel.services.ApplicationFeaturesInitConfiguration;
 import org.apache.isis.core.config.metamodel.specloader.IntrospectionMode;
 import org.apache.isis.core.config.viewer.wicket.DialogMode;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.validation.annotation.Validated;
 
 import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
 import static java.lang.annotation.ElementType.FIELD;
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
 import lombok.Data;
 import lombok.Getter;
 import lombok.Setter;
@@ -137,6 +134,42 @@ public class IsisConfiguration {
 
         }
 
+        private final Keycloak keycloak = new Keycloak();
+        @Data
+        public static class Keycloak {
+            /**
+             * The name of the realm for the Apache Isis application, as configured in
+             * Keycloak.
+             */
+            private String realm;
+
+            /**
+             * The base URL for the keycloak server.
+             *
+             * <p>
+             *     For example, if running a keycloak using Docker container, such as:
+             *     <pre>
+             *         docker run -p 9090:8080 \
+             *             -e KEYCLOAK_USER=admin \
+             *             -e KEYCLOAK_PASSWORD=admin \
+             *             quay.io/keycloak/keycloak:14.0.0
+             *     </pre>,
+             *
+             *     then the URL would be "http://localhost:9090/auth".
+             * </p>
+             */
+            private String baseUrl;
+
+            /**
+             * Specifies where users will be redirected after authenticating successfully if they
+             * have not visited a secured page prior to authenticating or {@code alwaysUse} is
+             * true.
+             */
+            private String loginSuccessUrl = "/wicket";
+        }
+        
+        
+
     }
 
     private final Applib applib = new Applib();
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java b/core/security/src/main/java/org/apache/isis/core/security/authentication/login/LoginSuccessHandlerUNUSED.java
similarity index 51%
copy from security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
copy to core/security/src/main/java/org/apache/isis/core/security/authentication/login/LoginSuccessHandlerUNUSED.java
index 213a0c7..c2034eb 100644
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
+++ b/core/security/src/main/java/org/apache/isis/core/security/authentication/login/LoginSuccessHandlerUNUSED.java
@@ -16,32 +16,24 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.security.keycloak;
-
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
-
-import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices;
-import org.apache.isis.security.keycloak.authentication.AuthenticatorKeycloak;
-import org.apache.isis.security.keycloak.webmodule.WebModuleKeycloak;
-import org.apache.isis.core.webapp.IsisModuleCoreWebapp;
+package org.apache.isis.core.security.authentication.login;
 
 /**
- * Configuration Bean to support Isis Security using Shiro.
+ * To allow login to be completed.
  *
- * @since 2.0 {@index}
+ * <p>
+ *     Provided as a hook for some of the more sophisticated
+ *     {@link org.apache.isis.core.security.authentication.Authenticator}s
+ *     (eg keycloak) to synchronise the state of the
+ *     {@link org.apache.isis.core.security.authentication.manager.AuthenticationManager}
+ *     on successful external login.
+ * </p>
  */
-@Configuration
-@Import({
-        // modules
-        IsisModuleCoreRuntimeServices.class,
-        IsisModuleCoreWebapp.class,
-
-        // @Service's
-        AuthenticatorKeycloak.class,
-        WebModuleKeycloak.class,
+public interface LoginSuccessHandlerUNUSED {
 
-})
-public class IsisModuleSecurityKeycloak {
+    /**
+     * Indicates a successful login
+     */
+    void onSuccess();
 
 }
diff --git a/mavendeps/webapp/pom.xml b/mavendeps/webapp/pom.xml
index 9d097f3..798ba28 100644
--- a/mavendeps/webapp/pom.xml
+++ b/mavendeps/webapp/pom.xml
@@ -100,10 +100,6 @@
 
 		<dependency>
 			<groupId>org.apache.isis.viewer</groupId>
-			<artifactId>isis-viewer-restfulobjects-viewer</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>org.apache.isis.viewer</groupId>
 			<artifactId>isis-viewer-restfulobjects-jaxrsresteasy4</artifactId>
 		</dependency>
 
@@ -115,10 +111,15 @@
 			<groupId>org.apache.isis.security</groupId>
 			<artifactId>isis-security-bypass</artifactId>
 		</dependency>
+
+		<!--
+		we no longer include isis-security-keycloak in order to reduce the
+		3rd party dependencies.
 		<dependency>
 			<groupId>org.apache.isis.security</groupId>
 			<artifactId>isis-security-keycloak</artifactId>
 		</dependency>
+		-->
 
 		<!--
 		we no longer include isis-security-shiro in order to reduce the
diff --git a/security/keycloak/pom.xml b/security/keycloak/pom.xml
index c6cd4fd..1239ed6 100644
--- a/security/keycloak/pom.xml
+++ b/security/keycloak/pom.xml
@@ -7,9 +7,9 @@
   to you under the Apache License, Version 2.0 (the
   "License"); you may not use this file except in compliance
   with the License.  You may obtain a copy of the License at
-  
+
          http://www.apache.org/licenses/LICENSE-2.0
-         
+
   Unless required by applicable law or agreed to in writing,
   software distributed under the License is distributed on an
   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -53,12 +53,12 @@
     </dependencyManagement>
 
     <dependencies>
-    
+
     	<dependency>
             <groupId>org.apache.isis.core</groupId>
             <artifactId>isis-core-runtime</artifactId>
         </dependency>
-    
+
         <dependency>
             <groupId>org.apache.isis.core</groupId>
             <artifactId>isis-core-runtimeservices</artifactId>
@@ -69,6 +69,32 @@
             <artifactId>isis-core-webapp</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.isis.security</groupId>
+            <artifactId>isis-security-spring</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-client</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-jose</artifactId>
+        </dependency>
+
+
         <!-- TESTING -->
 
         <dependency>
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/account-mgmt.png b/security/keycloak/src/main/adoc/modules/keycloak/images/account-mgmt.png
new file mode 100644
index 0000000..d261f44
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/account-mgmt.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/add-realm-prompt.png b/security/keycloak/src/main/adoc/modules/keycloak/images/add-realm-prompt.png
new file mode 100644
index 0000000..d41cfde
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/add-realm-prompt.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-to-regular-user-role.png b/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-to-regular-user-role.png
new file mode 100644
index 0000000..3e44896
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-to-regular-user-role.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-user-prompt.png b/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-user-prompt.png
new file mode 100644
index 0000000..d5022ca
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-user-prompt.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/client-app-config.png b/security/keycloak/src/main/adoc/modules/keycloak/images/client-app-config.png
new file mode 100644
index 0000000..4e9a445
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/client-app-config.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/client-secret.png b/security/keycloak/src/main/adoc/modules/keycloak/images/client-secret.png
new file mode 100644
index 0000000..d5645c3
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/client-secret.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/create-regular-user-role.png b/security/keycloak/src/main/adoc/modules/keycloak/images/create-regular-user-role.png
new file mode 100644
index 0000000..3cce286
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/create-regular-user-role.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/create-simpleapp-client.png b/security/keycloak/src/main/adoc/modules/keycloak/images/create-simpleapp-client.png
new file mode 100644
index 0000000..8acfb7d
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/create-simpleapp-client.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/define-simpleapp-realm.png b/security/keycloak/src/main/adoc/modules/keycloak/images/define-simpleapp-realm.png
new file mode 100644
index 0000000..e089f38
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/define-simpleapp-realm.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/logged-in-as-sven.png b/security/keycloak/src/main/adoc/modules/keycloak/images/logged-in-as-sven.png
new file mode 100644
index 0000000..39fd92c
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/logged-in-as-sven.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console-prompt.png b/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console-prompt.png
new file mode 100644
index 0000000..4006ddf
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console-prompt.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console.png b/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console.png
new file mode 100644
index 0000000..f739eaa
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/sven-credentials.png b/security/keycloak/src/main/adoc/modules/keycloak/images/sven-credentials.png
new file mode 100644
index 0000000..15b2ad9
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/sven-credentials.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/test-sven-login.png b/security/keycloak/src/main/adoc/modules/keycloak/images/test-sven-login.png
new file mode 100644
index 0000000..4e2f47c
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/test-sven-login.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/pages/about.adoc b/security/keycloak/src/main/adoc/modules/keycloak/pages/about.adoc
index c6eb1e6..f4dd010 100644
--- a/security/keycloak/src/main/adoc/modules/keycloak/pages/about.adoc
+++ b/security/keycloak/src/main/adoc/modules/keycloak/pages/about.adoc
@@ -4,15 +4,32 @@
 :page-partial:
 
 
-This guide describes the configuration of the Keycloak implementation of Apache Isis' `Authenticator and `Authorizor` APIs.
+This guide describes the configuration of the Keycloak implementation of Apache Isis' `Authenticator` API.
 
+It does _not_ however provide any implementation of xref:refguide:core:index/security/authorization/Authorizor.adoc[Authorizor] SPI.
+You will therefore need to configure an alternative implementation, eg the xref:bypass:about.adoc[Bypass] implementation (to disable authorisation checks completely), or use the xref:secman:about.adoc[SecMan] implementation.
+
+
+== Dependency
+
+In the webapp module of your application, add the following dependency:
+
+[source,xml]
+.pom.xml
+----
+<dependencies>
+    <dependency>
+        <groupId>org.apache.isis.security</groupId>
+        <artifactId>isis-security-keycloak</artifactId>
+    </dependency>
+</dependencies>
+----
 
-include::docs:mavendeps:partial$setup-and-configure-mavendeps-webapp.adoc[leveloffset=+1]
 
 
 == Update AppManifest
 
-In your application's `AppManifest` (top-level Spring `@Configuration` used to bootstrap the app), import the
+In your application's `AppManifest` (top-level Spring `@Configuration` used to bootstrap the app), import the `IsisModuleSecurityKeycloak` module and remove any other `IsisModuleSecurityXxx` modules.
 
 [source,java]
 .AppManifest.java
@@ -27,24 +44,163 @@ public class AppManifest {
 }
 ----
 
-Make sure that no other `IsisModuleSecurityXxx` module is imported.
+Also, as this module provides no implementation of the xref:refguide:core:index/security/authorization/Authorizor.adoc[Authorizor] SPI, instead you will need some an alternative implementation, such as the xref:bypass:about.adoc[Bypass] implementation.
+(Note: this will in effect disable authorisation checks).
+
+[source,java]
+.AppManifest.java
+----
+@Configuration
+@Import({
+        ...
+        IsisModuleSecurityKeycloak.class,   // <.>
+        AuthorizorBypass.class,             // <.>
+        ...
+})
+public class AppManifest {
+}
+----
+<.> make sure that no other `IsisModuleSecurityXxx` module is imported.
+<.> or some other implementation of `Authorizor`.
+
 
 
-== Design
 
-The module configures a filter that expects Keycloak to set three `X-Auth-Xxx` headers:
 
-* `X-Auth-Userid` - is used as the username
-* `X-Auth-Roles` - is a comma-separated set of roles.
+[#walk-through]
+== Walk-through
+
+For simplicity, we'll run Keycloak in Docker with an in-memory database.
+Obviously in production you would need a persistent database.
+
+NOTE: These notes were adapted from the tutorial provided on link:https://www.keycloak.org/getting-started/getting-started-docker[keycloak's website].
+
+
+=== Startup keycloak and login as keycloak admin
+
+* Start up keycloak; we'll run on port _9090_:
 +
-The `org.apache.isis.viewer.wicket.roles.USER` role -- as required by xref:vw::about.adoc[Web UI (Wicket viewer)]  -- is automatically added to this list of roles.
+[source,bash]
+----
+docker run -p 9090:8080 \
+    -e KEYCLOAK_USER=admin \
+    -e KEYCLOAK_PASSWORD=admin \
+    quay.io/keycloak/keycloak:14.0.0
+----
 
-* `X-Auth-Subject` - is unused
+* login to the Admin console:
++
+image::login-to-admin-console.png[width=300px]
++
+and
++
+image::login-to-admin-console-prompt.png[width=300px]
 
-The user and roles are accessible programmatically from the xref:refguide:applib:index/services/user/UserMemento.adoc[UserMemento] obtained from xref:refguide:applib:index/services/user/UserService.adoc[UserService] domain service.
 
+=== Create a realm for simpleapp
 
-== Walk-through
+WARNING: TODO: clean up these screenshots, make consistent with text.
+
+* create a realm:
++
+image::add-realm-prompt.png[width=250px]
++
+and:
++
+image::define-simpleapp-realm.png[width=400px]
+
+
+=== Create a client
+
+* create the client:
++
+image::create-simpleapp-client.png[width=400px]
+
+* specify _Access Type_ = confidential, and _Valid Redirect URI_ for the client:
++
+image::client-app-config.png[width=400px]
+
+* copy the secret from the "credentials" tab:
++
+image::client-secret.png[width=600px]
+
+=== Create 'regular-user' role in the realm
+
+* create role:
++
+image::create-regular-user-role.png[width=400px]
+
+
+=== Configure the application as a Keycloak client
+
+* the keycloak config:
++
+[source,properties]
+.config/application.properties
+----
+isis.security.keycloak.realm=demo                                       #<.>
+isis.security.keycloak.base-url=http://localhost:9090/auth              #<.>
+
+kc.realm-url=${isis.security.keycloak.base-url}/realms/${isis.security.keycloak.realm} #<.>
+
+spring.security.oauth2.client.registration.demo.client-id=app-demo      #<.>
+spring.security.oauth2.client.registration.demo.client-name=Demo App
+spring.security.oauth2.client.registration.demo.client-secret=e3f519b4-0272-4261-9912-8b7453ac4ecd                    #<.>
+
+
+spring.security.oauth2.client.registration.demo.provider=keycloak       #<.>
+spring.security.oauth2.client.registration.demo.authorization-grant-type=authorization_code
+spring.security.oauth2.client.registration.demo.scope=openid, profile
+spring.security.oauth2.client.registration.demo.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
+spring.security.oauth2.client.provider.keycloak.authorization-uri=${kc.realm-url}/protocol/openid-connect/auth
+spring.security.oauth2.client.provider.keycloak.jwk-set-uri=${kc.realm-url}/protocol/openid-connect/certs
+spring.security.oauth2.client.provider.keycloak.token-uri=${kc.realm-url}/protocol/openid-connect/token
+spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
+----
+
+<.> as defined in keycloak.
+The registration properties below must specify this property as the `registration` key.
+<.> URL where keycloak is running
+<.> application-defined property, just to reduce the boilerplate below
+<.> must match the client name entered in the admin console
+<.> as taken from the credential tab of the realm
+<.> remaining property values are boilerplate and should not need to change.
++
+IMPORTANT: Make sure though to change the key itself: `spring.security.oauth2.client.registration.xxx` where "xxx" is the name of the realm being registered to Spring Security.
+
+
+
+=== Create sven user in the realm
+
+* add sven user:
++
+image::add-sven-user-prompt.png[width=400px]
+
+* add credentials (password):
++
+image::sven-credentials.png[width=400px]
+
+* check that the account is setup by navigating to link:http://localhost:9090/auth/realms/simpleapp/account/[]:
++
+image::account-mgmt.png[width=800px]
++
+sign-in:
++
+image::test-sven-login.png[width=300px]
+
+* should be logged in ok:
++
+image::logged-in-as-sven.png[width=800px]
+
+* add to 'regular-user' role:
++
+image::add-sven-to-regular-user-role.png[width=800px]
+
+
+
+== Resources:
 
-WARNING: TODO - show how this fits together.
+* link:https://www.keycloak.org/docs/latest/securing_apps/index.html#_spring_boot_adapter[Keycloak documentation].
+* link:https://www.baeldung.com/spring-boot-keycloak[baeldung article].
+* link:https://dzone.com/articles/secure-spring-boot-application-with-keycloak[Dzone article]
 
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
index 213a0c7..1cc66e8 100644
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
+++ b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
@@ -18,16 +18,35 @@
  */
 package org.apache.isis.security.keycloak;
 
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
+import java.util.List;
 
+import org.apache.isis.core.config.IsisConfiguration;
 import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices;
-import org.apache.isis.security.keycloak.authentication.AuthenticatorKeycloak;
-import org.apache.isis.security.keycloak.webmodule.WebModuleKeycloak;
+import org.apache.isis.core.security.authentication.login.LoginSuccessHandlerUNUSED;
 import org.apache.isis.core.webapp.IsisModuleCoreWebapp;
+import org.apache.isis.security.keycloak.handler.LogoutHandlerForKeycloak;
+import org.apache.isis.security.keycloak.services.KeycloakOauth2UserService;
+import org.apache.isis.security.spring.IsisModuleSecuritySpring;
+import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
+import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
 
 /**
- * Configuration Bean to support Isis Security using Shiro.
+ * Configuration Bean to support Isis Security using Keycloak.
  *
  * @since 2.0 {@index}
  */
@@ -37,11 +56,103 @@ import org.apache.isis.core.webapp.IsisModuleCoreWebapp;
         IsisModuleCoreRuntimeServices.class,
         IsisModuleCoreWebapp.class,
 
-        // @Service's
-        AuthenticatorKeycloak.class,
-        WebModuleKeycloak.class,
+        // services
+        LogoutHandlerForKeycloak.class,
+
+        // builds on top of Spring
+        IsisModuleSecuritySpring.class,
 
 })
+@EnableWebSecurity
 public class IsisModuleSecurityKeycloak {
 
+
+    @Bean
+    public WebSecurityConfigurerAdapter webSecurityConfigurer(
+            final IsisConfiguration isisConfiguration,
+            final KeycloakOauth2UserService keycloakOidcUserService,
+            final List<LoginSuccessHandlerUNUSED> loginSuccessHandlersUNUSED,
+            final List<LogoutHandler> logoutHandlers
+            ) {
+        val realm = isisConfiguration.getSecurity().getKeycloak().getRealm();
+        return new KeycloakWebSecurityConfigurerAdapter(keycloakOidcUserService, logoutHandlers, isisConfiguration
+        );
+    }
+
+//    @RequiredArgsConstructor
+//    public static class AuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
+//
+//        final List<LoginSuccessHandler> loginSuccessHandlers;
+//
+//        @Override
+//        public void onAuthenticationSuccess(
+//                final HttpServletRequest request,
+//                final HttpServletResponse response,
+//                final Authentication authentication) throws ServletException, IOException {
+//            super.onAuthenticationSuccess(request, response, authentication);
+//            loginSuccessHandlers.forEach(LoginSuccessHandler::onSuccess);
+//        }
+//    }
+
+    @Bean
+    KeycloakOauth2UserService keycloakOidcUserService(OAuth2ClientProperties oauth2ClientProperties) {
+
+        // TODO use default JwtDecoder - where to grab?
+        val jwtDecoder = new NimbusJwtDecoderJwkSupport(
+                oauth2ClientProperties.getProvider().get("keycloak").getJwkSetUri());
+
+        val authoritiesMapper = new SimpleAuthorityMapper();
+        authoritiesMapper.setConvertToUpperCase(true);
+
+        return new KeycloakOauth2UserService(jwtDecoder, authoritiesMapper);
+    }
+
+    @RequiredArgsConstructor
+    public static class KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
+
+        private final KeycloakOauth2UserService keycloakOidcUserService;
+        private final List<LogoutHandler> logoutHandlers;
+        private final IsisConfiguration isisConfiguration;
+
+        @Override
+        public void configure(HttpSecurity http) throws Exception {
+
+            val successUrl = isisConfiguration.getSecurity().getKeycloak().getLoginSuccessUrl();
+            val realm = isisConfiguration.getSecurity().getKeycloak().getRealm();
+
+            val httpSecurityLogoutConfigurer =
+                http
+                    .sessionManagement()
+                        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
+                    .and()
+
+                    .authorizeRequests()
+                        .anyRequest().authenticated()
+                    .and()
+
+                    // responsibility to propagate logout to Keycloak is performed by
+                    // LogoutHandlerForKeycloak (called by Isis' LogoutMenu, not by Spring)
+                    // this is to ensure that Isis can invalidate the http session eagerly and not preserve it in
+                    // the SecurityContextPersistenceFilter (which uses http session to do its work)
+                    .logout()
+                        .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
+
+            logoutHandlers.forEach(httpSecurityLogoutConfigurer::addLogoutHandler);
+
+            httpSecurityLogoutConfigurer
+                    .and()
+
+                    // This is the point where OAuth2 login of Spring 5 gets enabled
+                    .oauth2Login()
+                        .defaultSuccessUrl(successUrl, true)
+//                            .successHandler(new AuthSuccessHandler(loginSuccessHandlers))
+                        .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
+                        .userInfoEndpoint()
+                            .oidcUserService(keycloakOidcUserService)
+                    .and()
+
+                    .loginPage(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + realm);
+        }
+    }
 }
+
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/authentication/AuthenticatorKeycloak.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/authentication/AuthenticatorKeycloak.java
deleted file mode 100644
index 1c63166..0000000
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/authentication/AuthenticatorKeycloak.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.security.keycloak.authentication;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
-
-import org.apache.isis.applib.annotation.PriorityPrecedence;
-import org.apache.isis.applib.services.iactn.InteractionProvider;
-import org.apache.isis.applib.services.iactnlayer.InteractionContext;
-import org.apache.isis.core.security.authentication.AuthenticationRequest;
-import org.apache.isis.core.security.authentication.Authenticator;
-
-/**
- * @since 2.0 {@index}
- */
-@Service
-@Named("isis.security.AuthenticatorKeycloak")
-@javax.annotation.Priority(PriorityPrecedence.EARLY)
-@Qualifier("Keycloak")
-@Singleton
-public class AuthenticatorKeycloak implements Authenticator {
-
-    @Inject private InteractionProvider interactionProvider;
-
-    @Override
-    public final boolean canAuthenticate(final Class<? extends AuthenticationRequest> authenticationRequestClass) {
-        return true;
-    }
-
-    @Override
-    public InteractionContext authenticate(final AuthenticationRequest request, final String code) {
-        // HTTP request filters should already have taken care of Authentication creation
-        return interactionProvider.currentInteractionContext().orElse(null);
-    }
-
-    @Override
-    public void logout(final InteractionContext session) {
-    }
-
-}
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/handler/LogoutHandlerForKeycloak.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/handler/LogoutHandlerForKeycloak.java
new file mode 100644
index 0000000..36513d9
--- /dev/null
+++ b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/handler/LogoutHandlerForKeycloak.java
@@ -0,0 +1,60 @@
+package org.apache.isis.security.keycloak.handler;
+
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import org.apache.isis.core.security.authentication.logout.LogoutHandler;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+/**
+ * Propagates logouts to Keycloak.
+ *
+ * <p>
+ * Necessary because Spring Security 5 (currently) doesn't support
+ * end-session-endpoints.
+ * </p>
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class LogoutHandlerForKeycloak implements LogoutHandler {
+
+    private final RestTemplate restTemplate;
+
+    public LogoutHandlerForKeycloak() {
+        this(new RestTemplate());
+    }
+
+    @Override public void logout() {
+        val authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication != null) {
+            propagateLogoutToKeycloak((OidcUser) authentication.getPrincipal());
+        }
+
+    }
+    private void propagateLogoutToKeycloak(OidcUser user) {
+
+        val endSessionEndpoint = String.format("%s/protocol/openid-connect/logout", user.getIssuer());
+
+        val builder = UriComponentsBuilder
+                .fromUriString(endSessionEndpoint)
+                .queryParam("id_token_hint", user.getIdToken().getTokenValue());
+
+        val logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class);
+        if (logoutResponse.getStatusCode().is2xxSuccessful()) {
+            log.info("Successfully logged out in Keycloak");
+        } else {
+            log.info("Could not propagate logout to Keycloak");
+        }
+    }
+
+    @Override public boolean isHandlingCurrentThread() {
+        return true;
+    }
+}
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/services/KeycloakOauth2UserService.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/services/KeycloakOauth2UserService.java
new file mode 100644
index 0000000..fe7e67d
--- /dev/null
+++ b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/services/KeycloakOauth2UserService.java
@@ -0,0 +1,103 @@
+package org.apache.isis.security.keycloak.services;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.util.CollectionUtils;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RequiredArgsConstructor
+public class KeycloakOauth2UserService extends OidcUserService {
+
+    private final static OAuth2Error INVALID_REQUEST = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
+
+    final JwtDecoder jwtDecoder;
+    final GrantedAuthoritiesMapper authoritiesMapper;
+
+    /**
+     * Augments {@link OidcUserService#loadUser(OidcUserRequest)} to add authorities
+     * provided by Keycloak.
+     * <p>
+     * Needed because {@link OidcUserService#loadUser(OidcUserRequest)} (currently)
+     * does not provide a hook for adding custom authorities from a
+     * {@link OidcUserRequest}.
+     */
+    @Override
+    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
+
+        OidcUser user = super.loadUser(userRequest);
+
+        Set<GrantedAuthority> authorities = new LinkedHashSet<>();
+        authorities.addAll(user.getAuthorities());
+        authorities.addAll(extractKeycloakAuthorities(userRequest));
+
+        return new DefaultOidcUser(authorities, userRequest.getIdToken(), user.getUserInfo(), "preferred_username");
+    }
+
+    /**
+     * Extracts {@link GrantedAuthority GrantedAuthorities} from the AccessToken in
+     * the {@link OidcUserRequest}.
+     *
+     * @param userRequest
+     * @return
+     */
+    private Collection<? extends GrantedAuthority> extractKeycloakAuthorities(OidcUserRequest userRequest) {
+
+        Jwt token = parseJwt(userRequest.getAccessToken().getTokenValue());
+
+        // Would be great if Spring Security would provide something like a pluggable
+        // OidcUserRequestAuthoritiesExtractor interface to hide the junk below...
+
+        @SuppressWarnings("unchecked")
+        val resourceMap = (Map<String, Object>) token.getClaims().get("resource_access");
+        String clientId = userRequest.getClientRegistration().getClientId();
+
+        @SuppressWarnings("unchecked")
+        val clientResource = (Map<String, Map<String, Object>>) resourceMap.get(clientId);
+        if (CollectionUtils.isEmpty(clientResource)) {
+            return Collections.emptyList();
+        }
+
+        @SuppressWarnings("unchecked")
+        List<String> clientRoles = (List<String>) clientResource.get("roles");
+        if (CollectionUtils.isEmpty(clientRoles)) {
+            return Collections.emptyList();
+        }
+
+        Collection<? extends GrantedAuthority> authorities = AuthorityUtils
+                .createAuthorityList(clientRoles.toArray(new String[0]));
+        if (authoritiesMapper == null) {
+            return authorities;
+        }
+
+        return authoritiesMapper.mapAuthorities(authorities);
+    }
+
+    private Jwt parseJwt(String accessTokenValue) {
+        try {
+            // Token is already verified by spring security infrastructure
+            return jwtDecoder.decode(accessTokenValue);
+        } catch (JwtException e) {
+            throw new OAuth2AuthenticationException(INVALID_REQUEST, e);
+        }
+    }
+}
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/KeycloakFilter.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/KeycloakFilter.java
deleted file mode 100644
index 83f6f34..0000000
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/KeycloakFilter.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.security.keycloak.webmodule;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Enumeration;
-import java.util.List;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.springframework.beans.factory.annotation.Autowired;
-
-import org.apache.isis.applib.services.iactnlayer.InteractionContext;
-import org.apache.isis.applib.services.iactnlayer.InteractionService;
-import org.apache.isis.applib.services.user.UserMemento;
-import org.apache.isis.applib.services.user.UserMemento.AuthenticationSource;
-
-import lombok.val;
-
-/**
- * @since 2.0 {@index}
- */
-public class KeycloakFilter implements Filter {
-
-    @Autowired private InteractionService interactionService;
-
-    @Override
-    public void doFilter(
-            final ServletRequest servletRequest,
-            final ServletResponse servletResponse,
-            final FilterChain filterChain) throws IOException, ServletException {
-
-        final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
-        final HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
-        final String userid = header(httpServletRequest, "X-Auth-Userid");
-        final String rolesHeader = header(httpServletRequest, "X-Auth-Roles");
-        final String subjectHeader = header(httpServletRequest, "X-Auth-Subject");
-        if(userid == null || rolesHeader == null || subjectHeader == null) {
-            httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            return;
-        }
-        final List<String> roles = toClaims(rolesHeader);
-
-        val user = UserMemento.ofNameAndRoleNames(userid, roles.stream())
-                .withAuthenticationSource(AuthenticationSource.EXTERNAL)
-                .withAuthenticationCode(subjectHeader);
-
-        interactionService.run(
-                InteractionContext.ofUserWithSystemDefaults(user),
-                ()->filterChain.doFilter(servletRequest, servletResponse));
-    }
-
-    static List<String> toClaims(final String claimsHeader) {
-        final List<String> roles = asRoles(claimsHeader);
-        roles.add("org.apache.isis.viewer.wicket.roles.USER");
-        return roles;
-    }
-
-    static List<String> asRoles(String claimsHeader) {
-        final List<String> roles = new ArrayList<>();
-        if(claimsHeader != null) {
-            roles.addAll(Arrays.asList(claimsHeader.split(",")));
-        }
-        return roles;
-    }
-
-    private String header(final HttpServletRequest httpServletRequest, final String headerName) {
-        final Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
-        while(headerNames.hasMoreElements()) {
-            final String header = headerNames.nextElement();
-            if(header.toLowerCase().equals(headerName.toLowerCase())) {
-                return httpServletRequest.getHeader(header);
-            }
-        }
-        return null;
-    }
-}
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/WebModuleKeycloak.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/WebModuleKeycloak.java
deleted file mode 100644
index 760dbd6..0000000
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/WebModuleKeycloak.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.security.keycloak.webmodule;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletContextListener;
-import javax.servlet.ServletException;
-
-import org.apache.isis.applib.annotation.PriorityPrecedence;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
-
-import org.apache.isis.applib.services.inject.ServiceInjector;
-import org.apache.isis.commons.collections.Can;
-import org.apache.isis.core.webapp.modules.WebModuleAbstract;
-
-import lombok.Getter;
-
-/**
- * WebModule to enable support for Keycloak.
- *
- * @since 2.0 {@index}
- */
-@Service
-@Named("isis.security.WebModuleKeycloak")
-@javax.annotation.Priority(PriorityPrecedence.FIRST + 100)
-@Qualifier("Keycloak")
-public final class WebModuleKeycloak extends WebModuleAbstract {
-
-    private static final String KEYCLOAK_FILTER_NAME = "KeycloakFilter";
-
-    @Getter
-    private final String name = "Keycloak";
-
-    @Inject
-    public WebModuleKeycloak(ServiceInjector serviceInjector) {
-        super(serviceInjector);
-    }
-
-    @Override
-    public Can<ServletContextListener> init(ServletContext ctx) throws ServletException {
-
-        registerFilter(ctx, KEYCLOAK_FILTER_NAME, KeycloakFilter.class)
-            .ifPresent(filterReg -> {
-                filterReg.addMappingForUrlPatterns(
-                        null,
-                        false, // filter is forced first
-                        "/*");
-
-            });
-
-        return Can.empty(); // registers no listeners
-    }
-
-}
diff --git a/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/IsisModuleViewerWicketViewer.java b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/IsisModuleViewerWicketViewer.java
index 9997e8f..9d5e4db 100644
--- a/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/IsisModuleViewerWicketViewer.java
+++ b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/IsisModuleViewerWicketViewer.java
@@ -33,6 +33,7 @@ import org.apache.isis.viewer.wicket.viewer.services.BookmarkUiServiceWicket;
 import org.apache.isis.viewer.wicket.viewer.services.DeepLinkServiceWicket;
 import org.apache.isis.viewer.wicket.viewer.services.HintStoreUsingWicketSession;
 import org.apache.isis.viewer.wicket.viewer.services.ImageResourceCacheClassPath;
+import org.apache.isis.viewer.wicket.viewer.services.ImpersonatedUserHolderForWicket;
 import org.apache.isis.viewer.wicket.viewer.services.LocaleProviderWicket;
 import org.apache.isis.viewer.wicket.viewer.services.TranslationsResolverWicket;
 import org.apache.isis.viewer.wicket.viewer.services.WicketViewerSettingsDefault;
@@ -53,6 +54,7 @@ import org.apache.isis.viewer.wicket.viewer.webmodule.WebModuleWicket;
         ComponentFactoryRegistryDefault.class,
         DeepLinkServiceWicket.class,
         ImageResourceCacheClassPath.class,
+        ImpersonatedUserHolderForWicket.class,
         LocaleProviderWicket.class,
         HintStoreUsingWicketSession.class,
         ObjectMementoServiceWicket.class,
diff --git a/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/services/ImpersonatedUserHolderForWicket.java b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/services/ImpersonatedUserHolderForWicket.java
new file mode 100644
index 0000000..660758e
--- /dev/null
+++ b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/services/ImpersonatedUserHolderForWicket.java
@@ -0,0 +1,77 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.viewer.wicket.viewer.services;
+
+import java.util.Optional;
+
+import javax.inject.Named;
+
+import org.apache.wicket.Session;
+import org.springframework.stereotype.Component;
+
+import org.apache.isis.applib.annotation.PriorityPrecedence;
+import org.apache.isis.applib.services.user.ImpersonatedUserHolder;
+import org.apache.isis.applib.services.user.UserMemento;
+
+/**
+ * Implementation that supports impersonation, using the Wicket {@link Session}
+ * to store the value.
+ *
+ * @since 2.0 {@index}
+ */
+@Component
+@Named("isis.webapp.ImpersonatedUserHolderForWicket")
+@javax.annotation.Priority(PriorityPrecedence.MIDPOINT - 100)
+public class ImpersonatedUserHolderForWicket implements ImpersonatedUserHolder {
+
+    private static final String HTTP_SESSION_KEY_IMPERSONATED_USER =
+            ImpersonatedUserHolderForWicket.class.getName() + "#userMemento";
+
+    @Override
+    public boolean supportsImpersonation() {
+        return session().isPresent();
+    }
+
+    @Override
+    public void setUserMemento(final UserMemento userMemento) {
+        session()
+        .ifPresent(session->
+            session.setAttribute(HTTP_SESSION_KEY_IMPERSONATED_USER, userMemento));
+    }
+
+    @Override
+    public Optional<UserMemento> getUserMemento() {
+        return session()
+            .map(session->session.getAttribute(HTTP_SESSION_KEY_IMPERSONATED_USER))
+            .filter(UserMemento.class::isInstance)
+            .map(UserMemento.class::cast);
+    }
+
+    @Override
+    public void clearUserMemento() {
+        session()
+        .ifPresent(session->
+            session.removeAttribute(HTTP_SESSION_KEY_IMPERSONATED_USER));
+    }
+
+    private static Optional<Session> session() {
+        return Optional.ofNullable(Session.get());
+    }
+
+}