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 "/logout". 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 "/logout". 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());
+ }
+
+}