14

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

Emmanuel F
  • 1,125
  • 1
  • 15
  • 34
Juan Vega
  • 1,030
  • 1
  • 16
  • 32

3 Answers3

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;
  }
}
grg
  • 5,023
  • 3
  • 34
  • 50
Juan Vega
  • 1,030
  • 1
  • 16
  • 32
  • does this handle automatically refreshing the access token with a refresh token? – PragmaticProgrammer Oct 29 '18 at 22:00
  • 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. – Juan Vega Oct 30 '18 at 12:32
  • Iam getting an error The blank final field userInfoRestTemplateFactory may not have been initialized – SAMUEL Aug 07 '19 at 05:29
  • @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. – Juan Vega Aug 07 '19 at 14:39
  • @JuanVega Did you find any other better way to do it ? – Dharm Feb 25 '21 at 12:20
  • @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. – Juan Vega Feb 25 '21 at 14:27
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.

Andy
  • 65
  • 1
  • 9
  • 1
    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. – Juan Vega Aug 22 '17 at 13:02
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:

Community
  • 1
  • 1
Deepak Kumar
  • 1,669
  • 3
  • 16
  • 36
  • 2
    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 – Juan Vega Jul 06 '17 at 16:01
  • @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. – abedurftig Oct 16 '18 at 12:05
  • 1
    @dasnervtdoch I just added a response with the code we use. – Juan Vega Oct 16 '18 at 21:27
  • 1
    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. – The Coder Feb 13 '19 at 21:49