Hello , I have setup a camunda spring boot project (7.18.0-alpha1) that uses keycloak (using keycloak sso). What I am trying to figure out is if there is a way to have the endpoint engine-rest secured and at the same time be able to make deployments using camunda modeler desktop application.
From what I have gathered I could use basic auth for paths that match engine-rest for example but that would be too broad.
Is there anything that could be done here ?
application.yaml
keycloak:
url.auth: ${KEYCLOAK_URL}
url.token: ${KEYCLOAK_URL}
url.plugin: ${KEYCLOAK_URL}
client.id: ${KEYCLOAK_CLIENT_ID}
client.secret: ${KEYCLOAK_CLIENT_SECRET}
spring.security:
oauth2:
client:
registration:
keycloak:
provider: keycloak
client-id: ${KEYCLOAK_CLIENT_ID}
client-secret: ${KEYCLOAK_CLIENT_SECRET}
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
scope: openid, profile, email
provider:
keycloak:
issuer-uri: ${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}
authorization-uri: ${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth
user-info-uri: ${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/userinfo
token-uri: ${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token
jwk-set-uri: ${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs
user-name-attribute: preferred_username
plugin.identity.keycloak:
keycloakIssuerUrl: ${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}
keycloakAdminUrl: ${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}
clientId: ${KEYCLOAK_CLIENT_ID}
clientSecret: ${KEYCLOAK_CLIENT_SECRET}
useUsernameAsCamundaUserId: true
useGroupPathAsCamundaGroupId: true
administratorGroupName: ${KEYCLOAK_CAMUNDA_ADMIN_GROUP}
administratorUserId: ${KEYCLOAK_CAMUNDA_ADMIN_USER}
disableSSLCertificateValidation: true
Security config:
/**
* Camunda Web application SSO configuration for usage with
* KeycloakIdentityProviderPlugin.
*/
@EnableWebSecurity
@Configuration
@Profile("keycloak")
public class WebAppSecurityConfig {
@Inject
private KeycloakLogoutHandler keycloakLogoutHandler;
@Bean
@Profile("keycloak")
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.ignoringAntMatchers("/api/**", "/engine-rest/**")) // Disable CSRF if necessary
.authorizeHttpRequests(auth -> auth
.antMatchers("/assets/**", "/app/**", "/api/**", "/lib/**").authenticated() // Secures Camunda
.anyRequest().permitAll() // Allows all other requests
)
.oauth2Login(withDefaults()) // Enables OAuth2 login
.oauth2Client(withDefaults())
.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/app/**/logout"))
.logoutSuccessHandler(keycloakLogoutHandler));
return http.build();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
@Profile("keycloak")
public FilterRegistrationBean containerBasedAuthenticationFilter() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new ContainerBasedAuthenticationFilter());
filterRegistration.setInitParameters(Collections.singletonMap("authentication-provider",
"com.example.demo.camunda.configuration.KeycloakAuthenticationProvider"));
filterRegistration.setOrder(201); // ensure the filter is registered after the Spring Security Filter Chain
filterRegistration.addUrlPatterns("/app/*");
return filterRegistration;
}
// The ForwardedHeaderFilter is needed to correctly assemble the redirect URL
// for OAuth2 login.
// Without the filter, Spring might generate an HTTP URL even though the
// container route is accessed via HTTPS.
@Bean
@Profile("keycloak")
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
FilterRegistrationBean<ForwardedHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new ForwardedHeaderFilter());
filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filterRegistrationBean;
}
@Bean
@Profile("keycloak")
@Order(0)
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}
Identity Provider:
@Component
@ConfigurationProperties(prefix = "plugin.identity.keycloak")
@Profile("keycloak")
public class KeycloakIdentityProvider extends KeycloakIdentityProviderPlugin {
}
Authentication Provider:
/**
* OAuth2 Authentication Provider for usage with Keycloak and
* KeycloakIdentityProviderPlugin.
*/
@Profile("keycloak")
public class KeycloakAuthenticationProvider extends ContainerBasedAuthenticationProvider {
@Override
public AuthenticationResult extractAuthenticatedUser(HttpServletRequest request, ProcessEngine engine) {
// Extract user-name-attribute of the OAuth2 token
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof OAuth2AuthenticationToken)
|| !(authentication.getPrincipal() instanceof OidcUser)) {
return AuthenticationResult.unsuccessful();
}
String userId = ((OidcUser) authentication.getPrincipal()).getName();
if (!StringUtils.hasLength(userId)) {
return AuthenticationResult.unsuccessful();
}
// Authentication successful
AuthenticationResult authenticationResult = new AuthenticationResult(userId, true);
authenticationResult.setGroups(getUserGroups(userId, engine));
return authenticationResult;
}
private List<String> getUserGroups(String userId, ProcessEngine engine) {
List<String> groupIds = new ArrayList<>();
// query groups using KeycloakIdentityProvider plugin
engine.getIdentityService().createGroupQuery().groupMember(userId).list()
.forEach(g -> groupIds.add(g.getId()));
return groupIds;
}
}