Skip to content

Commit

Permalink
feat(springboot): Use more flexible Spring Security + JAAS integration (
Browse files Browse the repository at this point in the history
fixes hawtio#3395) (hawtio#3457)

Signed-off-by: Grzegorz Grzybek <gr.grzybek@gmail.com>
  • Loading branch information
grgrzybek authored May 23, 2024
1 parent e32a8e7 commit 406c681
Show file tree
Hide file tree
Showing 22 changed files with 466 additions and 97 deletions.
5 changes: 5 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
<artifactId>hawtio-springboot-keycloak</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-springboot-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-system</artifactId>
Expand Down
4 changes: 4 additions & 0 deletions examples/springboot-security/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
<groupId>io.hawt</groupId>
<artifactId>hawtio-springboot</artifactId>
</dependency>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-springboot-security</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.hawt.example.spring.boot;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Properties;
import java.util.function.Supplier;

import jakarta.servlet.FilterChain;
Expand All @@ -10,8 +13,13 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
Expand Down Expand Up @@ -45,6 +53,35 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.build();
}

@Bean
public UserDetailsService userDetailsService(ResourceLoader loader) {
Resource users = loader.getResource("classpath:/users.properties");
try (InputStream is = users.getInputStream()) {
Properties properties = new Properties();
properties.load(is);
InMemoryUserDetailsManager db = new InMemoryUserDetailsManager();
for (String userName : properties.stringPropertyNames()) {
String credentials = properties.getProperty(userName);
String[] credentialsData = credentials.split("\\s*,\\s*");
if (credentialsData.length == 0) {
continue;
}
User.UserBuilder builder = User.withUsername(userName);
builder.password(credentialsData[0].startsWith("{")
? credentialsData[0] : "{noop}" + credentialsData[0]);
if (credentialsData.length > 1) {
builder.roles(Arrays.copyOfRange(credentialsData, 1, credentialsData.length));
}
db.createUser(builder.build());
}
return db;
} catch (IOException e) {
return new InMemoryUserDetailsManager(User.builder()
.username("hawtio").password("hawtio")
.roles("admin", "viewer").build());
}
}

static class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,18 @@ logging.level.org.springframework=WARN
logging.level.org.springframework.boot.autoconfigure.security=INFO
logging.level.io.undertow=WARN

# When Hawtio authentication is disabled, Spring Security is still used when configured
# with @org.springframework.security.config.annotation.web.configuration.EnableWebSecurity annotation
# but Hawtio will not be aware of this. So keep authentication enabled when using @EnableWebSecurity
hawtio.authenticationEnabled=true

# Spring Security
spring.security.user.name=hawtio
spring.security.user.password=hawtio
spring.security.user.roles=admin,viewer
# https://docs.spring.io/spring-security/reference/6.2/servlet/authentication/passwords/in-memory.html#servlet-authentication-inmemory
# We can use single user for org.springframework.security.core.userdetails.UserDetailsService using properties
# with "spring.security.user." prefix.
# But it's more flexible to configure a @Bean method returning UserDetailsService implementation, like:
# - org.springframework.security.provisioning.InMemoryUserDetailsManager
# - org.springframework.security.provisioning.JdbcUserDetailsManager
#spring.security.user.name=hawtio
#spring.security.user.password=hawtio
#spring.security.user.roles=admin,viewer
13 changes: 13 additions & 0 deletions examples/springboot-security/src/main/resources/users.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# a user registry definition file processed by
# io.hawt.example.spring.boot.SecurityConfig.userDetailsService() @Bean method
#
# to provide more sophisticated (encryption) registry of users, please define different @Bean method that
# creates org.springframework.security.core.userdetails.UserDetailsService bean

# the syntax is:
# user = password, role1, role2, ...
# passwords may be encoded.
# See org.springframework.security.crypto.factory.PasswordEncoderFactories.createDelegatingPasswordEncoder()
# (85e144aca83c28cef9e280f0e8f2ac0f49c60d47 is SHA1(hawtio))
hawtio = {SHA-1}85e144aca83c28cef9e280f0e8f2ac0f49c60d47, admin, viewer
viewer = viewer, viewer
16 changes: 14 additions & 2 deletions hawtio-system/src/main/java/io/hawt/HawtioContextListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Objects;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;

Expand Down Expand Up @@ -60,14 +61,13 @@ public void contextInitialized(ServletContextEvent servletContextEvent) {
}
servletContextEvent.getServletContext().setAttribute(ConfigManager.CONFIG_MANAGER, configManager);

// configure OIDC here, because it's needed later both in CSP filter and AuthConfigurationServlet
AuthenticationConfiguration authConfig
= AuthenticationConfiguration.getConfiguration(servletContextEvent.getServletContext());
if (!authConfig.isEnabled()) {
return;
}

authConfig.configureOidc();
configureAuthenticationProviders(servletContextEvent.getServletContext(), authConfig);
}

public void contextDestroyed(ServletContextEvent servletContextEvent) {
Expand All @@ -84,6 +84,18 @@ public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}

/**
* Extension method that configures authentication providers. hawtio-springboot may configure
* Spring Security if needed. This method is not called if authentication is disabled in Hawtio.
*
* @param servletContext
* @param authConfig
*/
protected void configureAuthenticationProviders(ServletContext servletContext, AuthenticationConfiguration authConfig) {
// configure OIDC here, because it's needed later both in CSP filter and AuthConfigurationServlet
authConfig.configureOidc();
}

protected RuntimeException createServletException(Exception e) {
return new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public class Authenticator {
private String username;
private String password;
private X509Certificate[] certificates;
private Principal requestPrincipal;

/**
* Explicit username/password authenticator when authenticating users from login page.
Expand Down Expand Up @@ -83,6 +84,9 @@ public Authenticator(HttpServletRequest request, AuthenticationConfiguration aut
if (certificates != null) {
this.certificates = (X509Certificate[]) certificates;
}

// existing auth - can be configured by Spring Security
this.requestPrincipal = request.getUserPrincipal();
}

/**
Expand Down Expand Up @@ -124,7 +128,8 @@ public boolean isUsernamePasswordSet() {
}

public boolean hasNoCredentials() {
return (!isUsernamePasswordSet() || username.equals("public")) && certificates == null;
return (!isUsernamePasswordSet() || username.equals("public")) && certificates == null
&& requestPrincipal == null;
}

public static void logout(AuthenticationConfiguration authConfiguration, Subject subject) {
Expand Down
3 changes: 1 addition & 2 deletions hawtio-system/src/main/java/io/hawt/web/ServletHelpers.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ public static InputStream loadFile(String path) {
}
return new URL(path).openStream();
} catch (Exception e) {
LOG.warn("Couldn't find file on location: {}", path);
LOG.debug("Couldn't find file", e);
LOG.debug("Couldn't find file: {}", path);
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -179,6 +181,7 @@ public class AuthenticationConfiguration {
private final String realm;
private final String roles;
private String rolePrincipalClasses;
private Class<? extends Principal> defaultRolePrincipalClass;
private final boolean noCredentials401;
private final boolean keycloakEnabled;
private Configuration configuration;
Expand All @@ -188,6 +191,12 @@ public class AuthenticationConfiguration {
// this.configuration field
private OidcConfiguration oidcConfiguration;

/**
* Flag indicating that Spring Security is not only available, but proper {@code SecurityFilterChain} was
* configured in web application context.
*/
private boolean springSecurityEnabled = false;

private AuthenticationConfiguration(ServletContext servletContext) {
ConfigManager config = (ConfigManager) servletContext.getAttribute(ConfigManager.CONFIG_MANAGER);
if (config == null) {
Expand All @@ -210,6 +219,7 @@ private AuthenticationConfiguration(ServletContext servletContext) {
this.roles = config.get(ROLES).orElse(DEFAULT_KARAF_ROLES);
String defaultRolePrincipalClasses = isKaraf() ? DEFAULT_KARAF_ROLE_PRINCIPAL_CLASSES : "";
this.rolePrincipalClasses = config.get(ROLE_PRINCIPAL_CLASSES).orElse(defaultRolePrincipalClasses);
this.defaultRolePrincipalClass = determineDefaultRolePrincipalClass(this.rolePrincipalClasses);
this.noCredentials401 = config.getBoolean(NO_CREDENTIALS_401, false);
this.keycloakEnabled = this.enabled && config.getBoolean(KEYCLOAK_ENABLED, false);

Expand Down Expand Up @@ -295,6 +305,10 @@ public void setRolePrincipalClasses(String rolePrincipalClasses) {
this.rolePrincipalClasses = rolePrincipalClasses;
}

public Class<? extends Principal> getDefaultRolePrincipalClass() {
return defaultRolePrincipalClass;
}

public Configuration getConfiguration() {
return configuration;
}
Expand All @@ -311,15 +325,12 @@ public boolean isOidcEnabled() {
return oidcConfiguration != null && oidcConfiguration.isEnabled();
}

public static boolean isSpringSecurityEnabled() {
try {
Class.forName("org.springframework.security.core.SpringSecurityCoreVersion");
LOG.trace("Spring Security enabled");
return true;
} catch (ClassNotFoundException e) {
LOG.trace("Spring Security not found");
return false;
}
public void setSpringSecurityEnabled(boolean springSecurityEnabled) {
this.springSecurityEnabled = springSecurityEnabled;
}

public boolean isSpringSecurityEnabled() {
return springSecurityEnabled;
}

public boolean isExternalAuthenticationEnabled() {
Expand All @@ -342,14 +353,16 @@ public void configureOidc() {
oidcConfigFile = defaultOidcConfigLocation();
}

LOG.info("Looking for OIDC configuration file in: {}", oidcConfigFile);

InputStream is = ServletHelpers.loadFile(oidcConfigFile);
if (is != null) {
LOG.info("Will load OIDC config from location: {}", oidcConfigFile);
LOG.info("Reading OIDC configuration.");
Properties props = new Properties();
try {
props.load(is);
this.oidcConfiguration = new OidcConfiguration(props);
this.oidcConfiguration.setRolePrincipalClasses(this.rolePrincipalClasses);
this.oidcConfiguration.setRolePrincipalClass(defaultRolePrincipalClass);
if (this.oidcConfiguration.isEnabled()) {
this.configuration = this.oidcConfiguration;
}
Expand Down Expand Up @@ -400,6 +413,59 @@ public OidcConfiguration getOidcConfiguration() {
return oidcConfiguration;
}

/**
* Parses Hawtio configuration option for role principal classes (comma-separated list of class names)
* and returns first that's available and has proper (1-arg String) constructor.
*
* @param rolePrincipalClasses
* @return
*/
private Class<? extends Principal> determineDefaultRolePrincipalClass(String rolePrincipalClasses) {
if (rolePrincipalClasses == null || rolePrincipalClasses.isBlank()) {
return null;
} else {
String[] roleClasses = rolePrincipalClasses.split("\\s*,\\s*");
Class<? extends Principal> roleClass = null;

// let's load first available class - needs 1-arg String constructor
for (String classCandidate : roleClasses) {
Class<? extends Principal> clz = tryLoadClass(classCandidate, Principal.class);
if (clz != null) {
try {
Constructor<?> ctr = clz.getConstructor(String.class);
roleClass = clz;
} catch (NoSuchMethodException e) {
LOG.warn("Can't use role principal class {}: {}", classCandidate, e.getMessage());
}
}
}

return roleClass;
}
}

private <T> Class<T> tryLoadClass(String roleClass, Class<T> clazz) {
try {
Class<?> cls = getClass().getClassLoader().loadClass(roleClass);
if (clazz.isAssignableFrom(cls)) {
return clazz;
} else {
LOG.warn("Class {} doesn't implement {}", cls, clazz);
}
} catch (ClassNotFoundException ignored) {
}
try {
Class<?> cls = Thread.currentThread().getContextClassLoader().loadClass(roleClass);
if (clazz.isAssignableFrom(cls)) {
return clazz;
} else {
LOG.warn("Class {} doesn't implement {}", cls, clazz);
}
} catch (ClassNotFoundException ignored) {
}
return null;
}

@Override
public String toString() {
return "AuthenticationConfiguration[" +
Expand Down
Loading

0 comments on commit 406c681

Please sign in to comment.