I'm building an application using Spring Data Rest, Spring Boot and Spring Security. I need to use #Secured annotations on methods and I've configured Spring Security in the following way:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// #formatter:off
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.securityContext().securityContextRepository(securityContextRepository())
.and()
.exceptionHandling()
.accessDeniedPage(RestPath.Errors.ROOT + RestPath.Errors.FORBIDDEN)
.and()
.csrf().disable();
}
// #formatter:on
#Bean
public SecurityContextRepository securityContextRepository() {
return new ApiUserSecurityContextRepository();
}
#Bean
public UserDetailsService userDetailsService() {
return new ApiUserDetailsService();
}
#Bean
public AuthenticationManager authenticationManager() throws Exception {
return new ProviderManager(Collections.singletonList(authenticationProvider()));
}
#Bean
public AuthenticationProvider authenticationProvider() throws Exception {
final DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
This type of configuration works well for regular MVC controllers and returns 403 when I try to access them. For example, the following controller security works:
#ResponseBody
#RequestMapping(value = RestPath.Configs.SLASH_TEST, method = RequestMethod.GET, produces = MediaTypes.HAL_JSON_VALUE)
#Secured({"ROLE_USER"})
public ResponseEntity test(#RequestParam(value = RestParam.DB_TEST, required = false) final boolean dbTest) throws ApplicationAvailabilityException {
final AppTestData appTestData = configService.testAppAvailability(dbTest);
return ResponseEntity.ok(projectionFactory.createProjection(AppTestProjection.class, appTestData));
}
However, when I try to use #Secured annotation over a rest repository - it does NOT, e.g.:
#RepositoryRestResource(collectionResourceRel = Shop.COLLECTION_NAME, path = RestResourceRel.SHOPS, excerptProjection = StandardShopProjection.class)
#Secured({"ROLE_USER"})
public interface RestShopRepository extends MongoRepository<Shop, String> {
#Secured({"ROLE_ADMIN"})
#Override
Shop findOne(String s);
}
ApiUserSecurityContextRepository is getting called for both of the methods, but only a custom MVC controller is get to the end of chain and I can check that it accesses vote() method in RoleVoter class for granting access.
As an example, I've checked Spring Data Rest + Spring Security sample, so #Secured or #PreAuthorize annotations should work with Spring Data Rest. Any ideas why they don't work?
Finally resolved the issue. The problem was in the following, I had another ShopRepository in different application module, which was not annotated with #RepositoryRestResource and it was the one which was used when accessing it using REST.
The following line of configuration in custom RepositoryRestConfigurerAdapter fixed the exploration of repositories which need to be exposed, so only annotated ones are exposed now:
config.setRepositoryDetectionStrategy(RepositoryDetectionStrategy.RepositoryDetectionStrategies.ANNOTATED);
After that I could not access the resource at all using REST, so I've figured out that it is not visible to Spring. I just had to enable Mongo repositories on API level with annotation #EnableMongoRepositories.
Related
So, I have the following scenario: I need to configure Spring Security for the Actuator endpoints. All this should be in an external library, as part of an autoconfiguration class dependent on a certain property. Access to some of those endpoints should only be allowed for a special role (I've configured an InMemoryUserDetailsManager just for the sake of it).
The applications using this external library can have their own security configurations. If the UserDetailsService bean does not exist in the app context the autoconfigured one is created. The problem is the following - if I declare an UserDetailsService in the app configuration, the autoconfiguration class updates it with the actuator admin user (and I can see that both users exist): the security configuration is all messed up though (I am getting 401 when using the correct credentials).
Does anyone know how to manage this?
The autoconfiguration class (in external library - Kotlin):
#Configuration
#EnableConfigurationProperties(Props::class)
#ConditionalOnProperty(prefix = Props.PROPERTY_PREFIX, name = ["securityEnabled"])
#ConditionalOnWebApplication(type = SERVLET)
open class ActuatorSecurityAutoConfiguration(private val props: Props) {
#Bean
#ConditionalOnMissingBean(PasswordEncoder::class)
public open fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
#Bean
#ConditionalOnBean(UserDetailsService::class)
public open fun actuatorUserDetailsManager(passwordEncoder: PasswordEncoder,
userDetailsManager: UserDetailsManager): UserDetailsManager {
userDetailsManager.createUser(
User.withUsername(props.username)
.password(props.password)
.passwordEncoder(passwordEncoder::encode)
.roles(ACTUATOR_USER_ROLE)
.build())
return userDetailsManager
}
#Bean
#ConditionalOnMissingBean(UserDetailsService::class)
public open fun defaultActuatorUserDetailsManager(passwordEncoder: PasswordEncoder): UserDetailsManager {
return InMemoryUserDetailsManager(listOf(
User.withUsername(props.username)
.password(props.password)
.passwordEncoder(passwordEncoder::encode)
.roles(ACTUATOR_USER_ROLE)
.build()
))
}
#Bean
#Throws(Exception::class)
#Order(100) //the order is not respected as this is loaded after the main config class
public open fun actuatorSecurityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
httpSecurity
.antMatcher("/actuator/**")
.authorizeHttpRequests {
it.antMatchers("/actuator/info", "/actuator/health").permitAll()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().hasRole(ACTUATOR_USER_ROLE)
}
.headers().disable()
.csrf().disable()
.httpBasic()
return httpSecurity.build()
}
The configuration class in the application using the external library:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Value("${credentials.username}")
private String username;
#Value("${credentials.password}")
private String password;
#Value("${credentials.role}")
private String role;
private final AuthClient authClient;
public DashboardSecurityConfiguration(AuthClient authClient) {
this.authClient = authClient;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Order(1)
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.antMatcher("/secure_endpoint/**")
.authorizeHttpRequests(authorize -> authorize
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().hasRole(role)
)
.authenticationProvider(new SsoAuthenticationProvider(authClient))
.addFilterBefore(new SsoTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.httpBasic();
return httpSecurity.build();
}
#Bean
#Order
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll())
.headers().disable()
.csrf().disable();
return httpSecurity.build();
}
#Bean
public UserDetailsService userDetailsManager() {
return new InMemoryUserDetailsManager(List.of(
createUser(username, password, role)
));
}
private UserDetails createUser(String username, String password, String... roles) {
return User.builder()
.username(username)
.password(passwordEncoder().encode(password))
.roles(roles)
.build();
}
application.properties:
prefix.securityEnabled=true
I can access /actuator endpoints without issues (even the secured paths) if I comment out the UserDetailsService in the Java application, but I can no longer access them (including /secure_endpoint) once I include it back in.
Any thoughts? Many thanks!
Imagine the following (hypothetical) data structure
endpoint | username | password
users admin 123
info george awd
data magnus e4
this means that every endpoint requires different credentials and no one username/password combo can log in to every endpoint. I am looking for a way to make this scalable in our Spring MVC project when adding more endpoints. We could use roles and hardcore this into the config class but the endpoints and login combinations vary for every customer installation
Given the following SecurityConfiguration with LookupAuthenticationService being the class that looks up the username/password data in the database
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String[] ENDPOINT_LIST = {
"/rest/**"
};
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(ENDPOINT_LIST)
.authenticated()
.and()
.httpBasic();
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected UserDetailsService userDetailsService() {
return new LookupAuthenticationService(passwordEncoder());
}
}
The ideal situation would be if LookupAuthenticationService has access to the request so we know which endpoint to fetch but I guess this is only possible when working with individual Filters
The possibilities I've found so far are:
Add a WebSecurityConfigurerAdapter and multiple UserDetailsServer specific per endpoint -> lots of code
Add a HandlerInterceptor per endpoint -> lots of code
AuthenticationManagerResolver returning a different AuthenticationManager based on pathInfo?
Any input how to best resolve this issue would be appreciated
You can have a table where you map endpoints to rules, like so:
pattern
authority
/users/**
ROLE_ADMIN
/info/**
ROLE_USER
/another/**
ROLE_ANOTHER
And instead of assigning a user to an endpoint, you assign a role to the users. With this in place, you can create an AuthorizationManager which is going to protect your endpoints based on the request path.
#Component
public class AccessRuleAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final AccessRuleRepository rules;
private RequestMatcherDelegatingAuthorizationManager delegate;
public AccessRuleAuthorizationManager(AccessRuleRepository rules) {
this.rules = rules;
}
#Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
return this.delegate.check(authentication, object.getRequest());
}
#EventListener
void applyRules(ApplicationReadyEvent event) {
Builder builder = builder();
for (AccessRule rule : this.rules.findAll()) {
builder.add(
new AntPathRequestMatcher(rule.getPattern()),
AuthorityAuthorizationManager.hasAuthority(rule.getAuthority())
);
}
this.delegate = builder.build();
}
}
And, in your SecurityConfiguration you simply do this:
#Autowired
private AccessRuleAuthorizationManager access;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) ->
authz.anyRequest().access(this.access)
)
.httpBasic(Customizer.withDefaults());
}
I recommend you to take a look at this repository and watch the presentation from the repository's description. The last steps of the presentation was adding the custom AuthorizationManager, and there's a great explanation about it.
I am currently working on Spring boot project which is acting as datasource for UI layer and all endpoint within project are protected using OIDC.
Everything is working as expected however when i try to protect any controller endpoint by putting #PreAuthorize annotation it works well in local development environment (JDK 8) but when things gets deployed over cloud (PCF, OpenJDK) it having weird behavior as 8 out of 10 times it works and 2 times it allows user to bypass security at method level and get desired output.Not sure how to fix this, i am suspecting something do with AOP proxy here but not sure.
Any pointer will be much appreciated.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, proxyTargetClass = true, jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
Security config class for reference.
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfig extends ResourceServerConfigurerAdapter {
#Value("${jwt-token.audience}")
private String resourceId;
#Value("${jwt-token.issuer-uri}")
private String issuer;
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(resourceId).stateless(false);
}
#Override
public void configure(final HttpSecurity http) throws Exception {
// #formatter:off
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/swagger-ui/**", "/v3/api-docs", "/swagger-ui.html").permitAll().anyRequest()
.authenticated().and().oauth2ResourceServer().jwt().decoder(jwtDecoder())
.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter());
// #formatter:on
}
#Bean
JwtDecoder jwtDecoder() {
//omitted for brevity
return jwtDecoder;
}
#Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}
I have RESTful spring resource server with #EnableResourceServer and extending ResourceServerConfigurerAdapter
In documentations says:
...In order to use this filter you must #EnableWebSecurity somewhere in your application, either in the same place as you use this annotation, or somewhere else.
But when I get to the public #interface EnableResourceServer I see ResourceServerConfiguration extends WebSecurityConfigurerAdapter.
Question:
So what do I need for pure RESTful API?
#EnableWebSecurity on any #Config
Extend the WebSecurityConfigurerAdapter?
1 + 2
Neither
My config
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class HGResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors().disable()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.jee().disable()
.logout().disable()
.rememberMe().disable()
.servletApi().disable()
.x509().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.and().authorizeRequests().antMatchers(Url.API_ERROR_LOGS_FRONTEND).permitAll()
.and().authorizeRequests().antMatchers(Url.API_REGISTER_PATH).permitAll()
.and().authorizeRequests().antMatchers(Url.API_VERIFY_EMAIL_PATH).permitAll()
.and().authorizeRequests().antMatchers(Url.API_RESET_PASSWORD_PATH).permitAll()
.and().authorizeRequests().antMatchers(Url.API_CONFIRM_RESET_PASSWORD_PATH).permitAll()
.and().authorizeRequests().anyRequest().authenticated();
}
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("http://localhost:8081/oauth/check_token");
tokenService.setClientId("client");
tokenService.setClientSecret("secret");
return tokenService;
}
//disable default user creation
#Bean
public UserDetailsService userDetailsService() throws Exception {
return new InMemoryUserDetailsManager();
}
//password encoder
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
No, the enable EnableWebSecurity is implicit.
I do not recommend to use WebSecurityConfigurerAdapter, you will come across some troubles :
Correctly configure spring security oauth2
Spring Boot makes #EnableWebSecurtiy implicit, but otherwise is it required.
You can prove this to yourself by taking a look at this OAuth2 resource server example. If you remove the #EnableWebSecurity annotation there, you will find that the Spring Security Filter Chain is not wired.
You can still extend WebSecurityConfigurerAdapter to separate general web application security concerns from those specific to resource server configuration. This isn't technically necessary, but can make for a cleaner separation of concerns.
I'm trying to use authentication by google. I am using springboot2, so most of the configuration is automatic. The authentication itself works good, but afterwards I would like to populate Principal with my own data (roles, username, and stuff).
I've created MyUserService that exteds DefaultOauth2UserService, and I am trying to use it as follows:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
MyUserService myUserService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(myUserService);
}
}
I've checked with debuger, that application never actually uses loadUser methods. And here is implementation of MyUserService:
#Component
public class MyUserService extends DefaultOAuth2UserService {
#Autowired
UserRepository userRepository;
public MyUserService(){
LoggerFactory.getLogger(MyUserService.class).info("initializing user service");
}
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
String emailFromGoogle = (String) attributes.get("email");
User user = userRepository.findByEmail(emailFromGoogle);
attributes.put("given_name", user.getFirstName());
attributes.put("family_name", user.getLastName());
Set<GrantedAuthority> authoritySet = new HashSet<>(oAuth2User.getAuthorities());
return new DefaultOAuth2User(authoritySet, attributes, "sub");
}
}
Actually the solution was just to add another property for google authentication:
spring.security.oauth2.client.registration.google.scope=profile email
Not sure, what is the default scope, and why entrance to the service is dependent on scope, but without this line the code never reached my custom service.
I think you're missing the #EnableOAuth2Client annotation at the top of your SecurityConfig class.
Regardless, I made an examplewith a Custom user service for oauth2 here https://github.com/TwinProduction/spring-security-oauth2-client-example/ if it helps