Skip to content

Commit

Permalink
feat(oauth2): revalidate access tokens (#4603)
Browse files Browse the repository at this point in the history
related to camunda/camunda-bpm-platform#4585

Backported commit f8142ef3e1 from the camunda-bpm-platform repository.
Original author: Daniel Kelemen <[email protected]>
  • Loading branch information
hauptmedia committed Nov 8, 2024
1 parent 2179228 commit c21499d
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
* <ul>
* <li>If the access token is valid, then does nothing.
* <li>If the access token is expired, then refreshes it.
* <li>If authorize failed, then clears the {@link org.springframework.security.core.context.SecurityContext} and {@link jakarta.servlet.http.HttpSession}.
* </ul>
* <p>
* References:
* <ul>
* <li> {@link OAuth2AuthorizedClientManager#authorize(OAuth2AuthorizeRequest)}
* <li> {@link org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider#authorize(OAuth2AuthorizationContext)}
* <li> {@link org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider#authorize(OAuth2AuthorizationContext)}
* <li> {@link org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider#authorize(OAuth2AuthorizationContext)}
* </ul>
*/
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);
}
}
}

0 comments on commit c21499d

Please sign in to comment.