Quantcast
Channel: Pivotal Engineering Journal
Viewing all articles
Browse latest Browse all 219

Faking OAuth2 Single Sign-on in Spring, Two Ways

$
0
0

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. I’ll describe two ways to structure your tests so that they no longer depend on a third-party SSO service.

Let’s assume we’re writing a web application with a controller that uses information gathered from the OAuth2 SSO service. It could be that our controller needs a properly configured Oauth2RestTemplate to make some other request for a protected resource. More commonly, our controller could need to access details about the authenticated user provided by the SSO service, such as a username or email address.

We’ll write a test for the following controller method, which prints the OAuth2 token and username provided by the SSO service:

@RestController
public class TokenController {
    @Autowired
    OAuth2RestTemplate oauthRestTemplate;

    @RequestMapping(path="/api/token", method=RequestMethod.GET)
    public TokenData getAuthenticationInfo() {
        OAuth2Authentication authentication = (OAuth2Authentication) SecurityContextHolder
            .getContext()
            .getAuthentication();
        HashMap<String, String> userDetails = (HashMap<String, String>)authentication
            .getUserAuthentication()
            .getDetails();
        return new AuthenticationInfo(oauthRestTemplate.getAccessToken(), userDetails);
    }

    class AuthenticationInfo {
        private OAuth2AccessToken token;
        private HashMap<String,String> userDetails;

        public AuthenticationInfo(OAuth2AccessToken token, HashMap<String,String> userDetails) {
            this.token = token;
            this.userDetails = userDetails;
        }

        public String getToken() { return this.token.getValue(); }

        public String getUsername() {
            return this.userDetails.get("user_name");
        }
    }
}

Setting up OAuth2 SSO

Since I’ll be focusing on testing with OAuth2 SSO, I won’t spend too much time describing how to properly configure SSO in a Spring Boot application. A more detailed explanation can be found here. In general, though, you’ll need to add the spring-security-oauth and spring-security-test dependencies to your project, add the @EnableOauth2Sso annotation to the class annotated with @SpringBootApplication, and configure your application to use the OAuth2 SSO service for authentication. Here’s the SSO configuration section from a sample application.yml file:

security:
  oauth2:
    client:
      clientId: oauth-client-id
      clientSecret: oauth-client-secret
      accessTokenUri: http://oauthService.com/oauth/token
      userAuthorizationUri: http://oauthService.com/oauth/authorize
      clientAuthenticationScheme: header
    resource:
      userInfoUri: http://oauthService.com/userinfo

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:

@Test
public void testGetAuthenticationInfo() throws Exception {
    MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
            .apply(springSecurity())
            .build();

    mockMvc.perform(MockMvcRequestBuilders.get("/api/token")
            .with(authentication(getOauthTestAuthentication()))
            .sessionAttr("scopedTarget.oauth2ClientContext", getOauth2ClientContext()))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(jsonPath("$.username").value("bwatkins"))
            .andExpect(jsonPath("$.token").value("my-fun-token"));
}

We first create a MockMvc object and configure it with the springSecurity() method. This initializes the Spring MVC testing environment so that it can integrate with the Spring security testing framework. Next, we configure the request so that it uses a fake authentication object (provided by the getOauthTestAuthentication() method). This authentication object describes properties of the authenticated user. Lastly, we inject an Oauth2ClientContext object into the session associated with this request. The client context holds the OAuth2 token; without this object in the session, Spring security will attempt to make a request to obtain the token if the controller object attempts to use it (as in our case).

Let’s take a closer look at some of the setup for our test.

private Authentication getOauthTestAuthentication() {
    return new OAuth2Authentication(getOauth2Request(), getAuthentication());
}

To create the Oauth2Authentication object, we need an Oauth2Request object and an Authentication object. We can create the request like so:

private OAuth2Request getOauth2Request () {
    String clientId = "oauth-client-id";
    Map<String, String> requestParameters = Collections.emptyMap();
    boolean approved = true;
    String redirectUrl = "http://my-redirect-url.com";
    Set<String> responseTypes = Collections.emptySet();
    Set<String> scopes = Collections.emptySet();
    Set<String> resourceIds = Collections.emptySet();
    Map<String, Serializable> extensionProperties = Collections.emptyMap();
    List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("Everything");

    OAuth2Request oAuth2Request = new OAuth2Request(requestParameters, clientId, authorities,
        approved, scopes, resourceIds, redirectUrl, responseTypes, extensionProperties);

    return oAuth2Request;
}

Normally, Spring would create the Oauth2Request object based on the parameters specified in the application.yml file along with parameters returned by the SSO service. Since we are bypassing the authentication step altogether, we’ll have to provide our own, but this gives us an opportunity to modify parameters – especially the approved flag or the list of granted authorities – as needed to support various testing scenarios.

The Authentication object stores details about the logged-in user that would be obtained when Spring makes a request to the SSO service userInfoUri specified in the application configuration. We provide anAuthentication object, configured for our tests, like so:

private Authentication getAuthentication() {
    List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("Everything");

    User userPrincipal = new User("user", "", true, true, true, true, authorities);

    HashMap<String, String> details = new HashMap<String, String>();
    details.put("user_name", "bwatkins");
    details.put("email", "bwatkins@test.org");
    details.put("name", "Brian Watkins");

    TestingAuthenticationToken token = new TestingAuthenticationToken(userPrincipal, null, authorities);
    token.setAuthenticated(true);
    token.setDetails(details);

    return token;
}

Finally, we need to create the Oauth2ClientContext object that will be injected into the session associated with the request. Here, we provide a mock object that provides the OAuth2 token we want whenever it is requested.

private OAuth2ClientContext getOauth2ClientContext () {
    OAuth2ClientContext mockClient = mock(OAuth2ClientContext.class);
    when(mockClient.getAccessToken()).thenReturn(new DefaultOAuth2AccessToken("my-fun-token"));

    return mockClient;
}

With these methods in place, our configured MockMvc object will make a fully authenticated request, including details about the authenticated user and an OAuth2 token. Using this strategy, we can test our application without needing to authenticate with the actual OAuth2 SSO service.

Strategy #2. Fake an OAuth2 SSO Service with WireMock

If you need to write an integration or acceptance test that drives a web browser, and you’re able to useHtmlUnit as your browser, you can build on the previous strategy, bypassing authentication by providing HtmlUnit with a properly configured MockMvc object to which it will delegate HTTP requests. SeeMockMvcHtmlUnitDriverBuilder and this series of blog posts for some strategies on how to accomplish this.

If you’d prefer that your integration or acceptance style tests use a framework likeFluentlenium to drive a headless browser likePhantomJS, then you’ll need a different approach. 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\");
    assertThat(pageSource()).contains("my-fun-token");
}

Our test makes a request to a protected endpoint, so we expect to be redirected to the SSO service’s login page for authentication. After the single sign-on flow is complete, we will be redirected to the endpoint we originally requested, and at that point we can make expectations about the response.

We’ll use WireMock to provide a fake OAuth2 SSO service on port 8077, so we first need to configure our test environment to use it. We provide an application-test.yml file that overrides the existing application properties, configuring Spring Security to use different URIs during testing:

security:
  oauth2:
    client:
      accessTokenUri: http://localhost:8077/oauth/token
      userAuthorizationUri: http://localhost:8077/oauth/authorize
    resource:
      userInfoUri: http://localhost:8077/userinfo

In our test class, we add a rule to start the WireMock server on port 8077:

@Rule
public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().port(8077)
    .extensions(new CaptureStateTransformer()));

Notice, the CaptureStateTransformer extension. This WireMock extension will allow us to examine and modify some of our requests as needed to complete the OAuth2 SSO flow; we’ll get to this in a moment. First, we need to set up the stubs required for the authentication process.

@Before
public void setUp() {
    stubFor(get(urlPathMatching("/oauth/authorize?.*"))
        .willReturn(aResponse()
            .withStatus(200)
            .withHeader("Content-Type", "text/html")
            .withBodyFile("login.html")
            .withTransformers("CaptureStateTransformer")));

    // More stubs later ...
}

When an unauthenticated user requests a protected resource in our app, Spring Security will first redirect that user to the SSO service for authentication, passing several parameters on the query string, including a generated state parameter to guard against CSRF attacks, which we’ll discuss later. The stub for /oauth/authorize?.* handles this redirect request and provides a login page.

This login page can be configured as necessary, but it should mimic the form that the real single sign-on service will use. Here’s a basic example:

<html><body>
Welcome to the login page!<br /><form method="POST" action="/loginSubmit"><input type="text" name="username" /><input type="password" name="password" /><input type="submit" /></form></body></html>

When the form in this login page is submiteed, a POST request will be made to another stubbed endpoint: /loginSubmit.

stubFor(post(urlEqualTo("/loginSubmit"))
    .willReturn(aResponse()
        .withStatus(302)
        .withHeader("Location", "http://localhost:8099/login?code=oauth_code&state=${state-key}")
        .withTransformers("CaptureStateTransformer")));

At this point, a real SSO service would check that the user has successfully authenticated before proceeding; our fake service will assume the credentials are fine and continue with the single sign-on flow.

The SSO service next issues a redirect to a /login endpoint within our application. Since we are using Spring Security, that login endpoint is already configured and maintained automatically in our application. When redirecting to this endpoint, the SSO service provides a code parameter and a state parameter. The code parameter is a short string generated by the SSO service; for the purposes of our test it can be anything we want. The state parameter must match the value provided earlier in the request to/oauth/authorize. For this reason, our fake SSO service must keep track of the state parameter between these requests. We provide a WireMock extension (CaptureStateTransformer) that records the state value from the request to /oauth/authorize and adds it to the redirect to /login.

class CaptureStateTransformer extends ResponseTransformer {
  private String state = null;

  @Override
  public ResponseDefinition transform(Request request, ResponseDefinition responseDef, FileSource files) {
      // Capture the state parameter from the /oauth/authorize request
      if (state == null && request.queryParameter("state") != null) {
          state = request.queryParameter("state").firstValue();
      }

      // Add the state parameter to the /login redirect
      if (responseDef.getHeaders().getHeader("Location").isPresent()) {
          String redirectLocation = responseDef.getHeaders().getHeader("Location").firstValue();
          return ResponseDefinition.redirectTo(redirectLocation.replace("${state-key}", this.state));
      }

      return responseDef;
  }

  @Override
  public String name() {
      return "CaptureStateTransformer";
  }

  @Override
  public boolean applyGlobally() {
      return false;
  }
}

This WireMock extension examines requests on the two stubs tagged with CaptureStateTransformer. If there is a state parameter among the request parameters (as there is in the request to /oauth/authorize), that value is stored. If there is a Location header (as there is in the redirect to /login), it replaces the variable ${state-key} with the stored value. This allows us to capture the state key in the original request for the login form and pass it back as part of the redirect after the login form is submitted.

We need to provide two more stubs. The request to /oauth/token must return the OAuth2 access token, and the request to/userInfo will return data about the authenticated user. The responses here can be configured as necessary for the purposes of the test.

stubFor(post(urlEqualTo("/oauth/token"))
    .willReturn(aResponse()
        .withStatus(200)
        .withHeader("Content-Type", "application/json")
        .withBody("{\"access_token\":\"my-fun-token\"}")));

stubFor(get(urlPathEqualTo("/userinfo"))
    .willReturn(aResponse()
        .withStatus(200)
        .withHeader("Content-Type", "application/json")
        .withBody("{\"user_id\":\"my-id\",\"user_name\":\"bwatkins\",\"email\":\"bwatkins@test.com\"}")));

With these four stubs in place, we’ve provided a fake OAuth2 SSO service to use during testing. Using this approach, we can write acceptance tests that describe the bahvior of our application when errors are encountered during the single sign-on process. Better yet, we can write tests that are faster and more resilient since they no longer depend on a third-party service for authentication.


Viewing all articles
Browse latest Browse all 219

Trending Articles