From 803556e3e3015f6496bde8366b8ee91dd280e1eb Mon Sep 17 00:00:00 2001 From: Julian Haupt Date: Sat, 9 Nov 2024 00:11:10 +0100 Subject: [PATCH] feat(oauth2): revalidate access tokens (#4603) related to https://github.com/camunda/camunda-bpm-platform/issues/4585 Backported commit f8142ef3e1 from the camunda-bpm-platform repository. Original author: Daniel Kelemen --- ...SpringSecurityOAuth2AutoConfiguration.java | 13 +- .../oauth2/impl/AuthorizeTokenFilter.java | 116 ++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/AuthorizeTokenFilter.java diff --git a/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonSpringSecurityOAuth2AutoConfiguration.java b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonSpringSecurityOAuth2AutoConfiguration.java index 5dacde8d77..4d35bbabbb 100644 --- a/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonSpringSecurityOAuth2AutoConfiguration.java +++ b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonSpringSecurityOAuth2AutoConfiguration.java @@ -23,9 +23,10 @@ import org.operaton.bpm.spring.boot.starter.OperatonBpmAutoConfiguration; import org.operaton.bpm.spring.boot.starter.property.OperatonBpmProperties; import org.operaton.bpm.spring.boot.starter.property.WebappProperty; +import org.operaton.bpm.spring.boot.starter.security.oauth2.impl.AuthorizeTokenFilter; +import org.operaton.bpm.spring.boot.starter.security.oauth2.impl.OAuth2AuthenticationProvider; import org.operaton.bpm.spring.boot.starter.security.oauth2.impl.OAuth2GrantedAuthoritiesMapper; import org.operaton.bpm.spring.boot.starter.security.oauth2.impl.OAuth2IdentityProviderPlugin; -import org.operaton.bpm.spring.boot.starter.security.oauth2.impl.OAuth2AuthenticationProvider; import org.operaton.bpm.webapp.impl.security.auth.ContainerBasedAuthenticationFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +45,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.web.SecurityFilterChain; import java.util.Map; @@ -96,20 +99,26 @@ protected GrantedAuthoritiesMapper grantedAuthoritiesMapper() { } @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http, OAuth2AuthorizedClientManager clientManager) + throws Exception { logger.info("Enabling Operaton Spring Security oauth2 integration"); + var validateTokenFilter = new AuthorizeTokenFilter(clientManager); + + // @formatter:off http.authorizeHttpRequests(c -> c .requestMatchers(webappPath + "/app/**").authenticated() .requestMatchers(webappPath + "/api/**").authenticated() .anyRequest().permitAll() ) + .addFilterAfter(validateTokenFilter, OAuth2AuthorizationRequestRedirectFilter.class) .anonymous(AbstractHttpConfigurer::disable) .oauth2Login(Customizer.withDefaults()) .oidcLogout(Customizer.withDefaults()) .oauth2Client(Customizer.withDefaults()) .cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable); + // @formatter:on return http.build(); } diff --git a/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/AuthorizeTokenFilter.java b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/AuthorizeTokenFilter.java new file mode 100644 index 0000000000..1f697dbe40 --- /dev/null +++ b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/AuthorizeTokenFilter.java @@ -0,0 +1,116 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.operaton.bpm.spring.boot.starter.security.oauth2.impl; + +import jakarta.annotation.Nonnull; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.operaton.bpm.engine.impl.util.ClockUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Date; + +/** + * Authorize or re-authorize (if required) oauth2 client using {@link OAuth2AuthorizedClientManager}. + * + *

+ * References: + *

+ */ +public class AuthorizeTokenFilter extends OncePerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(AuthorizeTokenFilter.class); + private final OAuth2AuthorizedClientManager clientManager; + + public AuthorizeTokenFilter(OAuth2AuthorizedClientManager clientManager) { + this.clientManager = clientManager; + } + + @Override + protected void doFilterInternal(@Nonnull HttpServletRequest request, + @Nonnull HttpServletResponse response, + @Nonnull FilterChain filterChain) throws ServletException, IOException { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof OAuth2AuthenticationToken) { + var token = (OAuth2AuthenticationToken) authentication; + authorizeToken(token, request, response); + } + filterChain.doFilter(request, response); + } + + protected boolean hasTokenExpired(OAuth2Token token) { + return token.getExpiresAt() == null || ClockUtil.now().after(Date.from(token.getExpiresAt())); + } + + protected void clearContext(HttpServletRequest request) { + SecurityContextHolder.clearContext(); + try { + request.getSession().invalidate(); + } catch (Exception ignored) { + } + } + + protected void authorizeToken(OAuth2AuthenticationToken token, + HttpServletRequest request, + HttpServletResponse response) { + // @formatter:off + var authRequest = OAuth2AuthorizeRequest + .withClientRegistrationId(token.getAuthorizedClientRegistrationId()) + .principal(token) + .attributes(attrs -> { + attrs.put(HttpServletRequest.class.getName(), request); + attrs.put(HttpServletResponse.class.getName(), response); + }).build(); + // @formatter:on + + try { + var res = clientManager.authorize(authRequest); + if (res == null || hasTokenExpired(res.getAccessToken())) { + logger.warn("Authorize failed: could not re-authorize expired access token"); + clearContext(request); + } else { + logger.debug("Authorize successful, access token expiry: {}", res.getAccessToken().getExpiresAt()); + } + } catch (OAuth2AuthorizationException e) { + logger.warn("Authorize failed: {}", e.getMessage()); + clearContext(request); + } + } +}