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

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.

Related

Spring security with basic auth: password is verified only the first time

I'm using Spring security with Basic auth
I have the following configuration:
#Configuration
public class SecurityConfiguration {
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
return http.build();
}
}
And also a service implementing UserDetailsService:
#Service
public class UserService implements UserDetailsService { //... }
This service reads the credentials from the database. I created some credentials username/password.
I'm using Postman to test it and I have the following results (in that order):
1) GET /endpoint using username/wrong_password -> 401
2) GET /endpoint using username/password -> 200
3) GET /endpoint using username/wrong_password -> 200
I expect the last call to return 401, but once it returns 200, it continues returning 200.
Any advice?
Thanks!
It was not an Spring Security issue. I disabled the "cookie jar" from Postman and it worked.
Answer update based on comments: If you don't want to support sessions at all in Spring Security you should set the session creation policy to NEVER (more info here)

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

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.

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.

Spring Cloud Gateway - Intercept under hood request/response to Keycloak IDP

We are implementing a Spring Cloud Gateway application (with Webflux) that is mediating the OAuth2 authentication with Keycloak.
SCG checks if the Spring Session is active: if not, redirects to Keycloak login page and handles the response from the IDP. This process is executed out-of-the-box by the framework itself.
Our needs is to intercept the IDP Keycloak response in order to retrieve a field from the response payload.
Do you have any advices that will help us to accomplish this behavior?
Thanks!
You can implement ServerAuthenticationSuccessHandler:
#Component
public class AuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private ServerRedirectStrategy redirectStrategy;
public AuthenticationSuccessHandler(AuthenticationService authenticationService) {
redirectStrategy = new DefaultServerRedirectStrategy();
}
#Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
if(authentication instanceof OAuth2AuthenticationToken) {
//Your logic here to retrieve oauth2 user info
}
ServerWebExchange exchange = webFilterExchange.getExchange();
URI location = URI.create(httpRequest.getURI().getHost());
return redirectStrategy.sendRedirect(exchange, location);
}
}
And update your security configuration to include success handler:
#Configuration
public class SecurityConfiguration {
private AuthenticationSuccessHandler authSuccessHandler;
public SecurityConfiguration(AuthenticationSuccessHandler authSuccessHandler) {
this.authSuccessHandler = authSuccessHandler;
}
#Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchange -> exchange
//other security configs
.anyExchange().authenticated()
.and()
.oauth2Login(oauth2 -> oauth2
.authenticationSuccessHandler(authSuccessHandler)
);
return http.build();
}
}

Spring Boot webservice (REST) - How to change JUnit 5 tests from basic authentication to OAuth2 (Keycloak)

I have a Spring Boot webservice with REST controllers and with basic authentication (username and password).
On this base I developed JUnit 5 test.
Now I switch to OAuth2, currently trying the Resource Owner Password Credentials grant type.
What do I need to change on my JUnit 5 tests to run now with OAuth2?
Of course, before running my new tests with OAuth2 I have to start first Keycloak, afterwards the tests.
Following is my setup for the current basic authentication and the new OAuth2.
BASIC AUTHENTICATION (old implementation)
On my webservice side the web security config class looks like following:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/articles/**").hasRole("ADMIN")
// More antMatchers...
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.formLogin().disable();
}
#Bean
#Override
public UserDetailsService userDetailsService() {
UserDetails admin = User
.withUsername("admin")
.password("{noop}" + "admin123")
.roles("ADMIN")
.build();
// More users...
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(admin);
...
return userDetailsManager;
}
}
For the JUnit 5 tests I allways use the user admin, for example
#SpringBootTest
#AutoConfigureMockMvc
#WithUserDetails(value = "admin")
#TestInstance(Lifecycle.PER_CLASS)
public class MyRestControllerMockMvcTest {
#Autowired
private MockMvc mockMvc;
#BeforeAll
public void init(ApplicationContext appContext) throws Exception {
TestUtils.setupSecurityContext(appContext);
// some initialization
}
#AfterAll
public void cleanup(ApplicationContext appContext) throws Exception {
TestUtils.setupSecurityContext(appContext);
// some cleanup
}
#Test
public void getSomeInformationFromMyRestController() throws Exception {
MvcResult mvcResult = TestUtils.performGet(mockMvc, "...REST controller endpoint...", status().isOk());
MockHttpServletResponse response = mvcResult.getResponse();
ObjectMapper objectMapper = new ObjectMapper();
... = objectMapper.readValue(response.getContentAsString(), ...);
assertNotNull(...);
}
}
public class TestUtils {
public static void setupSecurityContext(ApplicationContext appContext) {
UserDetailsService uds = (UserDetailsService) appContext.getBean("userDetailsService");
UserDetails userDetails = uds.loadUserByUsername ("admin");
Authentication authToken = new UsernamePasswordAuthenticationToken (userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
public static MvcResult performGet(MockMvc mockMvc, String endpoint, ResultMatcher status) throws Exception {
MvcResult mvcResult = mockMvc.perform(get(endpoint))
.andDo(print())
.andExpect(status)
.andReturn();
return mvcResult;
}
}
Looking right now on the test setup in #BeforeAll and #AfterAll I'm not sure all of a sudden if I have to do
TestUtils.setupSecurityContext(appContext);
because now I use
#WithUserDetails(value = "admin")
#TestInstance(Lifecycle.PER_CLASS)
on the class. Just curious if the tests would still run without TestUtils.setupSecurityContext(appContext);, will try.
OAUTH2 (new implementation, replacing basic authentication above)
application.properties
...
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8183/auth/realms/myrealm/protocol/openid-connect/certs
With OAuth2 I changed the web security config class in my webservice (resource server) as following:
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/articles/**").hasRole("ADMIN")
// More antMatchers...
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter())
;
return httpSecurity.build();
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new MyRoleConverter());
return jwtAuthenticationConverter;
}
public class MyRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
#Override
public Collection<GrantedAuthority> convert(final Jwt jwt) {
jwt.getClaims().get("realm_access");
// Create roles
return ...;
}
}
}
My users are now defined in Keycloak.
Keycloak is configured to use Resource Owner Password Credentials.
#jzheaux is right (sure, he's spring-security team member...).
Changes will occure in your security configuration but the test won't change ... for the most part: You'll probably want to have an Authentication of the right type in your test security-context.
If your new security configuration populates security-context with JwtAuthenticationToken, it would be nice to have JwtAuthenticationToken in test security-context too. #WithUserDetails(value = "admin") won't build JwtAuthenticationToken.
You should have a look at this lib I wrote and specifically at #WithMockJwtAuth. Usage is demonstrated there:
#Test
#WithMockJwtAuth(authorities = "ROLE_AUTHORIZED_PERSONNEL", claims = #OpenIdClaims(sub = "Ch4mpy"))
public void greetJwtCh4mpy() throws Exception {
api.get("/greet").andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
P.S.
You'll find in this same git repo samples for other kind of Authentication better adapted to OIDC than JwtAuthenticationToken like KeycloakAuthenticationToken (written by Keycloak team for Keycloak exclusively) or OidcAuthentication (written by myself for any OpenID Connect complient authorization server), along with #WithMockKeycloakAuth and #WithMockOidcAuth

Resources