When writing a Java Spring web application that uses an OAuth2 single sign-on (SSO) service for
authentication, testing can be difficult, especially if the SSO service is provided by a third party.
In such cases, it may be more expedient to fake the SSO service in your tests.
This article describes three ways to structure your tests so that they no longer depend on a third-party SSO service:
- Bypass authentication entirely using MockMvc.
- Use WireMock to simulate an OAuth2 SSO service.
- Use MockLab’s hosted OAuth2 / OpenID Connect simulation.
Setting up OAuth2 SSO
To enable OAuth2 login in your Spring Boot app, you need to add the spring-boot-starter-oauth2-client
dependency.
Autoconfiguration will handle most of the OAuth2 plumbing supported by a little bit of configuration, which
we’ll get into shortly
Let’s assume we’re writing a web application with a controller that uses user information gathered from the
OAuth2 SSO service:
@SpringBootApplication
@RestController
public class OAuth2ExampleApp extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(a -> a
.antMatchers("/", "/error", "/css/**", "/js/**", "/images/**", "/assets/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login();
}
@GetMapping("/api/user")
public Map<String, String> getUserInfo(@AuthenticationPrincipal OAuth2User user) {
Map<String, String> userInfo = new HashMap<>();
userInfo.put("email", user.getAttribute("email"));
userInfo.put("id", user.getAttribute("sub"));
return userInfo;
}
public static void main(String[] args) {
SpringApplication.run(OAuth2ExampleApp.class, args);
}
}
In order to test fetching of user details, we either need to authenticate a user against the app,
or convince Spring that we’ve already done this. The following sections show three ways this can be achieved.
Strategy #1: Bypass Authentication with MockMvc
We want to write a test that describes the behavior of our controller method without actually contacting the
third-party SSO service. For our first attempt at achieving this goal, we’ll structure our test so that it
bypasses the authentication process altogether. We’ll use Spring’s MockMvc
class to make
requests on behalf of a user who appears to have already been authenticated.
Here’s our test class:
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.MOCK,
classes = OAuth2ExampleApp.class)
@AutoConfigureMockMvc
public class MockMvcOAuth2Test {
@Autowired
private MockMvc mockMvc;
@Test
public void testGetAuthenticationInfo() throws Exception {
OAuth2AuthenticationToken principal = buildPrincipal();
MockHttpSession session = new MockHttpSession();
session.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
new SecurityContextImpl(principal));
mockMvc.perform(MockMvcRequestBuilders.get("/api/user")
.session(session))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$.id").value("my-id"));
}
private static OAuth2AuthenticationToken buildPrincipal() {
Map<String, Object> attributes = new HashMap<>();
attributes.put("sub", "my-id");
attributes.put("email", "[email protected]");
List<GrantedAuthority> authorities = Collections.singletonList(
new OAuth2UserAuthority("ROLE_USER", attributes));
OAuth2User user = new DefaultOAuth2User(authorities, attributes, "sub");
return new OAuth2AuthenticationToken(user, authorities, "whatever");
}
}
What we’re doing here is programmatically creating the authentication token
that would normally be created from the data fetched from the SSO service.
We place this into the session so that it is available to the Spring Security
filter chain. The presence of a valid token means that the protected resource /api/user
is
served, rather than a redirect to the login page.
Strategy #2. Fake an OAuth2 SSO Service with WireMock
If you want your tests use a real browser via an automation framework like
Fluentlenium, MockMvc
won’t cut it and you’ll need a different strategy.
For this second approach, instead of bypassing authentication altogether, we’ll
use WireMock to provide our own fake OAuth2 Single Sign-on service
for use during our tests.
Here’s our integration test, built with Fluentlenium:
@Test
public void testShowAuthenticationInfo () {
goTo("http://localhost:8099/api/token");
fill("input[name='username']").with("bwatkins");
fill("input[name='password']").with("password");
find("input[type='submit']").click();
assertThat(pageSource()).contains("username":""bwatkins"""");