From c421125eb4fa3d2b4c97985eae8aa2568f8a1a12 Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Tue, 1 Apr 2025 20:11:08 +0200
Subject: [PATCH] Fix Shibboleth/Tomcat providing a principal with a blank name
 in the unauthenticated case

---
 .../se/su/dsv/oauth2/AuthorizationServer.java | 24 +--------------
 ...ibbolethAuthenticatedProcessingFilter.java | 29 +++++++++++++++++++
 .../shibboleth/ShibbolethConfigurer.java      |  2 +-
 3 files changed, 31 insertions(+), 24 deletions(-)
 create mode 100644 src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethAuthenticatedProcessingFilter.java

diff --git a/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java b/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java
index 478e989..c123b0f 100644
--- a/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java
+++ b/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java
@@ -9,7 +9,6 @@ import org.springframework.context.annotation.Profile;
 import org.springframework.core.annotation.Order;
 import org.springframework.http.MediaType;
 import org.springframework.security.config.Customizer;
-import org.springframework.security.config.ObjectPostProcessor;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.http.SessionCreationPolicy;
@@ -24,10 +23,8 @@ import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.access.intercept.AuthorizationFilter;
 import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
 import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter;
-import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
 import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
 import se.su.dsv.oauth2.shibboleth.Entitlement;
-import se.su.dsv.oauth2.shibboleth.ShibbolethAuthenticationDetailsSource;
 import se.su.dsv.oauth2.shibboleth.ShibbolethConfigurer;
 import se.su.dsv.oauth2.shibboleth.ShibbolethTokenPopulator;
 import se.su.dsv.oauth2.staging.StagingSecurityConfigurer;
@@ -140,30 +137,11 @@ public class AuthorizationServer extends SpringBootServletInitializer {
                 .accessDeniedPage("/forbidden")
                 .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")));
 
-        http.jee(jee -> jee
-                .withObjectPostProcessor(makeShibbolethAware()));
+        http.with(new ShibbolethConfigurer(), Customizer.withDefaults());
 
         return http.build();
     }
 
-    private static ObjectPostProcessor<J2eePreAuthenticatedProcessingFilter> makeShibbolethAware()
-    {
-        return new ObjectPostProcessor<>() {
-            @Override
-            public <O extends J2eePreAuthenticatedProcessingFilter> O postProcess(final O object) {
-                // Using a custom authentication details source to extract the Shibboleth attributes
-                // and convert them to the relevant Spring Security objects.
-                object.setAuthenticationDetailsSource(new ShibbolethAuthenticationDetailsSource());
-
-                // Prevent session creation
-                // It can cause conflicts when running on the same host as an embedded docker container
-                // as it overwrites the session cookie (it does not factor in port)
-                object.setSecurityContextRepository(new RequestAttributeSecurityContextRepository());
-                return object;
-            }
-        };
-    }
-
     @Bean
     public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
         return new ShibbolethTokenPopulator();
diff --git a/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethAuthenticatedProcessingFilter.java b/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethAuthenticatedProcessingFilter.java
new file mode 100644
index 0000000..b642843
--- /dev/null
+++ b/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethAuthenticatedProcessingFilter.java
@@ -0,0 +1,29 @@
+package se.su.dsv.oauth2.shibboleth;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter;
+
+import java.security.Principal;
+
+/// Can't use the built-in [J2eePreAuthenticatedProcessingFilter] because Tomcat
+/// unconditionally provides a [HttpServletRequest#getUserPrincipal()] with a
+/// blank [Principal#getName()] that causes the authentication process to fail.
+class ShibbolethAuthenticatedProcessingFilter extends J2eePreAuthenticatedProcessingFilter {
+    @Override
+    protected Object getPreAuthenticatedPrincipal(final HttpServletRequest httpRequest) {
+        Principal userPrincipal = httpRequest.getUserPrincipal();
+        if (userPrincipal == null) {
+            return null;
+        }
+
+        // Tomcat provides a blank Principal name when the user is not authenticated.
+        String principalName = userPrincipal.getName();
+        if (principalName == null || principalName.isBlank()) {
+            return null;
+        }
+
+        this.logger.debug(LogMessage.format("PreAuthenticated J2EE principal: %s", principalName));
+        return principalName;
+    }
+}
diff --git a/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethConfigurer.java b/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethConfigurer.java
index 1aa0252..5a9a511 100644
--- a/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethConfigurer.java
+++ b/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethConfigurer.java
@@ -20,7 +20,7 @@ public class ShibbolethConfigurer extends AbstractHttpConfigurer<ShibbolethConfi
 
     @Override
     public void configure(final HttpSecurity http) {
-        J2eePreAuthenticatedProcessingFilter filter = new J2eePreAuthenticatedProcessingFilter();
+        J2eePreAuthenticatedProcessingFilter filter = new ShibbolethAuthenticatedProcessingFilter();
         filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
         filter.setAuthenticationDetailsSource(new ShibbolethAuthenticationDetailsSource());
         filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());