How to override SecurityFilterChain in Spring Boot context? - spring

I am facing the issue which is not obvious to resolve just by reading the documentation. While migrating to Spring Boot v2.7.4 / Spring Security v5.7.3 I have refactored the configuration not to extend WebSecurityConfigurerAdapter and to look like below:
#Configuration
#EnableWebSecurity
public class CustomSecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.
csrf().disable().
logout().disable().
authorizeRequests().anyRequest().permitAll();
return http.build();
}
}
The above method is called, however has no effect as SecurityFilterChain instance created by OAuth2SecurityFilterChainConfiguration is used instead (I see that from debug by inspecting the list of filter in the stack that has e.g. LogoutFilter which should be disabled by above configuration). Debug log:
2022-10-20 15:49:48.790 [main] o.s.b.a.s.DefaultWebSecurityCondition : Condition DefaultWebSecurityCondition on org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration$OAuth2SecurityFilterChainConfiguration matched due to AllNestedConditions 2 matched 0 did not; NestedCondition on DefaultWebSecurityCondition.Beans #ConditionalOnMissingBean (types: org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter,org.springframework.security.web.SecurityFilterChain; SearchStrategy: all) did not find any beans; NestedCondition on DefaultWebSecurityCondition.Classes #ConditionalOnClass found required classes 'org.springframework.security.web.SecurityFilterChain', 'org.springframework.security.config.annotation.web.builders.HttpSecurity'
2022-10-20 15:49:48.791 [main] a.ConfigurationClassBeanDefinitionReader : Registered bean definition for imported class 'org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration$OAuth2SecurityFilterChainConfiguration'
2022-10-20 15:49:48.792 [main] o.s.b.a.condition.OnBeanCondition : Condition OnBeanCondition on org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration$OAuth2SecurityFilterChainConfiguration#jwtSecurityFilterChain matched due to #ConditionalOnBean (types: org.springframework.security.oauth2.jwt.JwtDecoder; SearchStrategy: all) found bean 'jwtDecoderByJwkKeySetUri'
...
2022-10-20 15:49:49.082 [main] a.ConfigurationClassBeanDefinitionReader : Registering bean definition for #Bean method com.mycompany.CustomSecurityConfig.filterChain()
...
2022-10-20 15:49:52.276 [main] edFilterInvocationSecurityMetadataSource : Adding web access control expression [authenticated] for any request
2022-10-20 15:50:13.348 [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter#33502cfe, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#729d1428, org.springframework.security.web.context.SecurityContextPersistenceFilter#7d0312a, org.springframework.security.web.header.HeaderWriterFilter#6ca97ddf, org.springframework.security.web.csrf.CsrfFilter#38f569d, org.springframework.security.web.authentication.logout.LogoutFilter#1104ad6a, org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter#74ab8610, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#7833407, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#66acaa54, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#115924ba, org.springframework.security.web.session.SessionManagementFilter#6a905513, org.springframework.security.web.access.ExceptionTranslationFilter#5749e633, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#49741e80]
...
2022-10-20 15:50:13.384 [main] edFilterInvocationSecurityMetadataSource : Adding web access control expression [permitAll] for any request
2022-10-20 15:50:17.641 [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter#4a0f4282, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#19d3f4fb, org.springframework.security.web.context.SecurityContextPersistenceFilter#99f75e4, org.springframework.security.web.header.HeaderWriterFilter#118c1faa, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#2b6ff016, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#5aefdb9e, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#43cf97a8, org.springframework.security.web.session.SessionManagementFilter#da5b46f, org.springframework.security.web.access.ExceptionTranslationFilter#11267e87, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#7827cdfc]
Is it expected that the bean CustomSecurityConfig.filterChain participates in DefaultWebSecurityCondition evaluation and OAuth2SecurityFilterChainConfiguration.jwtSecurityFilterChain is not created. Or the issue with DefaultWebSecurityCondition is that the instance of WebSecurityConfigurerAdapter is not in the context anymore (as to issue #10822 it is deprecated)?
The suggestion to add #Order() annotation didn't work:
#Configuration
#EnableWebSecurity
public class CustomSecurityConfig {
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ...
as well as further attempts to exclude the autoconfiguration class like this:
#SpringBootApplication(excludeName = "org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration")
public class Application extends SpringBootServletInitializer { ...
failed probably due to issue #5427 with the following error
java.lang.IllegalStateException: The following classes could not be excluded because they are not auto-configuration classes:
- org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration
at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.handleInvalidExcludes(AutoConfigurationImportSelector.java:222) ~[spring-boot-autoconfigure-2.7.4.jar!/:2.7.4]
This way also does not work:
#ComponentScan(excludeFilters = {#ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*OAuth2ResourceServerJwtConfiguration.*")})
public class Application extends SpringBootServletInitializer { ...
The documentation I read before posting:
Spring Security without the WebSecurityConfigurerAdapter
Spring Security: Upgrading the Deprecated WebSecurityConfigurerAdapter
Spring Security - How to Fix WebSecurityConfigurerAdapter Deprecated
Update
I have created a small Maven project that demonstrates the issue. After project is started, request the controller like this:
$ wget -nv -O - 'http://localhost:8080/spring/test'
Username/Password Authentication Failed.
As one can see, the custom configured SecurityFilterChain is not active because otherwise the access would be granted (as to antMatchers( "/**/test").permitAll()). ContextRefreshedEvent listener dumps two SecurityFilterChain instances (jwtSecurityFilterChain and filterChain), the priority of them is not possible to configure reliably.

As follows from issue #33103, the beans imported from XML via #ImportResource do not participate in Spring Boot autoconfiguration, hence beans scanning should be performed using annotation i.e. #SpringBootApplication(scanBasePackages = "some.package") – this basically solves the issue. –
dma_k
Nov 17, 2022 at 9:21
Thanks to this reply I found the solution.
But since we are using different auth method so it might not work for you. I am using DaoAuthenticationProvider from this tutorial. Then rewrite to a new version without using "WebSecurityConfigurerAdapter".
Hope this solution helps. I got tortured 3-4 hours finding solution.
I created a subpackage called "config", on the same level with "models, controller, service, etc"(or whatever you name them).
Then, manully import this package by "scanBasePackages" along with others.
#SpringBootApplication(scanBasePackages = {"com.example.test.controller", "com.example.test.model", "com.example.test.repo", "com.example.test.service","com.example.test.config"})
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
Here are 3 rewritten files in my "config" folder. All rewrote from the tutorial mentioned above.
a.CustomUserDetails
package com.example.test.config;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.test.model.UserModel;
public class CustomUserDetails implements UserDetails {
//#Autowired no need cuz this is not a bean
private UserModel user;
public CustomUserDetails(UserModel user) {
this.user = user;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
#Override
public String getPassword() {
return user.getPassword();
}
#Override
public String getUsername() {
return user.getEmail();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
public String getFullName() {
return user.getFirstName() + " " + user.getLastName();
}
}
b. CustomUserDetailsService
package com.example.test.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.example.test.repo.UserRepo;
import com.example.test.model.UserModel;
import org.springframework.stereotype.Service;
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private UserRepo userRepo;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserModel user = userRepo.findByEmail(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new CustomUserDetails(user);
}
}
c. SecurityConfiguration
package com.example.test.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
#Configuration
public class SecurityConfiguration{
#Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests ((requests)->requests
.requestMatchers("/", "/register", "/try2Register").permitAll()
.anyRequest().authenticated()
)
.formLogin((form)->form
.usernameParameter("email")
.defaultSuccessUrl("/users")
.permitAll()
)
.logout((logout) -> logout
.logoutSuccessUrl("/").permitAll());
http.authenticationProvider(authenticationProvider());
return http.build();
}
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/images/**", "/js/**", "/webjars/**");
}
}

Related

Why is role based access control not working?

I am trying to implement RBAC on my Spring Boot resource server. Previously I used Keycloak adapters, but now that they are deprecated I am having issues. I have followed the solution proposed here: https://stackoverflow.com/a/74572732/16489856, but it seems like that my configuration is wrong somehow.
Here is my Spring Security configuration:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/workstations").hasRole("USER")
.anyRequest().authenticated();
http
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
}
#SuppressWarnings("unchecked")
#Bean
public Jwt2AuthoritiesConverter authoritiesConverter() {
// Roles are taken from realm_access.roles
return jwt -> {
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
return realmRoles.stream().map(SimpleGrantedAuthority::new).toList();
};
}
public interface Jwt2AuthenticationConverter extends Converter<Jwt, AbstractAuthenticationToken> {
}
#Bean
public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
}
}
Roles are properly set, if I print them like so:
Jwt jwt = (Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Map<String, Object> resourceAccess = jwt.getClaim("realm_access");
Collection<String> resourceRoles = (Collection<String>) resourceAccess.get("roles");
System.out.println((Collection<String>) resourceAccess.get("roles"));
I get:
[default-roles-spring_reservations_realm_0, offline_access, uma_authorization, USER]
which is correct.
However, when I access /workstation endpoint, with a user that has the USER role, it fails with 403 forbidden status, how can I fix this?
Your authentication convert bean is not picked by spring-boot auto-configuration because boot expects a Converter<Jwt, ? extends AbstractAuthenticationToken> when you provide a Converter<Jwt, AbstractAuthenticationToken>.
Two different soultions:
apply the solution exposed in my answer you linked: provide the authentication converter explicitly. For that, replace .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) with .oauth2ResourceServer().jwt().jwtAuthenticationConver(authenticationConverter(authoritiesConverter()) (can be shorter if you use authenticationConverter as filterChain argument: authenticationConverter is bean in your conf):
#Bean
public SecurityFilterChain filterChain(
HttpSecurity http,
/* next is the exact type of your current authentication converter bean */
/* not Converter<Jwt, ? extends AbstractAuthenticationToken> like provided by default (and scanned for overrides) by boot */
Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception {
http.oauth2ResourceServer().jwt().jwtAuthenticationConver(authenticationConverter);
http.cors().disable(); // Are you really sure about this one?
http.authorizeHttpRequests()
// The following requires that either
// - the user is granted with "ROLE_USER" in Keycloak (with that case)
// - the user is granted with "user" in Keycloak and the authorities mapper adds "ROLE_" prefix and forces roles to uppercase
.requestMatchers("/workstations").hasRole("USER")
.anyRequest().authenticated();
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.csrf().disable(); // This is safe because of disabled sessions. Put it together maybe?
return http.build();
}
// Keep the rest of your conf as it is
change the type of your authenticationConverter #Bean to be the one that spring-boot looks for: Converter<Jwt, ? extends AbstractAuthenticationToken> (and not Converter<Jwt, AbstractAuthenticationToken> like in your current conf). This means changing a bit your Jwt2AuthenticationConverter definition so that the configurer destination type is a sub-type of AbstractAuthenticationToken (JwtAuthenticationToken in your case) and not AbstractAuthenticationToken itself. Yes, I know, this Java generics can be tricky sometimes):
public interface Jwt2AuthenticationConverter extends Converter<Jwt, JwtAuthenticationToken> {
}
// keep .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
// or change Converter<Jwt, AbstractAuthenticationToken> to Converter<Jwt, ? extends AbstractAuthenticationToken>
// for the type of bean injected in filterChain if you prefer explicit
// authenticationConverter override in fiterChain config like in the other solution
PS regarding comments
#dur is right regarding CORS: it doesn't look like a great idea to disable it, specially if your API is intended to be consumed by a web app.
For spring, a role is is nothing more than an authority prefixed with ROLE_. As a consequence:
hasRole("USER") is equivalent to hasAuthority("ROLE_USER") (but not hasAuthority("ROLE_user") as case is important)
neither hasAuthority("user") nor hasAuthority("SCOPE_USER") have hasRole equivalent (JWT decoder or not)
hasRole("SCOPE_USER") is equivalent to hasAuthority("ROLE_SCOPE_USER")
etc.
The SCOPE_ prefix is a choice from the default authorities converter because it maps authorities from scope claim (there is no OAuth2 nor OpenID standard claim for roles and it must pick it from somewhere standard). Using this same prefix when you pick authorities from somewhere else than the scope claim (like you do) would be very misleading...
Just use as prefix:
nothing (like in your question) if you are ok with hasAuthority or if roles in the access token are already prefixed with ROLE_
ROLE_ if you prefer reading hasRole over hasAuthority in your code (and roles in access tokens are not prefixed with ROLE_ already)
Here is how to configure Keycloak RBAC for a Spring Boot Resource Server, thanks to #ch4mp and #dur:
Spring Security configuration:
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.security.web.SecurityFilterChain;
#RequiredArgsConstructor
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
private final JwtAuthConverter jwtAuthConverter;
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().disable().csrf().disable();
http.authorizeHttpRequests()
.requestMatchers("/workstations").hasRole("USER")
.anyRequest().authenticated();
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthConverter);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
}
JwtAuthConverter:
import lombok.NonNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
#Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private final JwtAuthConverterProperties properties;
public JwtAuthConverter(JwtAuthConverterProperties properties) {
this.properties = properties;
}
#Override
public AbstractAuthenticationToken convert(#NonNull Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream.concat(
jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
extractResourceRoles(jwt).stream()).collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
}
private String getPrincipalClaimName(Jwt jwt) {
String claimName = JwtClaimNames.SUB;
if (properties.getPrincipalAttribute() != null) {
claimName = properties.getPrincipalAttribute();
}
return jwt.getClaim(claimName);
}
#SuppressWarnings("unchecked")
private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt) {
Map<String, Object> resourceAccess = jwt.getClaim("realm_access");
Collection<String> resourceRoles = (Collection<String>) resourceAccess.get("roles");
return resourceRoles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
}
JwtAuthConverterProperties:
import lombok.Data;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
#Data
#Validated
#Configuration
public class JwtAuthConverterProperties {
private String resourceId;
private String principalAttribute;
}

Spring Boot 3 + Spring Security 6 => 403 Forbidden with "requestMatchers"

For several days now I have been trying to solve a problem with Spring Security 6. I've read almost all the spring documentation for Spring Security 6 and I watched several tutorials and just cannot see where the mistake is. I looked at the code under a magnifying glass:
WebSecurityConfigurer.class:
package com.transfer.market.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
#Configuration
#EnableWebSecurity
public class WebSecurityConfigurer {
private final String ADMIN;
private final String ADMIN_PASS;
private final String SUPER;
private final String SUPER_PASS;
#Autowired
public WebSecurityConfigurer(AppSecurityExternalConfig appSecurityExternalConfig) {
this.ADMIN = appSecurityExternalConfig.getUser().getAdmin();
this.ADMIN_PASS = appSecurityExternalConfig.getPassword().getAdmin();
this.SUPER = appSecurityExternalConfig.getUser().getSup();
this.SUPER_PASS = appSecurityExternalConfig.getPassword().getSup();
}
#Bean
public UserDetailsService users() {
UserDetails admin = User.builder()
.username(ADMIN)
.password(encoder().encode(ADMIN_PASS))
.roles("ADMIN")
.build();
UserDetails sup = User.builder()
.username(SUPER)
.password(encoder().encode(SUPER_PASS))
.roles("ADMIN", "DBA")
.build();
return new InMemoryUserDetailsManager(admin, sup);
}
#Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/resource/**").permitAll()
.requestMatchers("/api/**").hasAnyRole("ADMIN", "DBA")
.requestMatchers("/db/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasRole('DBA')"))
.anyRequest().denyAll()
);
return http.build();
}
#Bean
public static BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
I have printed ADMIN, SUPER and their passwords to the console and they are for sure read correctly from the application.properties. So that is not the problem.
AppSecurityExternalConfig.class:
package com.transfer.market.configuration;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
#Data
#Configuration
#ConfigurationProperties(prefix = "config.security")
public class AppSecurityExternalConfig {
private User user;
private Password password;
#Data
#Component
#ConfigurationProperties(prefix = "user")
public static class User {
private String user;
private String admin;
private String sup;
}
#Data
#Component
#ConfigurationProperties(prefix = "password")
public static class Password {
private String user;
private String admin;
private String sup;
}
}
application.properties:
...
# Security:
config.security.user.admin=admin
config.security.password.admin=pass
config.security.user.sup=super
config.security.password.sup=pass
...
PlayerController.class:
#RestController
#Validated
public class PlayerController {
private final PlayerService playerService;
#Autowired
public PlayerController(PlayerService playerService) {
this.playerService = playerService;
}
#PostMapping("/api/players")
public ResponseEntity<Player> addSingle(#RequestBody #Valid Player player) {
return new ResponseEntity<>(playerService.addSingle(player), HttpStatus.CREATED);
}
...
It just keeps getting "403 Forbidden", but for all end points starting with "/resource" where they are .permitAll() it works and it's 200 OK. Why doesnt the other requestMatchers work? Please help.
#Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth.requestMatchers("/resource/**").permitAll()
.requestMatchers("/api/**")
.hasAnyRole("ADMIN", "DBA")
.requestMatchers("/db/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and
hasRole('DBA')"))
.anyRequest().denyAll());
return http.build();
}
You have to configured the basic authentication. Add the following statement in the SecurityFilterChain bean.
http.httpBasic();
i.e.
#Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth.requestMatchers("/resource/**").permitAll()
.requestMatchers("/api/**")
.hasAnyRole("ADMIN")
.requestMatchers("/db/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasRole('DBA')"))
.anyRequest().denyAll());
http.httpBasic();
return http.build();
}

Why is an InMemoryUserDetailsManager injected into this controller even though I've configured a JdbcUserDetailsManager elsewhere?

I'm trying to use Spring Security JDBC Authentication in a Spring Boot web app.
Here's the (much simplified, relevant) configuration:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer;
import javax.sql.DataSource;
#Configuration
public class SecurityConfiguration {
#Autowired
private DataSource dataSource;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema();
}
}
Here's a controller:
import org.adventure.inbound.UserFormData;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/users")
public class UserController {
private final UserDetailsManager userDetailsManager;
public UserController(UserDetailsManager userDetailsManager) {
this.userDetailsManager = userDetailsManager;
}
#PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<Void> registerUser(UserFormData userFormData) {
userDetailsManager.createUser(
User
.withUsername(userFormData.getUsername())
.password(userFormData.getPassword())
.authorities(new SimpleGrantedAuthority("ROLE_USER"))
.build());
return ResponseEntity.created(null).build();
}
}
When I start the app, I can see that:
The AuthenticationManagerBuilder gets a JDBCUDM configured:
but perhaps some step is missing?
The InMemoryUserDetailsManager is instantiated with Spring Security's default user:
The Controller, when instantiated, receives an InMemoryUserDetailsManager:
This means that when I try to create a new user, it uses the InMemoryUDM instead of the JDBCUDM that I'd like to use. Why is that?
Working solution
We already configure the UserDetailsService through JdbcUserDetailsManagerConfigurer, but didn't expose it as a bean.
#Autowired
#Bean
public UserDetailsManager configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema();
return jdbcUserDetailsManagerConfigurer.getUserDetailsService();
}
In your configuration override the userDetailsServiceBean(), and let it call the super method and add the #Bean annotation to make the configured service available. This is also explained here in the Javadocs.
#Configuration
public class SecurityConfiguration extends WebSecutiryConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema();
}
#Bean
public UserDetailsService userDetailsServiceBean()
throws java.lang.Exception {
return super.userDetailsServiceBean();
}
}
Also because you want to use your configuration instead of the Spring Boot one, you might need to add #EnableWebSecurity to disable the Spring Boot defaults.
Make your SecurityConfiguration extend from WebSecurityConfigurerAdapter, add #EnableWebSecurity and expose your JdbcUserDetailsManager as #Bean.
#Configuration
#EnableWebSecurity
public class SecurityConfiguration
extends WebSecurityConfigurerAdapter {
#Bean
public JdbcUserDetailsManager userDetailsManager() {
return yourJdbcUserDetailsManager;
}
/// etc.
}

PROBLEM WITH required a bean of type 'org.springframework.security.core.userdetails.UserDetailsService' that could not be found

When launching with mvn spring-boot:run or even with gradle returns that issue.
***************************
APPLICATION FAILED TO START
***************************
Description:
Field userDetailsService in com.ess.study.jwt.integration.config.SecurityConfig required a bean of type 'org.springframework.security.core.userdetails.UserDetailsService' that could not be found.
The injection point has the following annotations:
- #org.springframework.beans.factory.annotation.Autowired(required=true)
The following candidates were found but could not be injected:
- Bean method 'inMemoryUserDetailsManager' in 'UserDetailsServiceAutoConfiguration' not loaded because #ConditionalOnMissingBean (types: org.springframework.security.authentication.AuthenticationManager,org.springframework.security.authentication.AuthenticationProvider,org.springframework.security.core.userdetails.UserDetailsService; SearchStrategy: all) found beans of type 'org.springframework.security.authentication.AuthenticationManager' authenticationManager
Action:
Consider revisiting the entries above or defining a bean of type 'org.springframework.security.core.userdetails.UserDetailsService' in your configuration.
Here are the main classes, all the requirements looks ok to me, I am using the org.springframework.boot release 2.0.6.RELEASE.RELEASE
package com.ess.study.jwt.integration.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.context.annotation.Primary;
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.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
#Configuration
#EnableWebSecurity // Habilita la seguridad de Spring y consejos Spring Boot para aplicar todos los
// valores predeterminados sensibles
#EnableGlobalMethodSecurity(prePostEnabled = true) // Nos permite tener control de acceso a nivel de método.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${security.signing-key}")
private String signingKey;
#Value("${security.encoding-strength}")
private Integer encodingStrength;
#Value("${security.security-realm}")
private String securityRealm;
#Autowired
private UserDetailsService userDetailsService;
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder(encodingStrength));
}
#Autowired
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().httpBasic()
.realmName(securityRealm).and().csrf().disable();
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
#Primary // Making this primary to avoid any accidental duplication with another token
// service instance of the same name
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
//Nuevo metodo de decodificación.
#Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
}
and:
package com.ess.study.jwt.integration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class StudySecuredApplication {
public static void main(String[] args) {
SpringApplication.run(StudySecuredApplication.class, args);
}
}
Using maven or gradle it returns the same issue. All annotations and packages names seems to be as required.
Thanks! :)
UserDetailsService is an interface and you have to create its implementation. Example:
#Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private UsersRepository usersRepository;
#Transactional(readOnly = true)
#Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
try {
User user = usersRepository.findByLogin(login).get();
return new UserPrincipal(usersRepository.findByLogin(login).get());
} catch (NoSuchElementException e) {
throw new UsernameNotFoundException("User " + login + " not found.", e);
}
}
}

Spring-boot Digest authentication failure using Digest filter

I am new to this technology . I have trying to implement Digest Authentication for my Springboot application . I am getting below error while I am trying to call my application :There is no PasswordEncoder mapped for the id \"null\"","path":"/countryId/"}* Closing connection 0
curl command I am using to invoke : curl -iv --digest -u test:5f4dcc3b5aa765d61d8327deb882cf99 -d {"CountryCode": "INDIA"} http://localhost:9090/countryId/
Classes Details :
package com.sg.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
import org.springframework.stereotype.Component;
#Component
#Configuration
#EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
CustomDigestAuthenticationEntryPoint customDigestAuthenticationEntryPoint;
/*#Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}*/
#Bean
public UserDetailsService userDetailsServiceBean()
{
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("test").password("{noop}password").roles("USER").build());
return manager;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/hello/**").permitAll().anyRequest().authenticated()
.and().exceptionHandling().authenticationEntryPoint(customDigestAuthenticationEntryPoint).and()
.addFilter(digestAuthenticationFilter());
}
//#Bean
DigestAuthenticationFilter digestAuthenticationFilter() throws Exception {
DigestAuthenticationFilter digestAuthenticationFilter = new DigestAuthenticationFilter();
digestAuthenticationFilter.setUserDetailsService(userDetailsServiceBean());
digestAuthenticationFilter.setAuthenticationEntryPoint(customDigestAuthenticationEntryPoint);
return digestAuthenticationFilter;
}
}
package com.sg.config;
import org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
#Component
public class CustomDigestAuthenticationEntryPoint extends DigestAuthenticationEntryPoint {
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("Digest-Realm");
setKey("MySecureKey");
setNonceValiditySeconds(300);
super.afterPropertiesSet();
}
}
I have resolved the issue. Let me explain what went wrong first, in current Spring security, you can not use a plain text password, so have to keep some encrypting logic. But unfortunately Digest doesn't work with a encrypted password.
I have found a work around, instead using a Bean (Bycrypt), I have directly implemented PasswordEncoder interface, in a way, it should able to hold plain text password.
#Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
#Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
#Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
};
}

Resources