Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(run): add oauth2 identity provider #4570

Merged
merged 3 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,27 @@
import org.camunda.bpm.spring.boot.starter.CamundaBpmAutoConfiguration;
import org.camunda.bpm.spring.boot.starter.property.CamundaBpmProperties;
import org.camunda.bpm.spring.boot.starter.property.WebappProperty;
import org.camunda.bpm.spring.boot.starter.security.oauth2.impl.OAuth2GrantedAuthoritiesMapper;
import org.camunda.bpm.spring.boot.starter.security.oauth2.impl.OAuth2IdentityProviderPlugin;
import org.camunda.bpm.spring.boot.starter.security.oauth2.impl.OAuth2AuthenticationProvider;
import org.camunda.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.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
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.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.web.SecurityFilterChain;

import java.util.Map;
Expand All @@ -47,13 +52,17 @@
@AutoConfigureAfter({ CamundaBpmAutoConfiguration.class, SpringProcessEngineServicesConfiguration.class })
@ConditionalOnBean(CamundaBpmProperties.class)
@Conditional(ClientsConfiguredCondition.class)
@EnableConfigurationProperties(OAuth2Properties.class)
public class CamundaSpringSecurityOAuth2AutoConfiguration {

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

public CamundaSpringSecurityOAuth2AutoConfiguration(CamundaBpmProperties properties) {
public CamundaSpringSecurityOAuth2AutoConfiguration(CamundaBpmProperties properties,
OAuth2Properties oAuth2Properties) {
this.oAuth2Properties = oAuth2Properties;
WebappProperty webapp = properties.getWebapp();
this.webappPath = webapp.getApplicationPath();
}
Expand All @@ -71,19 +80,35 @@ public FilterRegistrationBean<?> webappAuthenticationFilter() {
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
return filterRegistration;
}


@Bean
@ConditionalOnProperty(name = "identity-provider.enabled", prefix = OAuth2Properties.PREFIX)
tasso94 marked this conversation as resolved.
Show resolved Hide resolved
public OAuth2IdentityProviderPlugin identityProviderPlugin() {
logger.debug("Registering OAuth2IdentityProviderPlugin");
return new OAuth2IdentityProviderPlugin();
}

@Bean
@ConditionalOnProperty(name = "identity-provider.group-name-attribute", prefix = OAuth2Properties.PREFIX)
protected GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
logger.debug("Registering OAuth2GrantedAuthoritiesMapper");
return new OAuth2GrantedAuthoritiesMapper(oAuth2Properties);
}

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Please note that in the previous PR #cors() was removed but with the current changes and without it Camunda Run cannot be started when oauth2 flag is enabled. => cors config is required.

Current error that I observe when cors line is removed:

Details
2024-09-04T15:04:30.282+02:00 ERROR 18404 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'filterChain' defined in class path resource [org/camunda/bpm/spring/boot/starter/security/oauth2/CamundaSpringSecurityOAuth2AutoConfiguration.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'filterChain' threw exception with message: Bean named 'corsFilter' is expected to be of type 'org.springframework.web.filter.CorsFilter' but was actually of type 'org.springframework.boot.web.servlet.FilterRegistrationBean'
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:648) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:636) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1337) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1167) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:962) ~[spring-context-6.1.10.jar!/:6.1.10]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.10.jar!/:6.1.10]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.3.1.jar!/:3.3.1]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.3.1.jar!/:3.3.1]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.3.1.jar!/:3.3.1]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.3.1.jar!/:3.3.1]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.3.1.jar!/:3.3.1]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) ~[spring-boot-3.3.1.jar!/:3.3.1]
        at org.camunda.bpm.run.CamundaBpmRun.main(CamundaBpmRun.java:25) ~[!/:7.22.0-SNAPSHOT]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:91) ~[camunda-bpm-run-core.jar:7.22.0-SNAPSHOT]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:53) ~[camunda-bpm-run-core.jar:7.22.0-SNAPSHOT]
        at org.springframework.boot.loader.launch.PropertiesLauncher.main(PropertiesLauncher.java:574) ~[camunda-bpm-run-core.jar:7.22.0-SNAPSHOT]
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'filterChain' threw exception with message: Bean named 'corsFilter' is expected to be of type 'org.springframework.web.filter.CorsFilter' but was actually of type 'org.springframework.boot.web.servlet.FilterRegistrationBean'
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:177) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:644) ~[spring-beans-6.1.10.jar!/:6.1.10]
        ... 26 common frames omitted
Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'corsFilter' is expected to be of type 'org.springframework.web.filter.CorsFilter' but was actually of type 'org.springframework.boot.web.servlet.FilterRegistrationBean'
        at org.springframework.beans.factory.support.AbstractBeanFactory.adaptBeanInstance(AbstractBeanFactory.java:422) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:403) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205) ~[spring-beans-6.1.10.jar!/:6.1.10]
        at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1240) ~[spring-context-6.1.10.jar!/:6.1.10]
        at org.springframework.security.config.annotation.web.configurers.CorsConfigurer.getCorsFilter(CorsConfigurer.java:85) ~[spring-security-config-6.3.1.jar:6.3.1]
        at org.springframework.security.config.annotation.web.configurers.CorsConfigurer.configure(CorsConfigurer.java:73) ~[spring-security-config-6.3.1.jar:6.3.1]
        at org.springframework.security.config.annotation.web.configurers.CorsConfigurer.configure(CorsConfigurer.java:41) ~[spring-security-config-6.3.1.jar:6.3.1]
        at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.configure(AbstractConfiguredSecurityBuilder.java:376) ~[spring-security-config-6.3.1.jar:6.3.1]
        at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:330) ~[spring-security-config-6.3.1.jar:6.3.1]
        at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38) ~[spring-security-config-6.3.1.jar:6.3.1]
        at org.camunda.bpm.spring.boot.starter.security.oauth2.CamundaSpringSecurityOAuth2AutoConfiguration.filterChain(CamundaSpringSecurityOAuth2AutoConfiguration.java:113) ~[camunda-bpm-spring-boot-starter-security-7.22.0-SNAPSHOT.jar:7.22.0-SNAPSHOT]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:140) ~[spring-beans-6.1.10.jar!/:6.1.10]
        ... 27 common frames omitted

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add a config class to the oauth2 module that disables cors since otherwise it clashes with runs CorsFilter:

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    http.cors(AbstractHttpConfigurer::disable);

    return http.build();
  }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we should add this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed! 👍

tasso94 marked this conversation as resolved.
Show resolved Hide resolved
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable);

return http.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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.camunda.bpm.spring.boot.starter.security.oauth2;

import org.camunda.bpm.spring.boot.starter.security.oauth2.impl.OAuth2IdentityProvider;
import org.camunda.bpm.spring.boot.starter.property.CamundaBpmProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(OAuth2Properties.PREFIX)
public class OAuth2Properties {

public static final String PREFIX = CamundaBpmProperties.PREFIX + ".oauth2";

/**
* OAuth2 identity provider properties.
*/
private OAuth2IdentityProviderProperties identityProvider;

public static class OAuth2IdentityProviderProperties {
/**
* Enable {@link OAuth2IdentityProvider}.
*/
private boolean enabled = false;

/**
* Name of the attribute (claim) that holds the groups.
*/
private String groupNameAttribute;

/**
* Group name attribute delimiter. Only used if the {@link #groupNameAttribute} is a {@link String}.
*/
private String groupNameDelimiter = ",";

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public String getGroupNameAttribute() {
return groupNameAttribute;
}

public void setGroupNameAttribute(String groupNameAttribute) {
this.groupNameAttribute = groupNameAttribute;
}

public String getGroupNameDelimiter() {
return groupNameDelimiter;
}

public void setGroupNameDelimiter(String groupNameDelimiter) {
this.groupNameDelimiter = groupNameDelimiter;
}
}

public OAuth2IdentityProviderProperties getIdentityProvider() {
return identityProvider;
}

public void setIdentityProvider(OAuth2IdentityProviderProperties identityProvider) {
this.identityProvider = identityProvider;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.camunda.bpm.spring.boot.starter.security.oauth2.impl;

import org.camunda.bpm.spring.boot.starter.security.oauth2.OAuth2Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

public class OAuth2GrantedAuthoritiesMapper implements GrantedAuthoritiesMapper {

private static final Logger logger = LoggerFactory.getLogger(OAuth2GrantedAuthoritiesMapper.class);
private final OAuth2Properties oAuth2Properties;

public OAuth2GrantedAuthoritiesMapper(OAuth2Properties oAuth2Properties) {
this.oAuth2Properties = oAuth2Properties;
}

@Override
public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
var identityProviderProperties = oAuth2Properties.getIdentityProvider();
var groupNameAttribute = identityProviderProperties.getGroupNameAttribute();
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

authorities.forEach(authority -> {
if (authority instanceof OAuth2UserAuthority) {
var oauth2UserAuthority = (OAuth2UserAuthority) authority;
Object groupAttribute = oauth2UserAuthority.getAttributes().get(groupNameAttribute);

if (groupAttribute == null) {
logger.debug("Attribute {} is not available", groupNameAttribute);
return;
}

if (groupAttribute instanceof Collection) {
//noinspection unchecked
Collection<String> groupsAttribute = (Collection<String>) groupAttribute;
var grantedAuthorities = groupsAttribute.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
mappedAuthorities.addAll(grantedAuthorities);
} else if (groupAttribute instanceof String) {
String groupNameDelimiter = identityProviderProperties.getGroupNameDelimiter();
String groupsAttribute = (String) groupAttribute;

var grantedAuthorities = Arrays.stream(groupsAttribute.split(groupNameDelimiter))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
mappedAuthorities.addAll(grantedAuthorities);
} else {
logger.error("Could not map granted authorities, unsupported group attribute type: {}", groupAttribute.getClass());
}
}
});

logger.debug("Authorities mapped from {} to {}", authorities, mappedAuthorities);
return mappedAuthorities;
}

}
Loading