Store token from OAuth2 server in cookie using Spring OAuth

Juan Vega picture Juan Vega · Jul 5, 2017 · Viewed 16.5k times · Source

Is there any configuration provided by Spring OAuth2 that does the creation of a cookie with the opaque or JWT token? The configuration that I've found on the Internet so far describes the creation of an Authorization Server and a client for it. In my case the client is a gateway with an Angular 4 application sitting on top of it in the same deployable. The frontend makes requests to the gateway that routes them through Zuul. Configuring the client using @EnableOAuth2Sso, an application.yml and a WebSecurityConfigurerAdapter makes all the necessary requests and redirects, adds the information to the SecurityContext but stores the information in a session, sending back a JSESSIONID cookie to the UI.

Is there any configuration or filter needed to create a cookie with the token information and then use a stateless session that I can use? Or do I have to create it myself and then create a filter that looks for the token?

    @SpringBootApplication
    @EnableOAuth2Sso
    @RestController
    public class ClientApplication extends WebSecurityConfigurerAdapter{

        @RequestMapping("/user")
        public String home(Principal user) {
            return "Hello " + user.getName();
        }

        public static void main(String[] args) {
            new SpringApplicationBuilder(ClientApplication.class).run(args);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .antMatcher("/**").authorizeRequests()
                    .antMatchers("/", "/login**", "/webjars/**").permitAll()
                    .anyRequest()
                    .authenticated()
                    .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }


    server:
      port: 9999
      context-path: /client
    security:
      oauth2:
        client:
          clientId: acme
          clientSecret: acmesecret
          accessTokenUri: http://localhost:9080/uaa/oauth/token
          userAuthorizationUri: http://localhost:9080/uaa/oauth/authorize
          tokenName: access_token
          authenticationScheme: query
          clientAuthenticationScheme: form
        resource:
          userInfoUri: http://localhost:9080/uaa/me

Answer

Juan Vega picture Juan Vega · Oct 16, 2018

I ended up solving the problem by creating a filter that creates the cookie with the token and adding two configurations for Spring Security, one for when the cookie is in the request and one for when it isn't. I kind of think this is too much work for something that should be relatively simple so I'm probably missing something in how the whole thing is supposed to work.

public class TokenCookieCreationFilter extends OncePerRequestFilter {

  public static final String ACCESS_TOKEN_COOKIE_NAME = "token";
  private final UserInfoRestTemplateFactory userInfoRestTemplateFactory;

  @Override
  protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
    try {
      final OAuth2ClientContext oAuth2ClientContext = userInfoRestTemplateFactory.getUserInfoRestTemplate().getOAuth2ClientContext();
      final OAuth2AccessToken authentication = oAuth2ClientContext.getAccessToken();
      if (authentication != null && authentication.getExpiresIn() > 0) {
        log.debug("Authentication is not expired: expiresIn={}", authentication.getExpiresIn());
        final Cookie cookieToken = createCookie(authentication.getValue(), authentication.getExpiresIn());
        response.addCookie(cookieToken);
        log.debug("Cookied added: name={}", cookieToken.getName());
      }
    } catch (final Exception e) {
      log.error("Error while extracting token for cookie creation", e);
    }
    filterChain.doFilter(request, response);
  }

  private Cookie createCookie(final String content, final int expirationTimeSeconds) {
    final Cookie cookie = new Cookie(ACCESS_TOKEN_COOKIE_NAME, content);
    cookie.setMaxAge(expirationTimeSeconds);
    cookie.setHttpOnly(true);
    cookie.setPath("/");
    return cookie;
  }
}

/**
 * Adds the authentication information to the SecurityContext. Needed to allow access to restricted paths after a
 * successful authentication redirects back to the application. Without it, the filter
 * {@link org.springframework.security.web.authentication.AnonymousAuthenticationFilter} cannot find a user
 * and rejects access, redirecting to the login page again.
 */
public class SecurityContextRestorerFilter extends OncePerRequestFilter {

  private final UserInfoRestTemplateFactory userInfoRestTemplateFactory;
  private final ResourceServerTokenServices userInfoTokenServices;

  @Override
  public void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException {
    try {
      final OAuth2AccessToken authentication = userInfoRestTemplateFactory.getUserInfoRestTemplate().getOAuth2ClientContext().getAccessToken();
      if (authentication != null && authentication.getExpiresIn() > 0) {
        OAuth2Authentication oAuth2Authentication = userInfoTokenServices.loadAuthentication(authentication.getValue());
        SecurityContextHolder.getContext().setAuthentication(oAuth2Authentication);
        log.debug("Added token authentication to security context");
      } else {
        log.debug("Authentication not found.");
      }
      chain.doFilter(request, response);
    } finally {
      SecurityContextHolder.clearContext();
    }
  }
}

This is the configuration for when the cookie is in the request.

@RequiredArgsConstructor
  @EnableOAuth2Sso
  @Configuration
  public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final UserInfoRestTemplateFactory userInfoRestTemplateFactory;
    private final ResourceServerTokenServices userInfoTokenServices;

/**
 * Filters are created directly here instead of creating them as Spring beans to avoid them being added as filters      * by ResourceServerConfiguration security configuration. This way, they are only executed when the api gateway      * behaves as a SSO client.
 */
@Override
protected void configure(final HttpSecurity http) throws Exception {
  http
    .requestMatcher(withoutCookieToken())
      .authorizeRequests()
    .antMatchers("/login**", "/oauth/**")
      .permitAll()
    .anyRequest()
      .authenticated()
    .and()
      .exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
    .and()
      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and()
      .csrf().requireCsrfProtectionMatcher(csrfRequestMatcher()).csrfTokenRepository(csrfTokenRepository())
    .and()
      .addFilterAfter(new TokenCookieCreationFilter(userInfoRestTemplateFactory), AbstractPreAuthenticatedProcessingFilter.class)
      .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
      .addFilterBefore(new SecurityContextRestorerFilter(userInfoRestTemplateFactory, userInfoTokenServices), AnonymousAuthenticationFilter.class);
}

private RequestMatcher withoutCookieToken() {
  return request -> request.getCookies() == null || Arrays.stream(request.getCookies()).noneMatch(cookie -> cookie.getName().equals(ACCESS_TOKEN_COOKIE_NAME));
}

And this is the configuration when there is a cookie with the token. There is a cookie extractor that extends the BearerTokenExtractor functionality from Spring to search for the token in the cookie and an authentication entry point that expires the cookie when the authentication fails.

@EnableResourceServer
  @Configuration
  public static class ResourceSecurityServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(final ResourceServerSecurityConfigurer resources) {
      resources.tokenExtractor(new BearerCookiesTokenExtractor());
      resources.authenticationEntryPoint(new InvalidTokenEntryPoint());
    }

    @Override
    public void configure(final HttpSecurity http) throws Exception {
      http.requestMatcher(withCookieToken())
        .authorizeRequests()
        .... security config
        .and()
        .exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .logout().logoutSuccessUrl("/your-logging-out-endpoint").permitAll();
    }

    private RequestMatcher withCookieToken() {
      return request -> request.getCookies() != null && Arrays.stream(request.getCookies()).anyMatch(cookie -> cookie.getName().equals(ACCESS_TOKEN_COOKIE_NAME));
    }

  }

/**
 * {@link TokenExtractor} created to check whether there is a token stored in a cookie if there wasn't any in a header
 * or a parameter. In that case, it returns a {@link PreAuthenticatedAuthenticationToken} containing its value.
 */
@Slf4j
public class BearerCookiesTokenExtractor implements TokenExtractor {

  private final BearerTokenExtractor tokenExtractor = new BearerTokenExtractor();

  @Override
  public Authentication extract(final HttpServletRequest request) {
    Authentication authentication = tokenExtractor.extract(request);
    if (authentication == null) {
      authentication = Arrays.stream(request.getCookies())
        .filter(isValidTokenCookie())
        .findFirst()
        .map(cookie -> new PreAuthenticatedAuthenticationToken(cookie.getValue(), EMPTY))
        .orElseGet(null);
    }
    return authentication;
  }

  private Predicate<Cookie> isValidTokenCookie() {
    return cookie -> cookie.getName().equals(ACCESS_TOKEN_COOKIE_NAME);
  }

}

/**
 * Custom entry point used by {@link org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter}
 * to remove the current cookie with the access token, redirect the browser to the home page and invalidate the
 * OAuth2 session. Related to the session, it is invalidated to destroy the {@link org.springframework.security.oauth2.client.DefaultOAuth2ClientContext}
 * that keeps the token in session for when the gateway behaves as an OAuth2 client.
 * For further details, {@link org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2RestOperationsConfiguration.SessionScopedConfiguration.ClientContextConfiguration}
 */
@Slf4j
public class InvalidTokenEntryPoint implements AuthenticationEntryPoint {

  public static final String CONTEXT_PATH = "/";

  @Override
  public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
    log.info("Invalid token used. Destroying cookie and session and redirecting to home page");
    request.getSession().invalidate(); //Destroys the DefaultOAuth2ClientContext that keeps the invalid token
    response.addCookie(createEmptyCookie());
    response.sendRedirect(CONTEXT_PATH);
  }

  private Cookie createEmptyCookie() {
    final Cookie cookie = new Cookie(TokenCookieCreationFilter.ACCESS_TOKEN_COOKIE_NAME, EMPTY);
    cookie.setMaxAge(0);
    cookie.setHttpOnly(true);
    cookie.setPath(CONTEXT_PATH);
    return cookie;
  }
}