9

Thanks a lot in advance for reading this question.

Setup

I am using:

  • spring-security-oauth2:2.0.7.RELEASE
  • spring-cloud-security:1.0.1.RELEASE
  • spring-session:1.0.1.RELEASE

and would have a question regarding the persistence of spring-security-oauth2 OAuth2ClientContext in a Redis datastore when using spring-session (via @EnableRedisHttpSession) in a Single-Sign-On (@EnableOAuth2Sso), reverse proxy (@EnableZuulProxy) gateway.

Problem

It seems to me that the SessionScoped JdkDynamicAopProxied DefaultOAuth2ClientContext created in org.springframework.cloud.security.oauth2.client.OAuth2ClientAutoConfiguration is not correctly persisted in the Redis datastore.

@Configuration
@ConditionalOnBean(OAuth2SsoConfiguration.class)
@ConditionalOnWebApplication
protected abstract static class SessionScopedConfiguration extends BaseConfiguration {

    @Bean
    @Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
    public OAuth2ClientContext oauth2ClientContext() {
        return new DefaultOAuth2ClientContext(accessTokenRequest);
    }

}

Debugging the creation of the oauth2ClientContext without @EnableRedisHttpSession shows that (as expected) the bean will be instantiated once per client session and stored in the HttpSession. This instance will then be reused to store the fetched OAuth2 bearerToken details in addition to storing the OAuth2 accessToken in Spring SecurityContext's org.springframework.security.core.Authentication.

However, once using @EnableRedisHttpSession, the oauth2ClientContext bean will be first created on the session creation but also later on (while still using the same client session). Debugging the Redis client session content confirms that oauth2ClientContext is not correctly being persisted by session creation:

Before we retrieve the OAuth2 bearerToken (NO SpringContext, NO scopedTarget.oauth2ClientContext):

~$ redis-cli hkeys "spring:session:sessions:17c5e80b-390c-4fd6-b5f9-a6f225dbe8ea"
1) "maxInactiveInterval"
2) "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN"
3) "lastAccessedTime"
4) "creationTime"
5) "sessionAttr:SPRING_SECURITY_SAVED_REQUEST"

After we retrieved the OAuth2 bearerToken (SpringContext persisted, but NO scopedTarget.oauth2ClientContext):

~$ redis-cli hkeys "spring:session:sessions:844ca2c4-ef2f-43eb-b867-ca6b88025c8b"
1) "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN"
2) "lastAccessedTime"
3) "creationTime"
4) "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION"
5) "sessionAttr:SPRING_SECURITY_CONTEXT"
6) "maxInactiveInterval"

If we now try to access one of the configurer Zuul's routes (therefore requiring to call org.springframework.security.oauth2.client.DefaultOAuth2ClientContext#getAccessToken), another instance of oauth2ClientContext will be created (since not persisted in Redis, with a null AccessToken.

Funnily enough, this instance will later be persisted in Redis (but a null instance is persisted since the AccessToken is not re-asked for):

~$ redis-cli hkeys "spring:session:sessions:c7120835-6709-4c03-8d2c-98f830ed6104"
1) "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN"
2) "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION"
3) "sessionAttr:scopedTarget.oauth2ClientContext"
4) "sessionAttr:SPRING_SECURITY_CONTEXT"
5) "maxInactiveInterval"
6) "creationTime"
7) "lastAccessedTime"
8) "sessionAttr:org.springframework.web.context.request.ServletRequestAttributes.DESTRUCTION_CALLBACK.scopedTarget.oauth2ClientContext" 

Creating a Simple ScopedProxyMode.TARGET_CLASS Injected bean worked as expected however with the bean being persisted correctly in Redis.

public class HelloWorldService implements Serializable {

    public HelloWorldService(){
        System.out.println("HelloWorldService created");
    }

    private String name = "World";

    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name=name;
    }

    public String getHelloMessage() {
        return "Hello " + this.name;
    }
}

@Configuration
public class AppConfig {

    private SecureRandom random = new SecureRandom();

    @Bean
    @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public HelloWorldService myHelloService(){
        HelloWorldService s = new HelloWorldService();
        String name = new BigInteger(130, random).toString(32);
        System.out.println("name = " + name);
        s.setName(name);
        System.out.println("Resource HelloWorldService created = " + s);
        return s;
    }
}

Example

The described problem can be reproduced in @dave-syer example for an OAuth2 reverse proxy gateway by adding the following dependencies:

<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session</artifactId>
  <version>1.0.1.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

as well as the @EnableRedisHttpSession annotation in UiApplication.

Question

Should we ignore org.springframework.cloud.security.oauth2.client.OAuth2ClientAutoConfiguration from AutoConfiguration and manually create a oauth2ClientContext with a different setup to enable spring-session persistence in Redis? If so, can you please provide an example?

Otherwise: how to persist oauth2ClientContext in Redis?

Many in advance to anyone reading this question an trying to help.

Jérémie
  • 557
  • 4
  • 15
  • Jeremie did you configured oauth2 + spring session to store session between many applications? Can you share some deatils? – bilak May 26 '16 at 11:26
  • @bilak Oauth2 unfortunately not many (just a PoC, using JWOTs). But Spring-Session, more often, yes. What kind of details would you be interested in? (I cannot share the whole code base, since the Applications are proprietary) – Jérémie May 26 '16 at 22:37
  • I'm trying to create sample application with gateway as a entry point for all applications [look here](https://github.com/bilak/oauth2-sso-demo/tree/jdbc-and-gateway). What I don't follow is the fact how to share csrf/sessions data between applications. I don't want to use jwt so I was thinking about jdbc oauth2 + spring session. But I'm currently stuck at the point with authentication ...when I log in to UAA there is problem with csrf...it's not shared and I don't know how to do that at the moment. – bilak May 27 '16 at 07:17

3 Answers3

5

There's a known issue there (https://github.com/spring-projects/spring-session/issues/129 and https://github.com/spring-projects/spring-boot/issues/2637). You can work around it by adding a RequestContextFilter.

Dave Syer
  • 56,583
  • 10
  • 155
  • 143
  • Many thanks @dave-syer for taking the time to analyse this question. I will have a look at a solution using a `RequestContextFilter` and report... – Jérémie Jun 23 '15 at 08:36
4

@dave-syer hint was correct.

I post here the configuration which can be used to setup the RequestContextFilter and enable spring-session persistence of spring-security-oauth objects. In case this can help someone...

@Configuration
public class RequestContextFilterConfiguration {

    @Bean
    @ConditionalOnMissingBean(RequestContextFilter.class)
    public RequestContextFilter requestContextFilter() {
        return new RequestContextFilter();
    }

    @Bean
    public FilterRegistrationBean requestContextFilterChainRegistration(
            @Qualifier("requestContextFilter") Filter securityFilter) {
        FilterRegistrationBean registration = new FilterRegistrationBean(securityFilter);
        registration.setOrder(SessionRepositoryFilter.DEFAULT_ORDER + 1);
        registration.setName("requestContextFilter");
        return registration;
    }
}
Jérémie
  • 557
  • 4
  • 15
2

I came across this post and I have the exact same issue with some minor differences:

  • my application is not a Spring Boot application
  • I use JDBC persistence instead of Redis

However, and this might save some hours of future readers, the above solution also worked for me. Since I'm not using Spring Boot, I'll publish the solution here to be applied in a non Spring Boot application using web.xml configuration.

The "trick" is to define in the web.xml the RequestContextFilter. As far as my testing goes I have not seen any border effects of having both the request context filter living aside the request context listener.

What is important is the ordering of the filters. You need to define the filters in this order in your web.xml:

  • session repository filter
  • request context filter
  • security filter

So something like:

<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
    <filter-name>requestContextFilter</filter-name>
    <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>requestContextFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>
        org.springframework.web.filter.DelegatingFilterProxy
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

If this helps you save a few hours of digging into Stackoverflow and other web sites, it makes my day.