Support for simultaneous base and OAuth authentication (#1785)

* Remove _OidcAuthenticationSuccessHandler_:
  * _OAuth2AuthenticationToken.setDetails_ is made by jwt authentication converter
  * get tenant data (with potentially creating tenant) is done via a filter added in filterChainREST
* _filterChainREST_ uses _Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>>_ as configuration for OAuth. Thus it is not bound with oauth client configuration
* _OidcUserManagementAutoConfiguration_ - now registers (if conditions are met) Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> which covers both - oauth legacy filter from filterChainREST and OidcBearerTokenAuthenticationFilter
* Since oauth clients are not related to hawkBit anymore (since removal of legacy UI) and the proper configuration would be via resource server or whatever, the _OidcUserManagementAutoConfiguration_ is DEPRECATED and for removal
* _UserAuthenticationFilter_ is removed
* Enabled sumiltaneous base and oauth authentication. Still, by default, if OAuth configured http authentication is disabled. However, if OAuth it is configured (via _Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>>)_ and **hawkbit.server.security.allowHttpBasicOnOAuthEnabled** is set to **true** then http auth would be also enabled
* _OidcUserManagementAutoConfiguration_ could be disabled with **hawkbit.server.security.oAuth2OnClientsConfig.enabled=false**

Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-07-26 10:59:15 +03:00
committed by GitHub
parent 6b8917e229
commit 3a34ded4f6
5 changed files with 100 additions and 351 deletions

View File

@@ -9,42 +9,32 @@
*/ */
package org.eclipse.hawkbit.autoconfigure.security; package org.eclipse.hawkbit.autoconfigure.security;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails;
import org.eclipse.hawkbit.im.authentication.UserAuthenticationFilter;
import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition; import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication; import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2Error;
@@ -56,31 +46,24 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.util.Assert;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
* Auto-configuration for OpenID Connect user management. * Auto-configuration for OpenID Connect user management. Based on clients configuration.
* *
* @deprecated hawkBit doesn't use/depend on clients configuration (it was back in time of integrated UI.
*/ */
@Configuration @Configuration
@Conditional(value = ClientsConfiguredCondition.class) @Conditional(value = ClientsConfiguredCondition.class)
@ConditionalOnProperty(prefix = "hawkbit.server.security.oAuth2OnClientsConfig", name = "enabled", havingValue = "true", matchIfMissing = true)
@Deprecated(forRemoval = true)
public class OidcUserManagementAutoConfiguration { public class OidcUserManagementAutoConfiguration {
/**
* @return the OpenID Connect authentication success handler
*/
@Bean
public AuthenticationSuccessHandler oidcAuthenticationSuccessHandler(
final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) {
return new OidcAuthenticationSuccessHandler(systemManagement, systemSecurityContext);
}
/** /**
* @return a jwt authorities extractor which interprets the roles of a user * @return a jwt authorities extractor which interprets the roles of a user
* as their authorities. * as their authorities.
@@ -105,16 +88,43 @@ public class OidcUserManagementAutoConfiguration {
return new JwtAuthoritiesOidcUserService(extractor); return new JwtAuthoritiesOidcUserService(extractor);
} }
/**
* @return an authentication filter for using OAuth2 Bearer Tokens.
*/
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter( Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> oauth2ResourceServerCustomizer(
final JwtAuthoritiesExtractor authoritiesExtractor, final InMemoryClientRegistrationRepository clientRegistrationRepository,
final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) { final JwtAuthoritiesExtractor authoritiesExtractor) {
return new OidcBearerTokenAuthenticationFilter( // Only get the first client registration. Testing against every client could increase the attack vector (?)
authoritiesExtractor, systemManagement, systemSecurityContext); final ClientRegistration clientRegistration =
clientRegistrationRepository.iterator().hasNext() ? clientRegistrationRepository.iterator().next() : null;
Assert.notNull(clientRegistration, "There must be a valid client registration");
return configurer -> configurer.jwt(configurer2 -> {
if (clientRegistration.getProviderDetails().getJwkSetUri() == null) {
configurer2.decoder(JwtDecoders.fromIssuerLocation(clientRegistration.getProviderDetails().getIssuerUri()));
} else {
configurer2.jwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri());
}
configurer2.jwtAuthenticationConverter(jwt -> {
final String defaultTenant = "DEFAULT";
final OidcIdToken idToken = new OidcIdToken(
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims());
final OidcUserInfo userInfo = new OidcUserInfo(jwt.getClaims());
final Set<GrantedAuthority> authorities = authoritiesExtractor.extract(jwt, clientRegistration);
if (authorities.isEmpty()) {
throw new AccessDeniedException("No authorities found in token");
}
final DefaultOidcUser user = new DefaultOidcUser(authorities, idToken, userInfo);
final OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(
user, authorities, clientRegistration.getRegistrationId());
oAuth2AuthenticationToken.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false));
return oAuth2AuthenticationToken;
});
});
} }
/** /**
@@ -166,42 +176,11 @@ public class OidcUserManagementAutoConfiguration {
} }
} }
/**
* OpenID Connect Authentication Success Handler which load tenant data
*/
private static class OidcAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private final SystemManagement systemManagement;
private final SystemSecurityContext systemSecurityContext;
OidcAuthenticationSuccessHandler(
final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) {
this.systemManagement = systemManagement;
this.systemSecurityContext = systemSecurityContext;
}
@Override
public void onAuthenticationSuccess(
final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) throws ServletException, IOException {
if (authentication instanceof AbstractAuthenticationToken token) {
final String defaultTenant = "DEFAULT";
token.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false));
systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant);
}
super.onAuthenticationSuccess(request, response, authentication);
}
}
/** /**
* Utility class to extract authorities out of the jwt. It interprets the user's * Utility class to extract authorities out of the jwt. It interprets the user's
* role as their authorities. * role as their authorities.
*/ */
private record DefaultJwtAuthoritiesExtractor private record DefaultJwtAuthoritiesExtractor(GrantedAuthoritiesMapper authoritiesMapper) implements JwtAuthoritiesExtractor {
(GrantedAuthoritiesMapper authoritiesMapper) implements JwtAuthoritiesExtractor {
private static final OAuth2Error INVALID_REQUEST = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); private static final OAuth2Error INVALID_REQUEST = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
@@ -240,68 +219,4 @@ public class OidcUserManagementAutoConfiguration {
return new LinkedHashSet<>(authorities); return new LinkedHashSet<>(authorities);
} }
} }
static class OidcBearerTokenAuthenticationFilter implements UserAuthenticationFilter, Filter {
private final JwtAuthoritiesExtractor authoritiesExtractor;
private final SystemManagement systemManagement;
private final SystemSecurityContext systemSecurityContext;
private ClientRegistration clientRegistration;
OidcBearerTokenAuthenticationFilter(
final JwtAuthoritiesExtractor authoritiesExtractor,
final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) {
this.authoritiesExtractor = authoritiesExtractor;
this.systemManagement = systemManagement;
this.systemSecurityContext = systemSecurityContext;
}
void setClientRegistration(final ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken jwtAuthenticationToken) {
final String defaultTenant = "DEFAULT";
final Jwt jwt = jwtAuthenticationToken.getToken();
final OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
jwt.getClaims());
final OidcUserInfo userInfo = new OidcUserInfo(jwt.getClaims());
final Set<GrantedAuthority> authorities = authoritiesExtractor.extract(jwt, clientRegistration);
if (authorities.isEmpty()) {
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
final DefaultOidcUser user = new DefaultOidcUser(authorities, idToken, userInfo);
final OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(user, authorities,
clientRegistration.getRegistrationId());
oAuth2AuthenticationToken.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false));
systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant);
SecurityContextHolder.getContext().setAuthentication(oAuth2AuthenticationToken);
}
chain.doFilter(request, response);
}
@Override
public void init(final FilterConfig filterConfig) {
// Nothing to do
}
@Override
public void destroy() {
// Nothing to do
}
}
} }

View File

@@ -9,17 +9,10 @@
*/ */
package org.eclipse.hawkbit.autoconfigure.security; package org.eclipse.hawkbit.autoconfigure.security;
import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -28,7 +21,6 @@ import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
import org.eclipse.hawkbit.ddi.rest.resource.DdiApiConfiguration; import org.eclipse.hawkbit.ddi.rest.resource.DdiApiConfiguration;
import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.im.authentication.UserAuthenticationFilter;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.mgmt.rest.resource.MgmtApiConfiguration; import org.eclipse.hawkbit.mgmt.rest.resource.MgmtApiConfiguration;
import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.ControllerManagement;
@@ -48,13 +40,11 @@ import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware; import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.PropertySource;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
@@ -62,31 +52,23 @@ import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer; import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.firewall.FirewalledRequest; import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall; import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
@@ -104,31 +86,13 @@ import org.springframework.web.cors.CorsConfigurationSource;
public class SecurityManagedConfiguration { public class SecurityManagedConfiguration {
private static final int DOS_FILTER_ORDER = -200; private static final int DOS_FILTER_ORDER = -200;
public static final String ANONYMOUS_CONTROLLER_SECURITY_ENABLED_SHOULD_ONLY_BE_USED_FOR_DEVELOPMENT_PURPOSES = """
******************
** Anonymous controller security enabled, should only be used for development purposes **
******************""";
/** /**
* @return the {@link UserAuthenticationFilter} to include into the hawkBit security configuration. * Security configuration for the hawkBit server DDI interface.
* @throws Exception lazy bean exception maybe if the authentication manager cannot be instantiated
*/
@Bean
@ConditionalOnMissingBean
// Exception squid:S00112 - Is aspectJ proxy
@SuppressWarnings({ "squid:S00112" })
UserAuthenticationFilter userAuthenticationFilter(final AuthenticationConfiguration configuration)
throws Exception {
return new UserAuthenticationFilterBasicAuth(configuration.getAuthenticationManager());
}
private static final class UserAuthenticationFilterBasicAuth extends BasicAuthenticationFilter
implements UserAuthenticationFilter {
private UserAuthenticationFilterBasicAuth(final AuthenticationManager authenticationManager) {
super(authenticationManager);
}
}
/**
* {@link WebSecurityConfigurer} for the hawkBit server DDI interface.
*/ */
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -165,18 +129,14 @@ public class SecurityManagedConfiguration {
} }
/** /**
* Filter to protect the hawkBit server DDI interface against to many * Filter to protect the hawkBit server DDI interface against too many requests.
* requests.
* *
* @param securityProperties * @param securityProperties for filter configuration
* for filter configuration * @return the spring filter registration bean for registering a denial of service protection filter in the filter chain
*
* @return the spring filter registration bean for registering a denial
* of service protection filter in the filter chain
*/ */
@Bean @Bean
@ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true) @ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true)
public FilterRegistrationBean<DosFilter> dosFilterDDI(final HawkbitSecurityProperties securityProperties) { protected FilterRegistrationBean<DosFilter> dosFilterDDI(final HawkbitSecurityProperties securityProperties) {
final FilterRegistrationBean<DosFilter> filterRegBean = final FilterRegistrationBean<DosFilter> filterRegBean =
dosFilter(List.of(DDI_ANT_MATCHERS), dosFilter(List.of(DDI_ANT_MATCHERS),
securityProperties.getDos().getFilter(), securityProperties.getClients()); securityProperties.getDos().getFilter(), securityProperties.getClients());
@@ -201,11 +161,7 @@ public class SecurityManagedConfiguration {
final ControllerTenantAwareAuthenticationDetailsSource authenticationDetailsSource = new ControllerTenantAwareAuthenticationDetailsSource(); final ControllerTenantAwareAuthenticationDetailsSource authenticationDetailsSource = new ControllerTenantAwareAuthenticationDetailsSource();
if (ddiSecurityConfiguration.getAuthentication().getAnonymous().isEnabled()) { if (ddiSecurityConfiguration.getAuthentication().getAnonymous().isEnabled()) {
log.info( log.warn(ANONYMOUS_CONTROLLER_SECURITY_ENABLED_SHOULD_ONLY_BE_USED_FOR_DEVELOPMENT_PURPOSES);
"""
******************
** Anonymous controller security enabled, should only be used for developing purposes **
******************""");
final AnonymousAuthenticationFilter anonymousFilter = new AnonymousAuthenticationFilter( final AnonymousAuthenticationFilter anonymousFilter = new AnonymousAuthenticationFilter(
"controllerAnonymousFilter", "anonymous", "controllerAnonymousFilter", "anonymous",
@@ -253,8 +209,7 @@ public class SecurityManagedConfiguration {
} }
/** /**
* {@link WebSecurityConfigurer} for the hawkBit server DDI download * Security configuration for the hawkBit server DDI download interface.
* interface.
*/ */
@Configuration @Configuration
@ConditionalOnClass(DdiApiConfiguration.class) @ConditionalOnClass(DdiApiConfiguration.class)
@@ -284,14 +239,10 @@ public class SecurityManagedConfiguration {
} }
/** /**
* Filter to protect the hawkBit server DDI download interface against * Filter to protect the hawkBit server DDI download interface against too many requests.
* to many requests.
* *
* @param securityProperties * @param securityProperties for filter configuration
* for filter configuration * @return the spring filter registration bean for registering a denial of service protection filter in the filter chain
*
* @return the spring filter registration bean for registering a denial
* of service protection filter in the filter chain
*/ */
@Bean @Bean
@ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true) @ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true)
@@ -320,11 +271,7 @@ public class SecurityManagedConfiguration {
final ControllerTenantAwareAuthenticationDetailsSource authenticationDetailsSource = new ControllerTenantAwareAuthenticationDetailsSource(); final ControllerTenantAwareAuthenticationDetailsSource authenticationDetailsSource = new ControllerTenantAwareAuthenticationDetailsSource();
if (ddiSecurityConfiguration.getAuthentication().getAnonymous().isEnabled()) { if (ddiSecurityConfiguration.getAuthentication().getAnonymous().isEnabled()) {
log.info( log.warn(ANONYMOUS_CONTROLLER_SECURITY_ENABLED_SHOULD_ONLY_BE_USED_FOR_DEVELOPMENT_PURPOSES);
"""
******************
** Anonymous controller security enabled, should only be used for developing purposes **
******************""");
final AnonymousAuthenticationFilter anonymousFilter = new AnonymousAuthenticationFilter( final AnonymousAuthenticationFilter anonymousFilter = new AnonymousAuthenticationFilter(
"controllerAnonymousFilter", "anonymous", "controllerAnonymousFilter", "anonymous",
@@ -377,14 +324,10 @@ public class SecurityManagedConfiguration {
} }
/** /**
* Filter to protect the hawkBit server system management interface against * Filter to protect the hawkBit server system management interface against too many requests.
* to many requests.
* *
* @param securityProperties * @param securityProperties for filter configuration
* for filter configuration * @return the spring filter registration bean for registering a denial of service protection filter in the filter chain
*
* @return the spring filter registration bean for registering a denial of
* service protection filter in the filter chain
*/ */
@Bean @Bean
@ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true) @ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true)
@@ -411,7 +354,7 @@ public class SecurityManagedConfiguration {
} }
/** /**
* A Websecurity config to handle and filter the download ids. * Security config to handle and filter the download ids.
*/ */
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -435,7 +378,7 @@ public class SecurityManagedConfiguration {
.authorizeHttpRequests(armrRepository -> armrRepository.anyRequest().authenticated()) .authorizeHttpRequests(armrRepository -> armrRepository.anyRequest().authenticated())
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.anonymous(AbstractHttpConfigurer::disable) .anonymous(AbstractHttpConfigurer::disable)
.addFilterBefore(downloadIdAuthenticationFilter, FilterSecurityInterceptor.class) .addFilterBefore(downloadIdAuthenticationFilter, AuthorizationFilter.class)
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build(); return http.build();
@@ -460,15 +403,17 @@ public class SecurityManagedConfiguration {
* Filter to protect the hawkBit server Management interface against to * Filter to protect the hawkBit server Management interface against to
* many requests. * many requests.
* *
* @return the spring filter registration bean for registering a denial * @return the spring filter registration bean for registering a denial of service protection filter in the filter chain
* of service protection filter in the filter chain
*/ */
@Bean @Bean
@ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true) @ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true)
public FilterRegistrationBean<DosFilter> dosFilterREST() { public FilterRegistrationBean<DosFilter> dosFilterREST() {
final FilterRegistrationBean<DosFilter> filterRegBean = dosFilter(null, final FilterRegistrationBean<DosFilter> filterRegBean = dosFilter(null,
securityProperties.getDos().getFilter(), securityProperties.getClients()); securityProperties.getDos().getFilter(), securityProperties.getClients());
filterRegBean.setUrlPatterns(List.of("/rest/*", "/api/*")); filterRegBean.setUrlPatterns(List.of(
MgmtRestConstants.BASE_REST_MAPPING + "/*",
MgmtRestConstants.BASE_SYSTEM_MAPPING + "/admin/*",
MgmtRestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING_BASE + "/*"));
filterRegBean.setOrder(DOS_FILTER_ORDER); filterRegBean.setOrder(DOS_FILTER_ORDER);
filterRegBean.setName("dosMgmtFilter"); filterRegBean.setName("dosMgmtFilter");
@@ -479,25 +424,22 @@ public class SecurityManagedConfiguration {
@Order(350) @Order(350)
SecurityFilterChain filterChainREST( SecurityFilterChain filterChainREST(
final HttpSecurity http, final HttpSecurity http,
@Lazy
final UserAuthenticationFilter userAuthenticationFilter,
@Autowired(required = false) @Autowired(required = false)
final OidcUserManagementAutoConfiguration.OidcBearerTokenAuthenticationFilter final Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> oauth2ResourceServerCustomizer,
oidcBearerTokenAuthenticationFilter,
@Autowired(required = false)
final InMemoryClientRegistrationRepository clientRegistrationRepository,
final SystemManagement systemManagement, final SystemManagement systemManagement,
final SystemSecurityContext systemSecurityContext) final SystemSecurityContext systemSecurityContext) throws Exception {
throws Exception {
http http
.securityMatcher("/rest/**", MgmtRestConstants.BASE_SYSTEM_MAPPING + "/admin/**") .securityMatcher(MgmtRestConstants.BASE_REST_MAPPING + "/**", MgmtRestConstants.BASE_SYSTEM_MAPPING + "/admin/**")
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(amrmRegistry -> .authorizeHttpRequests(amrmRegistry ->
amrmRegistry amrmRegistry
.requestMatchers(MgmtRestConstants.BASE_SYSTEM_MAPPING + "/admin/**") .requestMatchers(MgmtRestConstants.BASE_SYSTEM_MAPPING + "/admin/**")
.hasAnyAuthority(SpPermission.SYSTEM_ADMIN) .hasAnyAuthority(SpPermission.SYSTEM_ADMIN)
.anyRequest() .anyRequest()
.authenticated()) .authenticated())
.anonymous(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(Customizer.withDefaults())
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterAfter( .addFilterAfter(
// Servlet filter to create metadata after successful authentication over RESTful. // Servlet filter to create metadata after successful authentication over RESTful.
(request, response, chain) -> { (request, response, chain) -> {
@@ -507,9 +449,7 @@ public class SecurityManagedConfiguration {
} }
chain.doFilter(request, response); chain.doFilter(request, response);
}, },
SessionManagementFilter.class) SessionManagementFilter.class);
.anonymous(AbstractHttpConfigurer::disable)
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
if (securityProperties.getCors().isEnabled()) { if (securityProperties.getCors().isEnabled()) {
http.cors(configurer -> configurer.configurationSource(corsConfigurationSource())); http.cors(configurer -> configurer.configurationSource(corsConfigurationSource()));
@@ -519,64 +459,21 @@ public class SecurityManagedConfiguration {
http.requiresChannel(crmRegistry -> crmRegistry.anyRequest().requiresSecure()); http.requiresChannel(crmRegistry -> crmRegistry.anyRequest().requiresSecure());
} }
if (oidcBearerTokenAuthenticationFilter != null) { if (oauth2ResourceServerCustomizer != null) {
// Only get the first client registration. Testing against every http.oauth2ResourceServer(oauth2ResourceServerCustomizer);
// client could increase the }
// attack vector if (oauth2ResourceServerCustomizer == null || securityProperties.isAllowHttpBasicOnOAuthEnabled()) {
final ClientRegistration clientRegistration = clientRegistrationRepository != null http.httpBasic(configurer -> {
&& clientRegistrationRepository.iterator().hasNext() final BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint();
? clientRegistrationRepository.iterator().next() basicAuthEntryPoint.setRealmName(securityProperties.getBasicRealm());
: null; configurer.authenticationEntryPoint(basicAuthEntryPoint);
});
Assert.notNull(clientRegistration, "There must be a valid client registration");
http.oauth2ResourceServer(configurer -> configurer.jwt(configurer2 -> {
if (clientRegistration.getProviderDetails().getJwkSetUri() == null) {
configurer2.decoder(JwtDecoders.fromIssuerLocation(clientRegistration.getProviderDetails().getIssuerUri()));
} else {
configurer2.jwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri());
}
}));
oidcBearerTokenAuthenticationFilter.setClientRegistration(clientRegistration);
http.addFilterAfter(oidcBearerTokenAuthenticationFilter, BearerTokenAuthenticationFilter.class);
} else {
final BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint();
basicAuthEntryPoint.setRealmName(securityProperties.getBasicRealm());
http.addFilterBefore(new Filter() {
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
userAuthenticationFilter.init(filterConfig);
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response,
final FilterChain chain) throws IOException, ServletException {
userAuthenticationFilter.doFilter(request, response, chain);
}
@Override
public void destroy() {
userAuthenticationFilter.destroy();
}
}, RequestHeaderAuthenticationFilter.class);
http
.httpBasic(Customizer.withDefaults())
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(basicAuthEntryPoint));
} }
return http.build(); return http.build();
} }
@Bean private CorsConfigurationSource corsConfigurationSource() {
@ConditionalOnProperty(prefix = "hawkbit.server.security.cors", name = "enabled")
CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = corsConfiguration();
return request -> configuration;
}
private CorsConfiguration corsConfiguration() {
final CorsConfiguration corsConfiguration = new CorsConfiguration(); final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(securityProperties.getCors().getAllowedOrigins()); corsConfiguration.setAllowedOrigins(securityProperties.getCors().getAllowedOrigins());
@@ -584,8 +481,7 @@ public class SecurityManagedConfiguration {
corsConfiguration.setAllowedHeaders(securityProperties.getCors().getAllowedHeaders()); corsConfiguration.setAllowedHeaders(securityProperties.getCors().getAllowedHeaders());
corsConfiguration.setAllowedMethods(securityProperties.getCors().getAllowedMethods()); corsConfiguration.setAllowedMethods(securityProperties.getCors().getAllowedMethods());
corsConfiguration.setExposedHeaders(securityProperties.getCors().getExposedHeaders()); corsConfiguration.setExposedHeaders(securityProperties.getCors().getExposedHeaders());
return request -> corsConfiguration;
return corsConfiguration;
} }
} }

View File

@@ -13,5 +13,5 @@ hawkbit.server.ddi.security.authentication.header.enabled=false
hawkbit.server.ddi.security.authentication.header.authority= hawkbit.server.ddi.security.authentication.header.authority=
hawkbit.server.ddi.security.authentication.targettoken.enabled=false hawkbit.server.ddi.security.authentication.targettoken.enabled=false
hawkbit.server.ddi.security.authentication.gatewaytoken.enabled=false hawkbit.server.ddi.security.authentication.gatewaytoken.enabled=false
hawkbit.server.download.anonymous.enabled=false hawkbit.server.ddi.security.authentication.gatewaytoken.key=
hawkbit.server.ddi.security.authentication.gatewaytoken.key= hawkbit.server.download.anonymous.enabled=false

View File

@@ -1,66 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.im.authentication;
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
/**
* Filter to integrate into the SP security filter-chain. The filter is called
* in any remote call through HTTP except the SP login screen. E.g. using the SP
* REST-API. To authenticate user e.g. using Basic-Authentication implement the
* {@link #doFilter(jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse, jakarta.servlet.FilterChain)}
* method.
*
*
*
*/
public interface UserAuthenticationFilter {
/**
* @see Filter#init(FilterConfig)
*
* @param filterConfig
* the filter config
*/
void init(FilterConfig filterConfig) throws ServletException;
/**
* @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
*
* @param request
* the servlet request
* @param response
* the servlet response
* @param chain
* the filterchain
* @throws IOException
* cannot read from request
* @throws ServletException
* servlet exception
*/
// this declaration of multiple checked exception is necessary so it's
// aligned with the servlet API.
@SuppressWarnings("squid:S1160")
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
/**
* @see Filter#destroy()
*/
void destroy();
}

View File

@@ -49,6 +49,10 @@ public class HawkbitSecurityProperties {
* Basic authentication realm, see https://tools.ietf.org/html/rfc2617#page-3 . * Basic authentication realm, see https://tools.ietf.org/html/rfc2617#page-3 .
*/ */
private String basicRealm = "hawkBit"; private String basicRealm = "hawkBit";
/**
* If to allow http authentication when there is OAuth2 authentication enabled.
*/
private boolean allowHttpBasicOnOAuthEnabled = false;
/** /**
* Security configuration related to CORS. * Security configuration related to CORS.