How can I unit test AuthControllerIntegrationTest and mock JwtDecoder in Spock correctly? - spring-boot

I have a spring security Book store api which is working as expected.
My Security Config class look like this.
#Configuration
public class SecurityConfiguration {
#Bean
JwtDecoder jwtDecoder () {
return JwtDecoders.fromIssuerLocation("https://jwt-issuer-location")
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.antMatchers(HttpMethod.GET, '/api/auth/**')
.antMatchers(HttpMethod.GET, '/api/books').authenticated()
.antMatchers(HttpMethod.GET, '/api/users').authenticated()
.anyRequest().authenticated()
.and().httpBasic()
)
.oauth2ResourceServer().jwt()
return http.build();
}
#Bean
InMemoryUserDetailsManagerService() {
UserDetails user = User.builder().username("bob").password("password123").roles("api").build()
return new InMemoryUserDetailsManager(user)
}
}
For controller integrations like returning the list of users and books, I have used #MockBean for JwtDecoder like following
#MockBean
JwtDecoder jwtDecoder
then the test works.
However, for my integration test for authentication, it does not work in the same way. The integration looks like this
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutocConfigureMockMvc
class AuthController extends Specification {
MockMvc mockMvc
#Autowired
WebApplicationContext context
def setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).apply(springSecurity()).build()
}
def "401 Unauthorized Exception"() {
given:
String url = "/api/books"
when:
ResultActions res = this.mockMvc.perform(get(url))
then:
response.andExpect(status().is4xxClientError())
}
}
For some reason, this auth controller integration test tries to make a http request for verifying the token sent by the user. the url is passed to jwtDecoders.fromIssuerLocation function.
I believe that is not an ideal way of make a unit test. It is not supposed to make any request to 3rd party
And since this is just the unit test, I passed some random url (I do not want to pass actual url for security purpose and push it. For the production app, I am pulling the actual url from environment variable).
Because of that, when mockMvc.perform runs, it throws I/O error on GET request for .....
I do not understand why other integration works by MockBean annotation on JwtDecoder. But it does not resolve AuthControllerIntegrationTest error.
Can someone tell I how I should mock the JwtDecoder instead?
Thank you in advance :)
I have tried adding MockBean on JwtDecoder but it does not work for AuthControllerIntegrationTest.
I would like to know how to mock the JwtDecoder correctly or maybe how to mock the JwtDecoders.fromIssuerLocation. But I do not know how to mock and return what.

Related

Unit test returns 401 Unauthorized on a permitted route in Spring WebFlux Security

I am trying to test a route that returns array of objects but the test fails because it returns Unauthorized instead of 200 OK
My test class
#RunWith(SpringRunner.class)
#WebFluxTest(value = CatController.class)
class ContentManagementTestApplicationTests {
#Autowired
ApplicationContext context;
#Autowired
private WebTestClient webTestClient;
#MockBean
CatRepository catRepository;
#BeforeEach
public void setup(){
webTestClient=WebTestClient.bindToApplicationContext(context)
.apply(SecurityMockServerConfigurers.springSecurity())
.configureClient()
.build();
}
#Test
void contextLoads() {
}
#Test
public void getApprovedCats(){
webTestClient.get()
.uri("/cat")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk();
}
}
And ApplicationSecurityConfig class, has a SecurityWebFilterChain Bean
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, AuthConverter jwtAuthConverter, AuthManager jwtAuthManager){
AuthenticationWebFilter jwtFilter = new AuthenticationWebFilter(jwtAuthManager);
jwtFilter.setServerAuthenticationConverter(jwtAuthConverter);
return http .csrf().disable()
.authorizeExchange()
.pathMatchers(HttpMethod.GET,"/cat").permitAll()
.anyExchange()
.authenticated()
.and()
.addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHORIZATION)
.formLogin().disable()
.httpBasic().disable()
.build();
}
The Error on JUint test shows following
java.lang.AssertionError: Status expected:<200 OK> but was:<401 UNAUTHORIZED>
Expected :200 OK
Actual :401 UNAUTHORIZED
Before test fails and shows Unauthorized, in console it prints
"Using generated security password: 8e5dd468-3fd1-42b6-864a-c4c2ed2227b7"
And I believe that should not be printed since I disabled it in securityFilterChain
From the Javadoc of #WebFluxTest:
Annotation that can be used for a Spring WebFlux test that focuses only on Spring WebFlux components.
Using this annotation will disable full auto-configuration and instead apply only configuration relevant to WebFlux tests
If you are looking to load your full application configuration and use WebTestClient, you should consider #SpringBootTest combined with #AutoConfigureWebTestClient rather than this annotation.
In other words, when using #WebFluxTest in your test, your SecurityWebFilterChain is not picked up.
Try annotating your class with #SpringBootTest instead.

How to test http status code 401 (unauthenticated) with MockMVC and Spring Boot OAuth2 Resource Server?

I am currently developing a Spring Boot 3 application which provides a REST API. To consume this API, users have to be authenticated via an OAuth2 workflow of our identity provider keycloak. Therefore, I have used org.springframework.boot:spring-boot-starter-oauth2-resource-server. When I run the application, authentification and authorization works as expected.
Unfortunately, I am unable to write a WebMvcTest for the use case when the user does not provide a JWT for authentification. In this case I expect a HTTP response with status code 401 (unauthenticated) but I get status code 403 (forbidden). Is this event possible because MockMvc mocks parts of the response processing?
I have successfully written test cases for the following to use cases.
The user provides a JWT with the expected claim => I expect status code 200 ✔
The user provides a JWT without the expected claim => I expect status code 403 ✔
I have tried to follow everything from the Spring Security documentation: https://docs.spring.io/spring-security/reference/servlet/test/index.html
Here is my code.
#WebMvcTest(CustomerController.class)
#ImportAutoConfiguration(classes = {RequestInformationExtractor.class})
#ContextConfiguration(classes = SecurityConfiguration.class)
#Import({TestConfiguration.class, CustomerController.class})
public class PartnerControllerTest {
#Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
#BeforeEach
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
// runs successfully
#Test
void shouldReturnListOfCustomers() throws Exception {
mockMvc.perform(
post("/search")
.contentType(MediaType.APPLICATION_JSON)
.content("{" +
"\"searchKeyword\": \"Mustermann\"" +
"}")
.with(jwt()
.authorities(
new SimpleGrantedAuthority("basic")
)))
.andExpect(status().isOk());
}
// fails: expect 401 but got 403
#Test
void shouldReturn401WithoutJwt() throws Exception {
mockMvc.perform(
post("/search")
.contentType(MediaType.APPLICATION_JSON)
.content("{" +
"\"searchKeyword\": \"Mustermann\"" +
"}"))
.andExpect(status().isUnauthorized());
}
// runs successfully
#Test
void shouldReturn403() throws Exception {
mockMvc.perform(
post("/search")
.contentType(MediaType.APPLICATION_JSON)
.content("{" +
"\"searchKeyword\": \"Mustermann\"" +
"}")
.with(jwt()))
.andExpect(status().isForbidden());
}
}
#org.springframework.boot.test.context.TestConfiguration
public class TestConfiguration {
#Bean
public JwtDecoder jwtDecoder() {
SecretKey secretKey = new SecretKeySpec("dasdasdasdfgsg9423942342394239492349fsd9fsd9fsdfjkldasd".getBytes(), JWSAlgorithm.HS256.getName());
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretKey).build();
return jwtDecoder;
}
}
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/actuator/**").permitAll()
.anyRequest().hasAuthority("Basic")
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("groups");
grantedAuthoritiesConverter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
You probably have a 403 because an exception is thrown before access control is evaluated (CORS or CSRF or something).
For instance, in your security configuration, you disable sessions (session-creation policy to stateless) but not CSRF protection.
Either disable CSRF in your conf (you can because CSRF attacks use sessions) or use MockMvc csrf() post-processor in your tests.
I have many demos of resource-servers with security configuration and tests (unit and integration) in my samples and tutorials. Most have references to my test annotations and boot starters (which enable to define almost all security conf from properties without Java conf), but this one is using nothing from my extensions. You should find useful tips for your security conf and tests there.

Spring Security letting any request go through

I am making an API with Spring boot + Spring Security. I am having issues with Spring Security "letting" any request go through.
What is happening is that I have two endpoints:
users/register/app
users/recoverpasswordbyemail
And I am testing them with Postman, the thing is that if I call one of those endpoints for the first time after the app started without an Authoritation header or a wrong one, it won't let me in and gives a 401 error. However, If I call the other method or the same method again with a correct Authoritation header and then call one of them without the Header again, it'll let me pass. I do not think it's suppossed to work without an auth header.
I am not sure if it is because I have already done some kind of "log in" when I put the correct auth header or if it's a Postman's issue. But I want that every time a call to one of those endpoints a check is done for the user.
This is my Spring config file:
#Configuration #EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserServiceImpl userService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/users/register/app", "/users/recoverpasswordbyemail")
.hasAnyRole("ADMIN", "SADMIN")
.and().httpBasic().authenticationEntryPoint(new NotAuthorizedExceptionMapper())
.and().sessionManagement().disable();
}
#Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userService);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Also every time this happens, the user doesn't get first if exists (however it does when I have a wrong auth header) or whatever as I have in my UsersService:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("Hi");
User user = userRepository.findById(username).get();
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.getRole());
}
This is the URL I use to call the endpoints:
http://localhost:8680/ulises/usersserver/users/register/app
where /ulises/usersserver/ is the servlet context
Has anyone a clue of why this can be? I researched quite a lot but saw nothing that could solve it.
Thank.
Okay, it turns out that a session was being created despite of sessions being disabled. I don't understand why this is happening but using
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
solved my problem.

Spring's basic security overwriting app's configuration

I have a Spring Boot 2 app with Spring security, as follow:
#SpringBootApplication(exclude = [(SecurityAutoConfiguration::class)])
class UntappdCqrsApplication
fun main(args: Array<String>) {
runApplication<UntappdCqrsApplication>(*args)
}
and the configuration class
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class TokenConfiguration(
val jwtTokenProvider: JwtTokenProvider
) : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers("/users/signup").permitAll()
.anyRequest().authenticated()
http.apply(JwtTokenConfigurer(jwtTokenProvider));
}
}
There are two endpoints: POST users/signup and GET users/test.
According to my configuration, /signup should not require authentication and /test should, but both endpoints are accessible without any authentication.
If I add #EnableWebSecurity in my TokenConfiguration class, Spring now generates a default password and both endpoints are now protected.
I think I'm missing something here, but I have no idea what
You haven't provided the source or imports for your JwtTokenProvider or JwtTokenConfigurer classes, but it seems likely that your JwtTokenProvider is throwing an unchecked exception or even directly sending a response on authentication failure. This will prevent permitAll() from ever being triggered.
See my response to a similar question:
https://stackoverflow.com/a/46086769/873590

Modify Spring Security Config at Runtime

I am using the latest Spring Boot + Spring Boot Starter Security for a simple proxy application. The goal is to launch the application with a single route/method:
#RequestMapping(value = "/api/register",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public ResponseEntity<?> register(Registration registration) {
With a security configuration of:
#Override
protected void configure(HttpSecurity http) throws Exception {
this.http = http
.authorizeRequests()
.antMatchers("/api/register").hasAuthority(AuthorityConstants.ADMIN)
.and();
}
public HttpSecurity getHttpSecurity() {
return http;
}
The goal of the application would be to accept registration requests of the form:
{
"route":"/api/foo/bar",
"proxy_location": "http://back-end-server/path/to/resource",
"role": "some-authority"
}
And then the application would add an /api/foo/bar route with a pre-defined method that will proxy (forward) future requests to the backend service.
I know this is a little goofy, the real use-case involves websockets and dynamic creation of topics.
The issue I'm facing is that I cannot seem to update the security configuration after the SecurityConfigurer has completed.
In the code sample above I am caching the HttpSecurity object given to my SecurityConfigurer and then trying to use that object again to configure a new route:
#Inject
private SecurityConfigurer security;
#RequestMapping(value = "/api/register",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public ResponseEntity<?> allowGetAccounts(Registration registration) {
try {
security.getHttpSecurity()
.authorizeRequests()
.antMatchers(registration.getRoute()).hasAuthority(registration.getRole());
...
} catch (Exception e) {
log.error("Updating security failed!", e);
}
return new ResponseEntity<>(null, HttpStatus.OK);
}
Is there any way to update the security configuration dynamically during runtime?
Also, if anyone has any notes on creating websocket topics dynamically that would be appreciated too!
You have several options:
use antchMatcher("/some/path").access("#someBean.hasAccess(authentication)") . This allows you basically use any bean in your application context to apply the validation you need.
Use #PreAuthorize("#someBean.hasAccess(authentication)") on you RequestMapping annotated method. Same idea as before but as an interceptor on the endpoint itself.
Implement your own SecurityExpressionHandler and plug it into http.authorizeRequests().expressionHandler(...).
Implement your own Security filter that handles whatever you need.

Resources