rdmueller
rdmueller

Reputation: 11012

Spring Security with oidc: refresh the tokens

Spring Boot 2 with Spring Security 5 can be configured to use an openID connect ID provider for authentication. I managed to setup up my project just by configuring Spring Security - that works fine with all kinds of perfectly preconfigured security mechanisms like mitigation of session fixation.

But it seems that Spring Security does not refresh the tokens (which are stored in the session) by itself when they are expired.

Is there a setting for that or do I have to care for the refresh myself?

Update: Spring Boot 2.1 has been released, so it is time to revisit this problem. I still have no clue if the accessToken can now be automatically refreshed or if I have to write code for doing so...

Upvotes: 16

Views: 14345

Answers (4)

Stalowy
Stalowy

Reputation: 41

In case someone get there looking for the answer for newer version of Spring. Currently we have Spring 6. I am using oauth2Login() feature to log in with the use of socials like github, facebook. I wanted to tie user session to access token expireAt property. If expired, should refresh token and try to extend the session before force user to log in once again. Non Reactive solution (Without WebClient and OAuth2AuthorizedClientExchangeFilterFunction) within Spring doesn't provide auto mechanism in oauth2Login() feature. But I configured a bean for OAuth2AuthorizedClientManager which can be used with oauth2Client() feature with Spring reactive. However it's probably a cool way to maually refresh the access token and clear manually an authentication context in case of exception.

    @Bean
        public OAuth2AuthorizedClientManager authorizedClientManager() {
            OAuth2AuthorizedClientProvider authorizedClientProvider =
                    OAuth2AuthorizedClientProviderBuilder.builder()
                            .authorizationCode()
                            .refreshToken()
                            .build();
    
            DefaultOAuth2AuthorizedClientManager authorizedClientManager =
                    new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository(), authorizedClientRepository());
            authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
            return authorizedClientManager;
        }


and Filter


    public class ExpiredTokenFilter extends OncePerRequestFilter {
    
        @Resource
        private OAuth2AuthorizedClientManager authorizedClientManager;
    
        @Resource
        private OAuth2AuthorizedClientRepository authorizedClientRepository;
    
        @Override
        protected void doFilterInternal(
                @Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response,
                @Nonnull FilterChain filterChain)
                throws ServletException, IOException {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (Objects.nonNull(authentication) && authentication.getPrincipal() instanceof OidcUser) {
                validateToken(authentication, request, response);
            }
    
            filterChain.doFilter(request, response);
        }
    
        private void validateToken(Authentication auth, HttpServletRequest request, HttpServletResponse response) {
            OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
                    .withClientRegistrationId(MY_PROVIDER)
                    .principal(auth)
                    .attributes(attrs -> {
                        attrs.put(HttpServletRequest.class.getName(), request);
                        attrs.put(HttpServletResponse.class.getName(), response);
                    })
                    .build();
            try {
                authorizedClientManager.authorize(authorizeRequest);
            } catch (ClientAuthorizationException e) {               
authorizedClientRepository.removeAuthorizedClient(MY_PROVIDER, auth, request, response);
SecurityContextHolder.getContext().setAuthentication(null);
            }
        }
    }

Remember to register Filter in SecurityFilterChain

http.addFilterBefore(expiredTokenFilter, AbstractPreAuthenticatedProcessingFilter.class));

Upvotes: 3

aberhallo
aberhallo

Reputation: 74

According to https://github.com/spring-projects/spring-security/issues/6742 it seems that the token is intentionally not refreshed:

An ID Token typically comes with an expiration date. The RP MAY rely on it to expire the RP session.

Spring does not. There are two enhancements mentioned at the end which should solve some of the refresh issues - both are still open.

As a workaround, I implemented a GenericFilterBean which checks the token and clears the authentication in the current security context. Thus a new token is needed.

@Configuration
public class RefreshTokenFilterConfig {

    @Bean
    GenericFilterBean refreshTokenFilter(OAuth2AuthorizedClientService clientService) {
        return new GenericFilterBean() {
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if (authentication != null && authentication instanceof OAuth2AuthenticationToken) {
                    OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
                    OAuth2AuthorizedClient client =
                            clientService.loadAuthorizedClient(
                                    token.getAuthorizedClientRegistrationId(),
                                    token.getName());
                    OAuth2AccessToken accessToken = client.getAccessToken();
                    if (accessToken.getExpiresAt().isBefore(Instant.now())) {
                        SecurityContextHolder.getContext().setAuthentication(null);
                    }
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }
        };
    }
}

Additionally I had to add the filter to the security config:

@Bean
public WebSecurityConfigurerAdapter webSecurityConfigurer(GenericFilterBean refreshTokenFilter) {
    return new WebSecurityConfigurerAdapter() {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                   .addFilterBefore(refreshTokenFilter,  AnonymousAuthenticationFilter.class)

Implemented with spring-boot-starter-parent and dependencies in version 2.2.7.RELEASE:

  • spring-boot-starter-web
  • spring-boot-starter-security
  • spring-boot-starter-oauth2-client

I appreciate opinions about this workaround since I'm still not sure if such an overhead is really needed in Spring Boot.

Upvotes: 5

Darren Forsythe
Darren Forsythe

Reputation: 11411

According to the documentation,

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#webclient

When using a WebClient configured correctly, as given in the documentation it will automatically be refreshed.

Spring Security will automatically refresh expired tokens (if a refresh token is present)

This is also supported by the features matrix that refresh tokens are supported.

https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Features-Matrix

There was an older blog on Spring Security 5 that gives you access to beans that you could do this manually,

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

OAuth2AuthenticationToken oauthToken =
    (OAuth2AuthenticationToken) authentication;

There will be an OAuth2AuthorizedClientService automatically configured as a bean in the Spring application context, so you’ll only need to inject it into wherever you’ll use it.

OAuth2AuthorizedClient client =
    clientService.loadAuthorizedClient(
            oauthToken.getAuthorizedClientRegistrationId(),
            oauthToken.getName());

String refreshToken = client.getRefreshToken();

And, failing to find it right now, but I assume as part of the OAuth2AuthorizedClientExchangeFilterFunction has the calls to do a refresh.

Upvotes: 10

rdmueller
rdmueller

Reputation: 11012

even a bounty of 100 rep points did not yield an answer. So I guess there is currently no mechanism implemented to automatically refresh the access token with Spring Security.

A valid alternative seems to use the spring boot keycloak adapter which is capable of refreshing the token.

Upvotes: 0

Related Questions