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/17 14:38:28 UTC

[isis] 01/03: ISIS-2793: rewriting keycloak to use Spring oauth2

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

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

commit 34ba40eff5aafd384e91a3c407be8eda5b0b0e00
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Jul 14 07:20:55 2021 +0100

    ISIS-2793: rewriting keycloak to use Spring oauth2
---
 .../isis/applib/services/user/ImpersonateMenu.java |   2 +-
 .../applib/id/LogicalTypeTest_valueSemantics.java  |   6 +-
 .../apache/isis/core/config/IsisConfiguration.java |  40 +++++
 core/pom.xml                                       |  11 ++
 .../authentication/login/LoginSuccessHandler.java  |  36 ++--
 .../authentication/logout/LogoutHandler.java       |  10 +-
 .../manager/AuthenticationManager.java             |   2 -
 .../viewer/vaadin/ui/auth/LogoutHandlerVaa.java    |  22 ++-
 mavendeps/webapp/pom.xml                           |   5 +
 security/keycloak/pom.xml                          |  53 +++++-
 .../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    | 184 ++++++++++++++++++++-
 .../keycloak/IsisModuleSecurityKeycloak.java       | 128 +++++++++++++-
 .../authentication/AuthenticatorKeycloak.java      |  61 -------
 .../keycloak/handler/KeycloakLogoutHandler.java    |  54 ++++++
 .../services/KeycloakOauth2UserService.java        | 103 ++++++++++++
 .../keycloak/webmodule/KeycloakFilter.java         | 101 -----------
 .../keycloak/webmodule/WebModuleKeycloak.java      |  73 --------
 .../spring/webmodule/SpringSecurityFilter.java     |   7 +-
 .../spring/webmodule/WebModuleSpringSecurity.java  |   2 +-
 .../wicket/ui/app/logout/LogoutHandlerWkt.java     |  23 ++-
 .../viewer/IsisModuleViewerWicketViewer.java       |   2 +
 .../isis/viewer/wicket/viewer/services/Aut.java    |   9 +
 .../services/ImpersonatedUserHolderForWicket.java  |  77 +++++++++
 37 files changed, 706 insertions(+), 305 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenu.java b/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenu.java
index 360fe3a..9bb74ef 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenu.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenu.java
@@ -78,7 +78,7 @@ public class ImpersonateMenu {
     public void impersonate(
             final String userName) {
 
-        this.userService.impersonateUser(userName, Collections.emptyList());
+        this.userService.impersonateUser(userName, Collections.singletonList("org.apache.isis.viewer.wicket.roles.USER"));
         this.messageService.informUser("Now impersonating " + userName);
     }
     public boolean hideImpersonate() {
diff --git a/api/applib/src/test/java/org/apache/isis/applib/id/LogicalTypeTest_valueSemantics.java b/api/applib/src/test/java/org/apache/isis/applib/id/LogicalTypeTest_valueSemantics.java
index 0045823..94462f2 100644
--- a/api/applib/src/test/java/org/apache/isis/applib/id/LogicalTypeTest_valueSemantics.java
+++ b/api/applib/src/test/java/org/apache/isis/applib/id/LogicalTypeTest_valueSemantics.java
@@ -24,21 +24,21 @@ import org.apache.isis.applib.SomeDomainClass;
 import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.core.internaltestsupport.contract.ValueTypeContractTestAbstract;
 
-public class LogicalTypeTest_valueSemantics 
+public class LogicalTypeTest_valueSemantics
 extends ValueTypeContractTestAbstract<LogicalType> {
 
     @Override
     protected List<LogicalType> getObjectsWithSameValue() {
         return _Lists.of(
                 LogicalType.fqcn(SomeDomainClass.class),
-                LogicalType.lazy(SomeDomainClass.class, ()->SomeDomainClass.class.getName()));
+                LogicalType.lazy(SomeDomainClass.class, SomeDomainClass.class::getName));
     }
 
     @Override
     protected List<LogicalType> getObjectsWithDifferentValue() {
         return _Lists.of(
                 LogicalType.fqcn(Object.class),
-                LogicalType.lazy(List.class, ()->List.class.getName()));
+                LogicalType.lazy(List.class, List.class::getName));
     }
 
 
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..7169529 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
@@ -2364,6 +2364,23 @@ public class IsisConfiguration {
                 private boolean enable = false;
             }
 
+            private final Logout logout = new Logout();
+            @Data
+            public static class Logout {
+                /**
+                 * Whether the Session (Wicket's Session, usually a wrapper around {@link javax.servlet.http.HttpSession}),
+                 * should be invalidated using <code>invalidateNow</code>.
+                 *
+                 * <p>
+                 *     Normally this is the case because otherwise it wouldn't be possible to logout.  However, some
+                 *     security integrations, for example Keycloak, require the Session to be preserved in order to
+                 *     obtain the credentials to be logged out on a redirect to &quot;/logout&quot;.  In such cases,
+                 *     the integration uses a separate logout to finally invalidate the Wicket session.
+                 * </p>
+                 */
+                private boolean invalidateSessiom = true;
+            }
+
             private final RememberMe rememberMe = new RememberMe();
             @Data
             public static class RememberMe {
@@ -2520,6 +2537,29 @@ public class IsisConfiguration {
             }
 
         }
+
+        private final Vaadin vaadin = new Vaadin();
+        @Data
+        public static class Vaadin {
+
+            private final Logout logout = new Logout();
+            @Data
+            public static class Logout {
+
+                /**
+                 * Whether the VaadinSession (Wicket's Session, usually a wrapper around {@link javax.servlet.http.HttpSession},
+                 * should be closed using <code>close</code>.
+                 *
+                 * <p>
+                 *     Normally this is the case because otherwise it wouldn't be possible to logout.  However, some
+                 *     security integrations, for example Keycloak, require the Session to be preserved in order to
+                 *     obtain the credentials to be logged out on a redirect to &quot;/logout&quot;.  In such cases,
+                 *     the integration uses a separate logout to finally invalidate the Vaadin session.
+                 * </p>
+                 */
+                private boolean invalidateSessiom = true;
+            }
+        }
     }
 
     private final ValueTypes valueTypes = new ValueTypes();
diff --git a/core/pom.xml b/core/pom.xml
index 9c484ad..8f111a8 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -150,6 +150,7 @@
 
 		<jsr305.version>3.0.2</jsr305.version>
 		<junit-platform.version>1.7.2</junit-platform.version>
+		<keycloak.version>14.0.0</keycloak.version>
 
 		<log4jdbc-remix.version>0.2.7</log4jdbc-remix.version>
 
@@ -1097,6 +1098,16 @@
 				<version>${jdo-api.version}</version>
 			</dependency>
 
+<!--
+			<dependency>
+				<groupId>org.keycloak.bom</groupId>
+				<artifactId>keycloak-adapter-bom</artifactId>
+				<version>${keycloak.version}</version>
+				<type>pom</type>
+				<scope>import</scope>
+			</dependency>
+-->
+
 			<dependency>
 				<groupId>net.sf.jopt-simple</groupId>
 				<artifactId>jopt-simple</artifactId>
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/LoginSuccessHandler.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/LoginSuccessHandler.java
index 213a0c7..6804676 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/LoginSuccessHandler.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 LoginSuccessHandler {
 
-})
-public class IsisModuleSecurityKeycloak {
+    /**
+     * Indicates a successful login
+     */
+    void onSuccess();
 
 }
diff --git a/core/security/src/main/java/org/apache/isis/core/security/authentication/logout/LogoutHandler.java b/core/security/src/main/java/org/apache/isis/core/security/authentication/logout/LogoutHandler.java
index 31b511c..0bf0301 100644
--- a/core/security/src/main/java/org/apache/isis/core/security/authentication/logout/LogoutHandler.java
+++ b/core/security/src/main/java/org/apache/isis/core/security/authentication/logout/LogoutHandler.java
@@ -19,15 +19,7 @@
 package org.apache.isis.core.security.authentication.logout;
 
 /**
- *
- * @since Apr 9, 2020
- * TODO we are at early stages of the design, a better idea occurred:
- * actually model the SignIn page as a true ViewModel similar to how we
- * render the home-page; this should allow for the LogoutHandler to be called
- * from the framework more directly and not from within the LogoutMenu's
- * logout action, which is more complicated because, this happens within
- * the context of an IsisInteraction, where we cannot simply purge the
- * current session, when in the middle of an interaction
+ * To allow viewers to close their session when a logout is requested.
  */
 public interface LogoutHandler {
 
diff --git a/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/AuthenticationManager.java b/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/AuthenticationManager.java
index 6336693..37d12df 100644
--- a/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/AuthenticationManager.java
+++ b/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/AuthenticationManager.java
@@ -113,9 +113,7 @@ public class AuthenticationManager {
             }
 
             return null;
-
         });
-
     }
 
     private String getUnusedRandomCode() {
diff --git a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/auth/LogoutHandlerVaa.java b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/auth/LogoutHandlerVaa.java
index ada7cce..71132ff 100644
--- a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/auth/LogoutHandlerVaa.java
+++ b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/auth/LogoutHandlerVaa.java
@@ -18,31 +18,42 @@
  */
 package org.apache.isis.incubator.viewer.vaadin.ui.auth;
 
-import javax.inject.Inject;
-
 import com.vaadin.flow.server.VaadinRequest;
 import com.vaadin.flow.server.VaadinSession;
 
 import org.springframework.stereotype.Service;
 
+import org.apache.isis.core.config.IsisConfiguration;
 import org.apache.isis.core.metamodel.context.MetaModelContext;
 import org.apache.isis.core.security.authentication.logout.LogoutHandler;
 
-import lombok.val;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
+import lombok.val;
 
 @Service
+@RequiredArgsConstructor
 @Log4j2
 public class LogoutHandlerVaa implements LogoutHandler {
 
-    @Inject private MetaModelContext metaModelContext;
+    final MetaModelContext metaModelContext;
+    final IsisConfiguration isisConfiguration;
 
     @Override
     public void logout() {
 
+        if(!isisConfiguration.getViewer().getWicket().getLogout().isInvalidateSessiom()) {
+            // no-op.
+            // instead, we expect that some other mechanism will close the Vaadin session.
+            return;
+        }
+        forceLogout();
+    }
+
+    public void forceLogout() {
         val sessionVaa = VaadinSession.getCurrent();
         if(sessionVaa==null) {
-            return; // ignore if there is no current session
+            return;
         }
 
         AuthSessionStoreUtil.get()
@@ -54,7 +65,6 @@ public class LogoutHandlerVaa implements LogoutHandler {
         });
 
         sessionVaa.close();
-
     }
 
     @Override
diff --git a/mavendeps/webapp/pom.xml b/mavendeps/webapp/pom.xml
index 9d097f3..b304927 100644
--- a/mavendeps/webapp/pom.xml
+++ b/mavendeps/webapp/pom.xml
@@ -115,10 +115,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..a8592e3 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,51 @@
             <artifactId>isis-core-webapp</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.isis.security</groupId>
+            <artifactId>isis-security-spring</artifactId>
+        </dependency>
+
+<!--
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-spring-boot-starter</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>
+
+        <!--
+                <dependency>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-security</artifactId>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>org.springframework.boot</groupId>
+                            <artifactId>spring-boot-starter-logging</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </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..b0b34a2 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,10 +44,161 @@ 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`.
+
+
+
+
+
+[#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_:
++
+[source,bash]
+----
+docker run -p 9090:8080 \
+    -e KEYCLOAK_USER=admin \
+    -e KEYCLOAK_PASSWORD=admin \
+    quay.io/keycloak/keycloak:14.0.0
+----
+
+* login to the Admin console:
++
+image::login-to-admin-console.png[width=300px]
++
+and
++
+image::login-to-admin-console-prompt.png[width=300px]
+
+
+=== Create a realm for simpleapp
+
+* 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]
+
+//=== Create token
+//
+//* send POST request to obtain token:
+//+
+//http://localhost:9090/auth/realms/SpringBootKeycloak/protocol/openid-connect/token
+//+
+//with body:
+//+
+//[source,txt]
+//----
+//client_id:<your_client_id>
+//username:<your_username>
+//password:<your_password>
+//grant_type:password
+//----
+
+
+=== Configure the application as a Keycloak client
+
+* the keycloak config:
++
+[source,properties]
+.config/application.properties
+----
+keycloak.realm=simpleapp
+keycloak.auth-server-url=http://localhost:9090/auth                     #<.>
+keycloak.resource=simpleapp-client                                      #<.>
+keycloak.credentials.secret=ea64432f-ea0a-429e-b4c8-c91778ee74b3        #<.>
+keycloak.use-resource-role-mappings=true
+
+keycloak.securityConstraints[0].authRoles[0]=regular-user               #<.>
+keycloak.securityConstraints[0].securityCollections[0].name=secured
+keycloak.securityConstraints[0].securityCollections[0].patterns[0]=/wicket
+----
+
+<.> URL where keycloak is running
+<.> must match the client name entered in the admin console
+<.> as taken from the credential tab of the simpleapp realm
+<.> role for all users
+
+
+
+
+=== 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]
 
-== Design
+
+== Design Notes
 
 The module configures a filter that expects Keycloak to set three `X-Auth-Xxx` headers:
 
@@ -44,7 +212,9 @@ The `org.apache.isis.viewer.wicket.roles.USER` role -- as required by xref:vw::a
 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.
 
 
-== Walk-through
+== 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..c3637ee 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,13 +18,43 @@
  */
 package org.apache.isis.security.keycloak;
 
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
 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.Authentication;
+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 org.springframework.web.client.RestTemplate;
+
+import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
 
 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.LoginSuccessHandler;
+import org.apache.isis.core.security.authentication.manager.AuthenticationManager;
 import org.apache.isis.core.webapp.IsisModuleCoreWebapp;
+import org.apache.isis.security.keycloak.handler.KeycloakLogoutHandler;
+import org.apache.isis.security.keycloak.services.KeycloakOauth2UserService;
+import org.apache.isis.security.spring.IsisModuleSecuritySpring;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
 
 /**
  * Configuration Bean to support Isis Security using Shiro.
@@ -37,11 +67,99 @@ import org.apache.isis.core.webapp.IsisModuleCoreWebapp;
         IsisModuleCoreRuntimeServices.class,
         IsisModuleCoreWebapp.class,
 
-        // @Service's
-        AuthenticatorKeycloak.class,
-        WebModuleKeycloak.class,
+        // builds on top of Spring
+        IsisModuleSecuritySpring.class,
 
 })
+@EnableWebSecurity
+@ComponentScan
 public class IsisModuleSecurityKeycloak {
 
+    @Bean
+    public WebSecurityConfigurerAdapter webSecurityConfigurer(
+            @Value("${kc.realm}") String realm,
+            KeycloakOauth2UserService keycloakOidcUserService,
+            KeycloakLogoutHandler keycloakLogoutHandler,
+            List<LoginSuccessHandler> loginSuccessHandlers,
+            List<LogoutHandler> logoutHandlers
+            ) {
+        return new WebSecurityConfigurerAdapter() {
+            @Override
+            public void configure(HttpSecurity http) throws Exception {
+
+                val httpSecurityLogoutConfigurer =
+                    http
+                        .sessionManagement()
+                            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
+                        .and()
+
+                        .authorizeRequests()
+                            .anyRequest().authenticated()
+                        .and()
+
+                        // Propagate logouts via /logout to Keycloak
+                        .logout()
+                            .addLogoutHandler(keycloakLogoutHandler)
+                            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
+
+                logoutHandlers.forEach(httpSecurityLogoutConfigurer::addLogoutHandler);
+
+                httpSecurityLogoutConfigurer
+                        .and()
+
+                        // This is the point where OAuth2 login of Spring 5 gets enabled
+                        .oauth2Login()
+                            .defaultSuccessUrl("/wicket", true)
+                            .successHandler(new AuthSuccessHandler(loginSuccessHandlers))
+                            .userInfoEndpoint()
+                            .oidcUserService(keycloakOidcUserService)
+                        .and()
+
+                        .loginPage(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + realm);
+                ;
+            }
+        };
+    }
+
+    @Bean LoginSuccessHandler loginSuccessHandler(final AuthenticationManager authenticationManager) {
+        return new LoginSuccessHandler() {
+            @Override public void onSuccess() {
+
+            }
+        };
+    }
+    @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);
+    }
+
+    @Bean
+    KeycloakLogoutHandler keycloakLogoutHandler() {
+        return new KeycloakLogoutHandler(new RestTemplate());
+    }
+
 }
+
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/KeycloakLogoutHandler.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/handler/KeycloakLogoutHandler.java
new file mode 100644
index 0000000..c7ce849
--- /dev/null
+++ b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/handler/KeycloakLogoutHandler.java
@@ -0,0 +1,54 @@
+package org.apache.isis.security.keycloak.handler;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Propagates logouts to Keycloak.
+ *
+ * <p>
+ * Necessary because Spring Security 5 (currently) doesn't support
+ * end-session-endpoints.
+ * </p>
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class KeycloakLogoutHandler extends SecurityContextLogoutHandler {
+
+    private final RestTemplate restTemplate;
+
+    @Override
+    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
+        super.logout(request, response, authentication);
+
+        if (authentication != null) {
+            propagateLogoutToKeycloak((OidcUser) authentication.getPrincipal());
+        }
+    }
+
+    private void propagateLogoutToKeycloak(OidcUser user) {
+
+        String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
+
+        UriComponentsBuilder builder = UriComponentsBuilder //
+                .fromUriString(endSessionEndpoint) //
+                .queryParam("id_token_hint", user.getIdToken().getTokenValue());
+
+        ResponseEntity<String> logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class);
+        if (logoutResponse.getStatusCode().is2xxSuccessful()) {
+            log.info("Successfulley logged out in Keycloak");
+        } else {
+            log.info("Could not propagate logout to Keycloak");
+        }
+    }
+}
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/security/spring/src/main/java/org/apache/isis/security/spring/webmodule/SpringSecurityFilter.java b/security/spring/src/main/java/org/apache/isis/security/spring/webmodule/SpringSecurityFilter.java
index 9199df0..eef18af 100644
--- a/security/spring/src/main/java/org/apache/isis/security/spring/webmodule/SpringSecurityFilter.java
+++ b/security/spring/src/main/java/org/apache/isis/security/spring/webmodule/SpringSecurityFilter.java
@@ -47,6 +47,7 @@ import lombok.val;
 public class SpringSecurityFilter implements Filter {
 
     @Autowired private InteractionService interactionService;
+    @Inject List<AuthenticationConverter> converters;
 
     @Override
     public void doFilter(
@@ -64,14 +65,13 @@ public class SpringSecurityFilter implements Filter {
         }
 
         UserMemento userMemento = null;
-        for (AuthenticationConverter converter : converters) {
+        for (final AuthenticationConverter converter : converters) {
             try {
                 userMemento = converter.convert(springAuthentication);
                 if(userMemento != null) {
                     break;
                 }
-            } catch(Exception ex) {
-                continue;
+            } catch(final Exception ignored) {
             }
         }
 
@@ -89,5 +89,4 @@ public class SpringSecurityFilter implements Filter {
                 ()->filterChain.doFilter(servletRequest, servletResponse));
     }
 
-    @Inject List<AuthenticationConverter> converters;
 }
diff --git a/security/spring/src/main/java/org/apache/isis/security/spring/webmodule/WebModuleSpringSecurity.java b/security/spring/src/main/java/org/apache/isis/security/spring/webmodule/WebModuleSpringSecurity.java
index ddd0717..44f7d2a 100644
--- a/security/spring/src/main/java/org/apache/isis/security/spring/webmodule/WebModuleSpringSecurity.java
+++ b/security/spring/src/main/java/org/apache/isis/security/spring/webmodule/WebModuleSpringSecurity.java
@@ -24,10 +24,10 @@ 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.annotation.PriorityPrecedence;
 import org.apache.isis.applib.services.inject.ServiceInjector;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.core.webapp.modules.WebModuleAbstract;
diff --git a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/app/logout/LogoutHandlerWkt.java b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/app/logout/LogoutHandlerWkt.java
index 7bc4045..c9ffe00 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/app/logout/LogoutHandlerWkt.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/app/logout/LogoutHandlerWkt.java
@@ -18,33 +18,45 @@
  */
 package org.apache.isis.viewer.wicket.ui.app.logout;
 
-import javax.inject.Inject;
-
 import org.apache.wicket.authroles.authentication.AuthenticatedWebSession;
 import org.apache.wicket.request.cycle.RequestCycle;
 import org.springframework.stereotype.Service;
 
 import org.apache.isis.applib.services.iactnlayer.InteractionLayerTracker;
+import org.apache.isis.core.config.IsisConfiguration;
 import org.apache.isis.core.interaction.session.IsisInteraction;
 import org.apache.isis.core.security.authentication.logout.LogoutHandler;
 
+import lombok.RequiredArgsConstructor;
 import lombok.val;
 
 @Service
+@RequiredArgsConstructor
 public class LogoutHandlerWkt implements LogoutHandler {
 
-    @Inject InteractionLayerTracker iInteractionLayerTracker;
+    final InteractionLayerTracker interactionLayerTracker;
+    final IsisConfiguration isisConfiguration;
 
     @Override
     public void logout() {
 
+        if(!isisConfiguration.getViewer().getWicket().getLogout().isInvalidateSessiom()) {
+            // no-op.
+            // instead, we expect that some other mechanism will invalidate the Wicket session.
+            return;
+        }
+        forceLogout();
+    }
+
+    public void forceLogout() {
+
         val currentWktSession = AuthenticatedWebSession.get();
         if(currentWktSession==null) {
             return;
         }
 
-        if(iInteractionLayerTracker.isInInteraction()) {
-            iInteractionLayerTracker.currentInteraction()
+        if(interactionLayerTracker.isInInteraction()) {
+            interactionLayerTracker.currentInteraction()
             .map(IsisInteraction.class::cast)
             .ifPresent(interaction->
                 interaction.setOnClose(currentWktSession::invalidateNow));
@@ -59,5 +71,4 @@ public class LogoutHandlerWkt implements LogoutHandler {
         return RequestCycle.get()!=null;
     }
 
-
 }
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/Aut.java b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/services/Aut.java
new file mode 100644
index 0000000..87219be
--- /dev/null
+++ b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/services/Aut.java
@@ -0,0 +1,9 @@
+package org.apache.isis.viewer.wicket.viewer.services;
+
+public class Aut /*extends SavedRequestAwareAuthenticationSuccessHandler*/ {
+//    @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
+//        super.onAuthenticationSuccess(request, response, authentication);
+//
+//    }
+}
+
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());
+    }
+
+}