spring-securityredisoauth-2.0spring-security-oauth2spring-session

Spring Session/Redis and Oauth2 not working together


Oauth2 and Redis will not play well together. As soon as I'm enabling Spring Session, two session IDs are created after I have been authenticated (OIDC) and sent back to the application — one JSESSIONID from Redis and another from Spring Security Oauth. As soon as I'm disabling Redis/Spring Session, everything works very well.

I have created a very small Maven application which can be downloaded from: http://folk.uio.no/erlendfg/oidc/oidc.zip

If I run the application by Jetty and Redis on localhost, I'm able to reproduce the problem locally. As shown in the screenshot from Firefox, two session cookies are created: http://folk.uio.no/erlendfg/oidc/two-sessions.png

I have followed Baeldung's guide, but made some small changes to make the application compatible with our OIDC provider. https://www.baeldung.com/spring-security-openid-connect

All these classes are available in the zip file (see link above). The most important ones are:

RedisConfiguration.java

@Configuration
@EnableRedisHttpSession(redisNamespace = "oidc", maxInactiveIntervalInSeconds = 10800)
public class RedisConfiguration {

  @Bean
  public LettuceConnectionFactory connectionFactory() {
    return new LettuceConnectionFactory("localhost", 6379);
  }

}

FeideOpenIdConnectConfig.java

@Configuration
@EnableOAuth2Client
public class FeideOpenIdConnectConfig {

  @Value("${feide.auth.clientId}")
  private String clientId;

  @Value("${feide.auth.clientSecret}")
  private String clientSecret;

  @Value("${feide.auth.accessTokenUri}")
  private String accessTokenUri;

  @Value("${feide.auth.userAuthorizationUri}")
  private String userAuthorizationUri;

  @Value("${feide.auth.preEstablishedRedirectUri}")
  private String preEstablishedRedirectUri;


  @Bean
  public OAuth2ProtectedResourceDetails feideOpenId() {
    AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
    details.setClientId(clientId);
    details.setClientSecret(clientSecret);
    details.setAccessTokenUri(accessTokenUri);
    details.setUserAuthorizationUri(userAuthorizationUri);
    details.setScope(Arrays.asList("openid", "email", "userid-feide", "profile", "groups"));
    details.setPreEstablishedRedirectUri(preEstablishedRedirectUri);
    details.setUseCurrentUri(false);
    details.setGrantType("authorization_code");
    return details;
  }


  @Bean
  public OAuth2RestTemplate feideOpenIdTemplate(OAuth2ClientContext clientContext) {
    return new OAuth2RestTemplate(feideOpenId(), clientContext);
  }

}

FeideConnectFilter.java

public class FeideConnectFilter extends OAuth2ClientAuthenticationProcessingFilter {

  public FeideConnectFilter(String defaultFilterProcessesUrl) {
    super(defaultFilterProcessesUrl);
  }


  @Override
  public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

    OAuth2AccessToken accessToken;
    try {
      accessToken = restTemplate.getAccessToken();
    } catch (OAuth2Exception e) {
      throw new BadCredentialsException("Could not obtain access token", e);
    }
    try {
      String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
      Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier("https://auth.dataporten.no/openid/jwks"));

      @SuppressWarnings("unchecked")
      Map<String, String> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);

      verifyClaims(authInfo, "https://auth.dataporten.no");

      request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
      request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());

      OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
      return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
    } catch (Exception e) {
      throw new BadCredentialsException("Could not obtain user details from token", e);
    }
  }


  @Override
  protected boolean requiresAuthentication(final HttpServletRequest request, final HttpServletResponse response) {
    if (super.requiresAuthentication(request, response)) {
      return true;
    }

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    // Already authenticated:
    if (authentication != null) {
      return false;
    }
    OAuth2AccessToken accessToken = restTemplate.getAccessToken();
    if (accessToken == null) {
        return true;
    }
    return true;
  }


  @SuppressWarnings("rawtypes")
  protected void verifyClaims(final Map claims, final String issuer) {
    int exp = (Integer) claims.get("exp");
    Date expireDate = new Date(exp * 1000L);
    Date now = new Date();
    if (expireDate.before(now) || !claims.get("iss").equals(issuer) ||
            !claims.get("aud").equals(restTemplate.getResource().getClientId())) {
      throw new RuntimeException("Invalid claims");
    }
  }


  protected RsaVerifier verifier(final String jwkSigningUri) throws Exception {
    CustomUrlJwkProvider provider = new CustomUrlJwkProvider(new URL(jwkSigningUri));
    Jwk jwk = provider.getJwk();
    return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
  }


  protected HttpHeaders getHttpHeaders() {
    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", "Bearer " + restTemplate.getAccessToken());
    return headers;
  }

}

SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Inject
  private OAuth2RestTemplate restTemplate;


  @Bean
  public FeideConnectFilter feideConnectFilter() {
    FeideConnectFilter filter = new FeideConnectFilter("/oauth/login");
    filter.setRestTemplate(restTemplate);
    return filter;
  }


  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .addFilterAfter(new OAuth2ClientContextFilter(), AbstractPreAuthenticatedProcessingFilter.class)
        .addFilterAfter(feideConnectFilter(), OAuth2ClientContextFilter.class)
        .httpBasic()
        .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/oauth2/login"))
        .and()
        .authorizeRequests()
        .anyRequest().authenticated();
  }

}

The filters (in WebInitializer.java) are added in this order:

private void addFilters(final ServletContext container, final WebApplicationContext applicationContext) {
  container.addFilter("springSessionRepositoryFilter", DelegatingFilterProxy.class).addMappingForUrlPatterns(null, false, "/*");
  container.addFilter("springSecurityFilterChain", DelegatingFilterProxy.class).addMappingForUrlPatterns(null, false, "/*");
}

Solution

  • OIDC works with Spring Session if I use Spring Security's own OIDC implementation, which was introduced in version 5. By the way, it was surprisingly easy to implement, almost with no code at all. The Spring Security team has done a great job when they added support for OIDC, and also SAML2.0, in Spring Security. In other words, I have found a solution.