Spring

Faking OAuth2 Single Sign-on in Spring, 3 Ways

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:

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"""");