Skip to content

Commit

Permalink
feat(run): add spring security oauth2 integration (#4481)
Browse files Browse the repository at this point in the history
related to camunda/camunda-bpm-platform#4452

Backported commit b53ad55475 from the camunda-bpm-platform repository.
Original author: Daniel Kelemen <[email protected]>"
  • Loading branch information
hauptmedia authored and javahippie committed Nov 8, 2024
1 parent d7b6604 commit 6107308
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 113 deletions.
2 changes: 1 addition & 1 deletion distro/run/assembly/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<groupId>org.operaton.bpm.run</groupId>
<artifactId>operaton-bpm-run-modules-oauth2</artifactId>
<version>${project.version}</version>
<type>jar</type>
<type>pom</type>
<exclusions>
<!-- we don't need any transitive dependencies -->
<exclusion>
Expand Down
2 changes: 1 addition & 1 deletion distro/run/modules/oauth2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<artifactId>operaton-bpm-run-modules-oauth2</artifactId>
<name>Operaton Platform - Run - Module Spring Security</name>
<packaging>jar</packaging>
<packaging>pom</packaging>

<properties>
<!-- generate a bom of compile time dependencies for the license book.
Expand Down

This file was deleted.

18 changes: 12 additions & 6 deletions spring-boot-starter/starter-security/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>operaton-bpm-spring-boot-starter-webapp</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>operaton-bpm-spring-boot-starter-rest</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>

<!-- spring security dependencies -->
<dependency>
Expand All @@ -31,12 +43,6 @@
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>operaton-bpm-spring-boot-starter-rest</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<artifactId>operaton-bpm-spring-boot-starter-test</artifactId>
<groupId>${project.groupId}</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.operaton.bpm.spring.boot.starter.security;
package org.operaton.bpm.spring.boot.starter.security.oauth2;

import org.operaton.bpm.spring.boot.starter.security.oauth2.impl.ClientsNotConfiguredCondition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Conditional(ClientsNotConfiguredCondition.class)
public class OperatonBpmSpringSecurityDisableAutoConfiguration {

private static final Logger logger = LoggerFactory.getLogger(OperatonBpmSpringSecurityDisableAutoConfiguration.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import org.operaton.bpm.engine.rest.security.auth.ProcessEngineAuthenticationFilter;
import org.operaton.bpm.engine.spring.SpringProcessEngineServicesConfiguration;
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.OAuth2AuthenticationProvider;
import org.operaton.bpm.webapp.impl.security.auth.ContainerBasedAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.Ordered;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

import java.util.Map;

@AutoConfigureOrder(OperatonSpringSecurityOAuth2AutoConfiguration.CAMUNDA_OAUTH2_ORDER)
@AutoConfigureAfter({ OperatonBpmAutoConfiguration.class, SpringProcessEngineServicesConfiguration.class })
@ConditionalOnBean(OperatonBpmProperties.class)
@Conditional(ClientsConfiguredCondition.class)
public class OperatonSpringSecurityOAuth2AutoConfiguration {

private static final Logger logger = LoggerFactory.getLogger(OperatonSpringSecurityOAuth2AutoConfiguration.class);
public static final int CAMUNDA_OAUTH2_ORDER = Ordered.HIGHEST_PRECEDENCE + 100;
private final String webappPath;

public OperatonSpringSecurityOAuth2AutoConfiguration(OperatonBpmProperties properties) {
WebappProperty webapp = properties.getWebapp();
this.webappPath = webapp.getApplicationPath();
}

@Bean
public FilterRegistrationBean<?> webappAuthenticationFilter() {
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<>();
filterRegistration.setName("Container Based Authentication Filter");
filterRegistration.setFilter(new ContainerBasedAuthenticationFilter());
filterRegistration.setInitParameters(Map.of(
ProcessEngineAuthenticationFilter.AUTHENTICATION_PROVIDER_PARAM, OAuth2AuthenticationProvider.class.getName()));
// make sure the filter is registered after the Spring Security Filter Chain
filterRegistration.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER + 1);
filterRegistration.addUrlPatterns(webappPath + "/app/*", webappPath + "/api/*");
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
return filterRegistration;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
logger.info("Enabling Operaton Spring Security oauth2 integration");

http.authorizeHttpRequests(c -> c
.requestMatchers(webappPath + "/app/**").authenticated()
.requestMatchers(webappPath + "/api/**").authenticated()
.anyRequest().permitAll()
)
.oauth2Login(Customizer.withDefaults())
.oidcLogout(Customizer.withDefaults())
.oauth2Client(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable);

return http.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
* Condition that matches if no {@code spring.security.oauth2.client.registration} properties are defined
* by inverting the outcome of {@link ClientsConfiguredCondition}.
*/
public class ClientsNotConfiguredCondition extends ClientsConfiguredCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
var matchOutcome = super.getMatchOutcome(context, metadata);
return new ConditionOutcome(!matchOutcome.isMatch(), matchOutcome.getConditionMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.servlet.http.HttpServletRequest;
import org.operaton.bpm.engine.ProcessEngine;
import org.operaton.bpm.engine.rest.security.auth.AuthenticationResult;
import org.operaton.bpm.engine.rest.security.auth.impl.ContainerBasedAuthenticationProvider;
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.authentication.OAuth2AuthenticationToken;

public class OAuth2AuthenticationProvider extends ContainerBasedAuthenticationProvider {

private static final Logger logger = LoggerFactory.getLogger(OAuth2AuthenticationProvider.class);

@Override
public AuthenticationResult extractAuthenticatedUser(HttpServletRequest request, ProcessEngine engine) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null) {
logger.debug("Authentication is null");
return AuthenticationResult.unsuccessful();
}

if (!(authentication instanceof OAuth2AuthenticationToken)) {
logger.debug("Authentication is not OAuth2, it is {}", authentication.getClass());
return AuthenticationResult.unsuccessful();
}
var oauth2 = (OAuth2AuthenticationToken) authentication;
String operatonUserId = oauth2.getName();
if (operatonUserId == null || operatonUserId.isEmpty()) {
logger.debug("UserId is empty");
return AuthenticationResult.unsuccessful();
}

logger.debug("Authenticated user '{}'", operatonUserId);
return AuthenticationResult.successful(operatonUserId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.operaton.bpm.spring.boot.starter.security.oauth2.OperatonBpmSpringSecurityDisableAutoConfiguration
org.operaton.bpm.spring.boot.starter.security.oauth2.OperatonSpringSecurityOAuth2AutoConfiguration
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@

import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

@SpringBootConfiguration
@EnableAutoConfiguration
@SpringBootApplication
public class SampleApplication {

public static void main(String... args) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,14 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SampleApplication.class, webEnvironment = RANDOM_PORT)
@SpringBootTest(classes = SampleApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OperatonBpmSampleApplicationTest {

private String baseUrl;
Expand All @@ -52,11 +47,16 @@ public void postConstruct() {
baseUrl = "http://localhost:" + port;
}

@Test
public void webappApiIsAvailableAndAuthorized() {
ResponseEntity<String> entity = testRestTemplate.getForEntity(baseUrl + "/operaton/api/engine/engine/default/user", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}

@Test
public void restApiIsAvailable() {
ResponseEntity<String> entity = testRestTemplate.getForEntity(baseUrl + "/engine-rest/engine/", String.class);
// default Spring Security filter chain config redirects to /login
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND);
assertThat(entity.getHeaders()).contains(entry(HttpHeaders.LOCATION, List.of(baseUrl + "/login")));
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("[{\"name\":\"default\"}]");
}
}
Loading

0 comments on commit 6107308

Please sign in to comment.