3030import org .springframework .security .oauth2 .client .OAuth2AuthorizedClientService ;
3131import org .springframework .security .oauth2 .client .RefreshTokenOAuth2AuthorizedClientProvider ;
3232import org .springframework .security .oauth2 .client .authentication .OAuth2AuthenticationToken ;
33- import org .springframework .security .oauth2 .core .OAuth2ErrorCodes ;
3433import org .springframework .security .web .util .matcher .AntPathRequestMatcher ;
3534import org .springframework .security .web .util .matcher .OrRequestMatcher ;
3635import org .springframework .security .web .util .matcher .RequestMatcher ;
4241import javax .servlet .ServletException ;
4342import javax .servlet .http .HttpServletRequest ;
4443import javax .servlet .http .HttpServletResponse ;
44+ import javax .servlet .http .HttpSession ;
4545import java .io .IOException ;
4646import java .time .Clock ;
4747import java .time .Duration ;
5050
5151/**
5252 * Ensures that the access token of the user is refreshed when needed.
53- * If the refresh token is invalid (e.g. because the session in the IdP expired), we throw a
54- * {@link ClientAuthorizationRequiredException} exception such that the user is can re-login.
55- *
53+ * If the refresh token is invalid (e.g. because the session in the IdP expired), we first invalidate the session and then throw a
54+ * {@link ClientAuthorizationRequiredException} exception such that the user can re-login.
5655 * This filter only applies to a limited set of requests and not to requests that are proxied to apps.
57- * Otherwise, this filter would be called too much and cause too much overhead.
56+ * Otherwise, this filter would be called too much and cause too much overhead. In addition, this filter should
57+ * only be used on non-ajax requests. This is required for the redirect to the IDP to properly work.
58+ *
59+ * A special case is the /refresh-openid endpoint, which does not throw the exception (i.e. it does not cause a redirect to the IDP)
60+ * but only invalidates the session. This endpoint is frequently called (at least every <40 seconds) when the user is using an app.
61+ * It refreshes the OIDC session as long as possible and when it fails (e.g. because the session was revoked or reached it max life),
62+ * the session is invalidated. The app page detects this and shows a message that the user was logged out.
63+ *
64+ * See #30569, #28976
5865 */
5966public class OpenIdReAuthorizeFilter extends OncePerRequestFilter {
6067
68+ private static final RequestMatcher REFRESH_OPENID_MATCHER = new AntPathRequestMatcher ("/refresh-openid" );
69+
6170 private static final RequestMatcher REQUEST_MATCHER = new OrRequestMatcher (
6271 new AntPathRequestMatcher ("/app/**" ),
6372 new AntPathRequestMatcher ("/app_i/**" ),
6473 new AntPathRequestMatcher ("/" ),
65- new AntPathRequestMatcher ( "/heartbeat" ) );
74+ REFRESH_OPENID_MATCHER );
6675
6776 @ Inject
6877 private OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager ;
@@ -72,8 +81,8 @@ public class OpenIdReAuthorizeFilter extends OncePerRequestFilter {
7281
7382 private final Clock clock = Clock .systemUTC ();
7483
75- // use clock skew of 20 seconds instead of 60 seconds. Otherwise, if the access token is valid for 1 minute, it would get refreshed at each request.
76- private final Duration clockSkew = Duration .ofSeconds (20 );
84+ // use clock skew of 40 seconds instead of 60 seconds. Otherwise, if the access token is valid for 1 minute, it would get refreshed at each request.
85+ private final Duration clockSkew = Duration .ofSeconds (40 );
7786
7887 @ Override
7988 protected void doFilterInternal (@ Nonnull HttpServletRequest request , @ Nonnull HttpServletResponse response , @ Nonnull FilterChain chain ) throws ServletException , IOException {
@@ -83,25 +92,32 @@ protected void doFilterInternal(@Nonnull HttpServletRequest request, @Nonnull Ht
8392 if (auth instanceof OAuth2AuthenticationToken ) {
8493 OAuth2AuthorizedClient authorizedClient = oAuth2AuthorizedClientService .loadAuthorizedClient (REG_ID , auth .getName ());
8594
86- if (accessTokenExpired (authorizedClient )) {
87- OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
88- .withAuthorizedClient (authorizedClient )
89- .principal (auth )
90- .build ();
91-
92- // re-authorize
93- try {
94- oAuth2AuthorizedClientManager .authorize (authorizeRequest );
95- } catch (ClientAuthorizationException ex ) {
96- if (ex .getError ().getErrorCode ().equals (OAuth2ErrorCodes .INVALID_GRANT )) {
97- // if refresh token has expired or is invalid -> re-start authorization process
98- throw new ClientAuthorizationRequiredException (ex .getClientRegistrationId ());
95+ if (authorizedClient == null ) {
96+ invalidateSession (request , response );
97+ return ;
98+ } else {
99+ if (accessTokenExpired (authorizedClient )) {
100+ OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
101+ .withAuthorizedClient (authorizedClient )
102+ .principal (auth )
103+ .build ();
104+
105+ try {
106+ oAuth2AuthorizedClientManager .authorize (authorizeRequest );
107+ logger .info ("Refresh" );
108+ } catch (ClientAuthorizationException ex ) {
109+ invalidateSession (request , response );
110+ return ;
99111 }
100- throw ex ;
101112 }
102113 }
103114 }
104115 }
116+ if (REFRESH_OPENID_MATCHER .matches (request )) {
117+ response .getWriter ().write ("{\" status\" :\" success\" }" );
118+ response .setStatus (200 );
119+ return ;
120+ }
105121 chain .doFilter (request , response );
106122 }
107123
@@ -115,4 +131,17 @@ private boolean accessTokenExpired(OAuth2AuthorizedClient authorizedClient) {
115131 return clock .instant ().isAfter (authorizedClient .getAccessToken ().getExpiresAt ().minus (this .clockSkew ));
116132 }
117133
134+ private void invalidateSession (@ Nonnull HttpServletRequest request , @ Nonnull HttpServletResponse response ) throws IOException {
135+ HttpSession session = request .getSession (false );
136+ if (session != null ) {
137+ session .invalidate ();
138+ }
139+ if (REFRESH_OPENID_MATCHER .matches (request )) {
140+ response .getWriter ().write ("{\" status\" :\" success\" }" );
141+ response .setStatus (200 );
142+ } else {
143+ throw new ClientAuthorizationRequiredException (REG_ID );
144+ }
145+ }
146+
118147}
0 commit comments