16

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

3 Answers 3

7

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;
  }
}
Sign up to request clarification or add additional context in comments.

6 Comments

does this handle automatically refreshing the access token with a refresh token?
no, it does not. It was created for an application that didn't have any problem just creating a long lived access token directly instead of having to deal with refreshing it every now and then. The refresh token needs to be stored securely somewhere anyway, storing it as another cookie wouldn't had improved the implementation and would had made it more cumbersome. The cookie created is a HttpOnly one so XSS should be prevented in most cases and in case of theft the token can be invalidated. The implementation doesn't show it but it is configured to verify the token for every request.
Iam getting an error The blank final field userInfoRestTemplateFactory may not have been initialized
@SamuelJMathew That's weird. The bean should be created by @EnableOAuth2Sso, specifically by ResourceServerTokenServicesConfiguration.class that is imported by the previous one. Check if you have any other config that may cause the problem. There is a @ConditionalOnMissingBean(AuthorizationServerEndpointsConfiguration.class) on ResourceServerTokenServicesConfiguration so verify you haven't created it somewhere else. Also, the example uses Lombok to create the constructor. It's weird the compiler doesn't complain of a not initialised final field.
@Dharm not really and that was done on a project I don't maintain anymore. I'm not sure but I think there are other Spring Boot libraries that deal with this now so I'm not sure whether this is still relevant.
|
2

I believe that Spring's default position on this is that we should all use HTTP session storage, using Redis (or equiv) for replication if required. For a fully stateless environment that will clearly not fly.

As you have found, my solution was to add pre-post filters to strip and add cookies where required. You should also look at OAuth2ClientConfiguration.. this defines the session scoped bean OAuth2ClientContext. To keep things simple I altered the auto config and made that bean request scoped. Just call setAccessToken in the pre filter that strips the cookie.

1 Comment

I personally find the Spring implementation very confusing. I found the session scoped client context by chance while investigating why there was a JSSESSIONID and not a token in the browser. Even the use of a JWT seems overkilled when you have to code a blacklist or something complicated to be able to invalidate it. I finally discarded JWT and instead decided to go for an opaque token that is validated for every request with a RemoteTokenService that adds the user principal to Spring Security. In the browser I store the token in a cookie HttpOnly and Secure to allow long sessions.
1

Make sure you have imported these classes present in javax.servlet:

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse; 

Initialize cookie like this:

Cookie jwtCookie = new Cookie(APP_COOKIE_TOKEN, token.getToken());
jwtCookie.setPath("/");
jwtCookie.setMaxAge(20*60);
//Cookie cannot be accessed via JavaScript
jwtCookie.setHttpOnly(true);

Add cookie in HttpServletResponse:

response.addCookie(jwtCookie);

If you are using angular 4 and spring security+boot , then this github repo can become a big help:

Reference blog for this repo is:

4 Comments

Thanks but I was looking for a way to configure Spring OAuth to do it automatically. I ended up creating the cookie manually with a filter, basically doing something similar to what you describe. To me it sounds weird that Spring OAuth allows you to configure everything and make all the redirects to get the token but in the end it just stores it in an HttpSession. I was searching for a filter or a configuration that injected a filter that created something similar to what it does to provide the JSESSIONID
@JuanVega, I am struggeling with this for a few days now. Have you found a solid solution. Could you provide a Git repo or some code? Appreciate it.
@dasnervtdoch I just added a response with the code we use.
Spring framework don't give any readymade approach for this. Like it handles JSESSIONID. I talked to a Spring security team guy, he told that using Filter is the right and the only way. Nor they are planning to implement this feature into the security project. As this may cause some security vulnerabilties.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.