JUnit5 test a secured controller in using Spring security - spring-security

I have a simple controller that should only be accessible in a secured context.
In my application, I only whitelist other URLs, not /user.
In the unit test for the single #GetResponse I get an Http Status of 401 instead of a 200. I expect this, as it is secured.
When I actually run the application with a logged in user, I get a 200, and a valid result.
How can I Unit Test the behaviour of the request and any other secured controller paths?
Unit test:
#SpringBootTest
#AutoConfigureMockMvc
class UserControllerTest {
#InjectMocks
UserController userController;
#Autowired
private MockMvc mockMvc;
#Autowired
private ApplicationContext applicationContext;
#Test
void getUserNone() throws Exception {
mockMvc
.perform(MockMvcRequestBuilders.get("/user"))
.andExpect(status().isOk());
}
}
App:
#SpringBootApplication
public class WebApp extends WebSecurityConfigurerAdapter {
public static void main(final String... args) {
SpringApplication.run(WebApp.class, args);
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.authorizeRequests(a -> a
.antMatchers("/error", "/css/**", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(e -> e
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
)
.oauth2Login();
}
}
Controller:
#Controller
public class UserController {
#GetMapping("/user")
public #ResponseBody
Map<String, Object> getUser(#AuthenticationPrincipal final OAuth2User principal) {
if (principal == null) {
return Collections.EMPTY_MAP;
} else {
return Collections.singletonMap("name", principal.getName());
}
}
}
Obviously, I could add /user to the whitelisted paths, but that would be incorrect for any secured parts of the application.

You can use the annotation #WithMockUser and #WithAnonymousUser which creates a fake Authentication that is populated in SecurityContext.
Documentation can be found here .
Example code:
#Test
#WithMockUser("john")
void testAuthorize() throws Exception {
mockMvc
.perform(MockMvcRequestBuilders.get("/user"))
.andExpect(status().isOk());
}
#Test
#WithAnonymousUser
void testUnauthorized() throws Exception {
mockMvc
.perform(MockMvcRequestBuilders.get("/user"))
.andExpect(status().isUnauthorized());
}

Related

Use several spring security configuration and apply them according to the calling url

I currently have a backend application that implements a very simple Spring security based on login / password that must be added in the http headers.
I also have a front end that uses OKTA as a provider and works with JWT tokens.
I now want to make the end points dedicated to the front end applications use the JWT token system and all the others use the current login/password system.
I can make my application work with an OKTA configuration or with a login / password configuration but I can't make both work together.
Looking at the different messages on stack overflow I have implemented a double configuration but it is always the first one that is applied. The second one is simply ignored and the endpoints of the perimeter are allowed without any token or login / password
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Configuration
#Order(1)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.csrf().disable();
http
.authorizeRequests().antMatchers("/api/v1/end-point/**").authenticated()
.and().oauth2ResourceServer().jwt();
Okta.configureResourceServer401ResponseBody(http);
}
}
#Configuration
#Order(2)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
#Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilter(initAuthenticationFilter())
.antMatcher("/api/v1/tools/**")
.authorizeRequests().anyRequest().authenticated();
}
private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
apiKeyRequestHeaderName);
requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
}
#Override
#Bean
#Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
In this code, configuration 2 is never used even if I make a call to /api/v1/tools
If I remove configuration 1, configuration 2 is applied.
Can you help me to understand what I am doing wrong?
EDIT 1 :
With the help and suggestion of Eleftheria Stein-Kousathana, i change my configuration (and i add Swagger white list configuration)
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Configuration
#Order(1)
public static class SwaggerConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 1");
http.cors();
http.csrf().disable();
http
.requestMatchers(matchers -> matchers.antMatchers(AUTH_WHITELIST))
.authorizeRequests(authz -> {
authz.anyRequest().permitAll();
});
}
}
#Configuration
#Order(2)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 2");
http.cors();
http.csrf().disable();
http
.requestMatchers(matchers -> matchers.antMatchers("/api/v1/end-point/**"))
.authorizeRequests(authz -> {
try {
authz.anyRequest().authenticated().and().oauth2ResourceServer().jwt();
} catch (Exception e) {
e.printStackTrace();
}
});
Okta.configureResourceServer401ResponseBody(http);
}
}
#Configuration
#Order(3)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${algo.http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${algo.http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
#Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 3");
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilter(initAuthenticationFilter())
.requestMatchers(matchers -> matchers.antMatchers("/api/**"))
.authorizeRequests(authz -> {
try {
authz.anyRequest().authenticated();
} catch (Exception e) {
e.printStackTrace();
}
});
}
private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
apiKeyRequestHeaderName);
requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
}
#Override
#Bean
#Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
I feel that I am very close to succeeding
Swaggers is accessible when not authenticated
The routes corresponding to "/api/v1/end-point/**" need a JWT token otherwise I get a 401 error
The routes corresponding to "/api/** " need a login / password otherwise I get a 401 error
But now I have the following error:
Every time I request a page under swagger or make a call to my api, my web browser asks me for a login / password.
If I cancel I can still navigate on Swagger UI and make call to "/api/v1/end-point/**".
Every Login / password are rejected even they are valid in configuration 3.
If I don't fill the login / password and make a call to any route of "/api/**" i got the following error :
2021-07-23 14:49:16.642 [http-nio-8081-exec-9] INFO c.c.a.a.c.CorrelationIdLoggingAspect - Calling api.controller.endpoint.getActivities executed in 197ms.
2021-07-23 14:49:22.247 [http-nio-8081-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [/secret] threw exception [Filter execution threw an exception] with root cause
java.lang.StackOverflowError: null
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
at jdk.internal.reflect.GeneratedMethodAccessor220.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
If I'm understanding your program sketch and description correctly, let me attempt to summarize. Your application seeks to support the following:
Serve up swagger UI to the public and allow browsing of API definitions.
Use authenticated API endpoints (prefixed by /api/v1/end-point) with an Okta-provided JWT from another client (not swagger).
Use authenticated API endpoints (prefixed by /api, but not /api/v1/end-point) via swagger with username/password as headers.
Note: I'm not going to cover how to configure Okta as a provider here, nor configuring swagger. If those steps are not done correctly, you may still have issues.
As far as Spring Security, I think your main issue is due to the fact that you don't appear to have configured an authentication provider for your header-based configuration. This is typically done via a UserDetailsService (see section on UserDetailsService):
#Bean
public UserDetailsService userDetailsService() {
// #formatter:off
UserDetails userDetails = User.builder()
.username("api-client")
.password("{noop}my-api-key")
.roles("USER")
.build();
// #formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
This is obviously an example not meant for production. But the important point is that you have to provide a way for Spring Security to determine that the credentials are valid. Whether it's a user's username/password, or an API client's appId/apiKey, the principal (see Authentication) is looked up through a UserDetailsService, and then the credentials are validated by the AuthenticationProvider.
Unfortunately, the built-in RequestHeaderAuthenticationFilter is built on top of a different type of provider that assumes you are pre-authenticated, and is therefore incompatible with username/password authentication. While you could work around this by adapting one type of provider to another, it's more straight forward (at least for example purposes) to adapt the UsernamePasswordAuthenticationFilter to your use case. For example:
private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
#Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
#Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
};
usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernamePasswordAuthenticationFilter.setPostOnly(false);
usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
// Do nothing
});
return usernamePasswordAuthenticationFilter;
}
If you're interested in making this feel more built-in, check out the section of the docs on custom DSLs.
I would also suggest you override the configure(WebSecurity web) method in WebSecurityConfigurerAdapter to perform your permitAll and condense the configuration down to two, as well as eliminating the /api/** pattern so your entire application is secure by default. Here's a full example (omitting any Okta-specific code) that also demonstrates correct usage of the Spring Security lambda DSL:
#Configuration
public class SecurityConfiguration {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Order(1)
#EnableWebSecurity
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.antMatcher("/api/v1/end-point/**")
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
// #formatter:on
}
}
#Order(2)
#EnableWebSecurity
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${algo.http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${algo.http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
#Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.addFilterAt(usernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
// #formatter:on
}
private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
#Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
#Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
};
usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernamePasswordAuthenticationFilter.setPostOnly(false);
usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
// Do nothing
});
return usernamePasswordAuthenticationFilter;
}
#Bean
public UserDetailsService userDetailsService() {
// #formatter:off
UserDetails userDetails = User.builder()
.username("api-client")
.password("{noop}my-api-key")
.roles("USER")
.build();
// #formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
}
}
Final note: One caveat is that I included disabling CSRF, which you have done. This is only a reasonable thing to do if you don't intend to use this application in a web browser with sessions. Since I marked both configurations as stateless (your Okta+JWT example was not), this seems reasonable. Most of the time, however, you really don't want to disable CSRF protection, especially if the reason is "I can't figure out how to make my UI application work with CSRF enabled."
First of all, thank you very much for your help.
I took the time to respond because I wanted to understand your answer.
You are right about the description of the sketch I am trying to implement.
With your configuration I can now access Swagger without any login/password.
The first configuration (OKTA) works fine and I think the last one (login / password) does too.
I now face one last error when I try to access the routes
protected by login and password.
I am facing an issue where Spring throws an "org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken" exception.
I am looking to resolve this issue and I think everything will work after that.
Let me modestly point out that the setter methods :
requestHeaderAuthenticationFilter.setPrincipalRequestHeader(appIdRequestHeaderName);
requestHeaderAuthenticationFilter.setCredentialsRequestHeader(apiKeyRequestHeaderName);
are not accessible and I keep setting them by constructor.
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName, apiKeyRequestHeaderName)
Thank you very much for all the answers.
We found the solution thanks your help.
Here the final code for helping everyone who needs to do the same things as us. ​
Security configuration :
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Order(1)
#Configuration
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/v1/end-point/**")
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
Okta.configureResourceServer401ResponseBody(http);
}
}
#Order(2)
#Configuration
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${http.app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${http.api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
#Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
#Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAt(initAuthenticationFilter(), UsernameRequestHeaderAuthenticationFilter.class)
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
}
private UsernameRequestHeaderAuthenticationFilter initAuthenticationFilter() throws Exception {
UsernameRequestHeaderAuthenticationFilter usernameRequestHeaderAuthenticationFilter = new UsernameRequestHeaderAuthenticationFilter();
usernameRequestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
usernameRequestHeaderAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernameRequestHeaderAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernameRequestHeaderAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernameRequestHeaderAuthenticationFilter.setPostOnly(false);
usernameRequestHeaderAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
// Do nothing
});
return usernameRequestHeaderAuthenticationFilter;
}
}
}
Authentication filter :
public class UsernameRequestHeaderAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
#Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
#Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
}
}
We have also adapted our AuthenticationManager to use UserAuthorities
Thanks again to all

Loading security for some specific tests only

I'm trying to have some integration tests that exercise some business controllers, and other tests that exercise the security configuration.
So I have the business controllers tests extend a BaseTest class and the security configuration tests extend a SecurityBaseTest class.
I want to load the security configuration only for the tests that exercise it, and not for the business controllers tests as these should only exercise the controllers and not the security configuration.
#SpringBootTest(classes = { TestConfiguration.class, WebConfiguration.class })
#RunWith(SpringRunner.class)
public abstract class BaseTest {
#Before
public void setup() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilters(springSecurityFilterChain).build();
httpHeaders = new HttpHeaders();
}
}
#SpringBootTest(classes = { TestConfiguration.class, SecurityConfiguration.class, WebConfiguration.class })
public abstract class SecurityBaseTest extends BaseTest {
#Before
public void setup() throws Exception {
super.setup();
userFixtureService.addUserFixture();
addTokenToRequestHeader(httpHeaders, UserFixtureService.USER_EMAIL);
}
private void addTokenToRequestHeader(HttpHeaders headers, String username) {
tokenAuthenticationService.addTokenToResponseHeader(headers, username);
}
}
When running the business controllers tests using the command mvn clean install -Denv="test" -Ddb="h2" -Dtest=UserControllerTest -Dmaven.surefire.debug="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 -Xnoagent -Djava.compiler=NONE" then the debugger shows that the security configuration is rightfully not being loaded.
But the testCrudOperations method fails as it returns a 403 status and not the 201 as expected. And the debugger shows that the UserController controller endpoint is not being hit at all.
public class UserControllerTest extends BaseTest {
#Test
public void testCrudOperations() throws Exception {
MvcResult mvcResult = this.mockMvc
.perform(post(RESTConstants.SLASH + UserDomainConstants.USERS)
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)
.headers(httpHeaders)
.content(jacksonObjectMapper.writeValueAsString(userResource0)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.firstname").exists())
.andExpect(jsonPath("$.firstname").value(userResource0.getFirstname()))
.andExpect(jsonPath("$.lastname").value(userResource0.getLastname()))
.andExpect(jsonPath("$.email").value(userResource0.getEmail()))
.andExpect(header().string("Location", Matchers.containsString("/users/"))).andReturn();
UserResource retrievedUserResource = deserializeResource(mvcResult, UserResource.class);
assertThatUserResource(retrievedUserResource).hasEmail(userResource0.getEmail());
assertThatUserResource(retrievedUserResource)
.hasRole(retrievedUserResource.getUserRoles().iterator().next().getRole());
userResource0.setResourceId(retrievedUserResource.getResourceId());
}
}
When running the security configuration tests using the command mvn clean install -Denv="test" -Ddb="h2" -Dtest=UserAuthenticationTest -Dmaven.surefire.debug="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 -Xnoagent -Djava.compiler=NONE" then the debugger shows that the security configuration is rightfully being loaded.
But the testUnsecuredResourceGrantsAccess method fails, with the error No qualifying bean of type 'org.springframework.security.web.FilterChainProxy' available message.
public class UserAuthenticationTest extends SecurityBaseTest {
#Test
public void testUnsecuredResourceGrantsAccess() throws Exception {
this.mockMvc.perform(
post(RESTConstants.SLASH + UserDomainConstants.USERS + RESTConstants.SLASH + UserDomainConstants.LOGIN)
.accept(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isBadRequest())
.andReturn();
}
}
The security configuration is:
#EnableWebSecurity(debug = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Bean
public AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter() throws Exception {
AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter = new AuthenticationFromCredentialsFilter(new AntPathRequestMatcher("/users/login", RequestMethod.POST.name()));
authenticationFromCredentialsFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFromCredentialsFilter;
}
#Bean
public AuthenticationFromTokenFilter authenticationFromTokenFilter() throws Exception {
AuthenticationFromTokenFilter authenticationFromTokenFilter = new AuthenticationFromTokenFilter(new NegatedRequestMatcher(new AntPathRequestMatcher("/users/login")));
authenticationFromTokenFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFromTokenFilter;
}
#Bean
FilterRegistrationBean<AuthenticationFromTokenFilter> disableAutoRegistration(final AuthenticationFromTokenFilter filter) {
final FilterRegistrationBean<AuthenticationFromTokenFilter> registration = new FilterRegistrationBean<AuthenticationFromTokenFilter>(filter);
registration.setEnabled(false);
return registration;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(simpleCORSFilter, ChannelProcessingFilter.class);
http
.addFilterBefore(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/", "/error").permitAll()
.antMatchers("/users/login").permitAll()
.antMatchers("/admin/**").hasRole(UserDomainConstants.ROLE_ADMIN)
.anyRequest().authenticated();
}
}

How to config ip address dynamically with spring-security?

I'm new to spring-security, I need to setup a authorization system to secure a REST service.
In my case, my "users" are some servers of different departments and companies.So I tried to config the servers as MyUser which is sub class of UserDetails.
But I got an issue when I was asked to authorize the ip address of the servers. I saw there're ip address authorizations in WebSecurityConfigurerAdapter.configure(HttpSecurity http), and I can retrieve the configuration with configure(AuthenticationManagerBuilder auth){auth.userDetailsService(myUserDetailsService);}. But it seems that the configure(HttpSecurity http) method only runs once when the system boots.
So, what should I do? Is there any way to add a customized checker or something to verify the ip address?
These are my code:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.httpBasic().and()
.authorizeRequests()
.antMatchers("/order/**").hasAuthority("read_order") //(1)
.antMatchers("/order/**").hasIpAddress("192.168.1.45") //(2)
.anyRequest().denyAll();
// #formatter:on
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
}
With customized userService which implements the UserDetailsService, I can replace the (1) line with configuration in database. Which means when I change the configuration, Spring Security will load it from database. I don't need to restart the system.
How do I do the similar thing to the (2) line?
I found a solution. I'm not sure is it the best way, but it can work. This is the solution.
First, we can define a class which implements AccessDecisionManager:
#Service
public class ResourceAccessDecisionManager implements AccessDecisionManager {
#Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws
AccessDeniedException, InsufficientAuthenticationException {
//...
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
String ip = request.getRemoteHost();
Object principal = authentication.getPrincipal();
User user;
if (principal instanceof User){
user=(User)principal;
if (!ip.equals(user.getIpConfig())){
throw new AccessDeniedException("wrong ip");
}
}
//...
}
//...
}
Surely, you had to store the ip config within the User class.
Then, we define a class extends AbstractSecurityInterceptor implements Filter:
#Service
public class ResourceFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
//...
#Autowired
public void setMyAccessDecisionManager(ResourceAccessDecisionManager resourceAccessDecisionManager) {
super.setAccessDecisionManager(resourceAccessDecisionManager);
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
private void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
//...
}
And add the filter into security config class:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final ResourceService resourceService;
private final UserService userService;
private final HttpServletRequest request;
private final ResourceFilterSecurityInterceptor resourceFilterSecurityInterceptor;
private final EnvironmentConfig environmentConfig;
#Autowired
public SecurityConfiguration(ResourceService resourceService, UserService userService,
HttpServletRequest request,
ResourceFilterSecurityInterceptor resourceFilterSecurityInterceptor,
EnvironmentConfig environmentConfig) {
this.resourceService = resourceService;
this.userService = userService;
this.request = request;
this.resourceFilterSecurityInterceptor = resourceFilterSecurityInterceptor;
this.environmentConfig = environmentConfig;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.httpBasic().and()
.authorizeRequests()
.antMatchers("/order/**").authenticated()
.anyRequest().authenticated()
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
http.addFilterBefore(resourceFilterSecurityInterceptor,FilterSecurityInterceptor.class);
// #formatter:on
}
//...
}
Now it can work, but any suggestion is welcome and admirable to improve the solution.

How to secure Apache Camel rest endpoint with Spring Security and OAuth2

I'm working on Spring Boot application with configured SSO/OAuth2 security.
Authentication works fine for my rest controllers and now I need to secure my Apache Camel route with a rest endpoint.
As I understand there are several ways how to do it:
By adding auth processor to my route
By adding policy (SpringSecurityAuthorizationPolicy) to my route
By handlers option to jetty endpoint
I'm trying to do it by adding new auth processor to my rest endpoint but I stuck on this exception:
org.springframework.security.oauth2.common.exceptions.OAuth2Exception:
No AuthenticationProvider found for
org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
During debugging I see that org.springframework.security.authentication.ProviderManager.getProviders() contains only one provider AnonymousAuthenticationProvider so probably I have to register appropriate provider...
Can someone help me to find the right way to solve this problem please?
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().anyRequest().permitAll();
}
#Configuration
#EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Value("${oauth2.token.endpoint}")
private String tokenEndpoint;
#Bean
public ResourceServerTokenServices tokenService() {
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setClientId("clientId");
tokenServices.setClientSecret("clientSecret");
tokenServices.setCheckTokenEndpointUrl(tokenEndpoint);
return tokenServices;
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
}
#Configuration
public class EmbeddedServerRoute {
#Bean
public RoutesBuilder embeddedServer() {
return new RouteBuilder() {
#Override
public void configure() throws Exception {
restConfiguration().component("jetty").port("8081").bindingMode(RestBindingMode.json);
}
};
}
}
#Component
public class RestTestRoute extends RouteBuilder {
#Autowired
private AuthProcessor authProcessor;
#Override
public void configure() throws Exception {
from("rest:get:/test").process(authProcessor).to("mock:end").end();
}
}
#Component
public class AuthProcessor implements Processor {
#Autowired
private AuthenticationManager authenticationManager;
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
#Override
public void process(Exchange exchange) throws Exception {
HttpServletRequest request = exchange.getIn().getBody(HttpServletRequest.class);
Subject subject = new Subject();
Authentication auth = getAuth(request);
subject.getPrincipals().add(auth);
exchange.getIn().setHeader(Exchange.AUTHENTICATION, subject);
}
private Authentication getAuth(HttpServletRequest request) throws OAuth2Exception {
Authentication authentication = null;
try {
authentication = tokenExtractor.extract(request);
if (authentication != null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
return authenticationManager.authenticate(authentication);
}
} catch (Exception e) {
throw new OAuth2Exception(e.getMessage());
}
throw new OAuth2Exception("Not Authorized to view resource");
}
}
As a final solution I decided to use Spring Boot embedded servlet container instead of Apache Camel rest component. So it could be easily secured by Spring Security. This could be done by creating additional beans:
#Bean
public ServletRegistrationBean servletRegistrationBean() {
SpringServerServlet serverServlet = new SpringServerServlet();
ServletRegistrationBean regBean = new ServletRegistrationBean(serverServlet, "/camel/*");
Map<String, String> params = new HashMap<>();
params.put("org.restlet.component", "restletComponent");
regBean.setInitParameters(params);
return regBean;
}
#Bean
public Component restletComponent() {
return new Component();
}
#Bean
public RestletComponent restletComponentService() {
return new RestletComponent(restletComponent());
}

Spring Security with Java Configuration: How to handle BadCredentialsException from a custom provider

I need to authenticate some rest services using a token id in the url (or maybe in the request header - but this is not important for now). I am trying to use java configuration to set this up using as a guide this post. My problem is that I do not know how to handle "BadCredentialsException" that is thrown when the authentication fails from the provider. Here is my Security Config:
public static class SecurityConfigForRS extends
WebSecurityConfigurerAdapter {
#Autowired
TokenAuthenticationProvider tokenAuthenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(tokenAuthenticationProvider);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.regexMatcher("^/rest.*")
.addFilterBefore(
new TokenAuthenticationFilter(
authenticationManagerBean()),
AbstractPreAuthenticatedProcessingFilter.class)
.and().csrf().disable();
}
}
For now I skip the other implementations - if it helps I will post them later.
When the token is missing or is invalid, the TokenAuthernticationProvider throws a BadCredentialsException. I need to catch this and send back an 401-Unauthorized. Is it possible to do this?
The first Filter I created was a subclass of GenericFilterBean and it did not have support for authentication failure handler or success handler. However AbstractAuthenticationProcessingFilter supports success and failure handlers. My filter is as simple as that:
public class TokenAuthenticationProcessingFilter extends
AbstractAuthenticationProcessingFilter {
public TokenAuthenticationProcessingFilter(
RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException,
IOException, ServletException {
Authentication auth = new TokenAuthentication("-1");
try {
Map<String, String[]> params = request.getParameterMap();
if (!params.isEmpty() && params.containsKey("auth_token")) {
String token = params.get("auth_token")[0];
if (token != null) {
auth = new TokenAuthentication(token);
}
}
return this.getAuthenticationManager().authenticate(auth);
} catch (AuthenticationException ae) {
unsuccessfulAuthentication(request, response, ae);
}
return auth;
}}
and my http security is:
public static class SecurityConfigForRS extends
WebSecurityConfigurerAdapter {
#Autowired
TokenAuthenticationProvider tokenAuthenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(tokenAuthenticationProvider);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
}
#Bean
protected AbstractAuthenticationProcessingFilter getTokenAuthFilter()
throws Exception {
TokenAuthenticationProcessingFilter tapf = new TokenAuthenticationProcessingFilter(
new RegexRequestMatcher("^/rest.*", null));
tapf.setAuthenticationManager(authenticationManagerBean());
return tapf;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.regexMatcher("^/rest.*")
.addFilterAfter(getTokenAuthFilter(),
BasicAuthenticationFilter.class).csrf().disable();
}
}
The filter chain order does matter! I placed it after BasicAuthenticationFilter and it works fine. Of course there might be a better solution but for now this works!
May be you can try with Global Exception handling with ControllerAdvice
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
#ControllerAdvice
public class ExceptionControllerAdvice {
#ExceptionHandler // when Invalid Credentials
public ResponseEntity<ErrorMessage> handleInvalidCredentialsException(
BadCredentialsException ex) {
return new ResponseEntity<ErrorMessage>(
new ErrorMessage(ex.getMessage()), HttpStatus.UNAUTHORIZED);
}
#Getter
#Setter
#AllArgsConstructor
class ErrorMessage {
private String error;
}
}

Resources