diff --git a/pom.xml b/pom.xml
index 380e9bc..879358e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
com.inteligr8.activiti
keycloak-activiti-app-ext
- 1.0.1
+ 1.1.0
Keycloak Authentication & Authorization for APS
diff --git a/src/main/java/com/activiti/conf/ActivitiOotbSecurityConfigurationAdapter.java b/src/main/java/com/activiti/conf/ActivitiOotbSecurityConfigurationAdapter.java
new file mode 100644
index 0000000..d90e7c8
--- /dev/null
+++ b/src/main/java/com/activiti/conf/ActivitiOotbSecurityConfigurationAdapter.java
@@ -0,0 +1,57 @@
+package com.activiti.conf;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Component;
+
+import com.inteligr8.activiti.ActivitiSecurityConfigAdapter;
+
+/**
+ * This class/bean executes the OOTB security configuration without the
+ * override, so you can still use its OOTB features. This will allow you to
+ * enable/disable features, chain them, and uset he OOTB features as a
+ * fallback or failsafe.
+ *
+ * This class must be in the com.activiti.conf package so it can use protected
+ * fields and methods of the OOTB class instance.
+ *
+ * @author brian@inteligr8.com
+ * @see com.activiti.conf.SecurityConfiguration
+ */
+@Component
+public class ActivitiOotbSecurityConfigurationAdapter implements ActivitiSecurityConfigAdapter {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Value("${keycloak-ext.ootbSecurityConfig.enabled:true}")
+ private boolean enabled;
+
+ @Autowired
+ private SecurityConfiguration ootbSecurityConfig;
+
+ @Override
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public int getPriority() {
+ return 0;
+ }
+
+ @Override
+ public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
+ this.logger.trace("configureGlobal()");
+
+ this.logger.info("Using OOTB authentication");
+
+ // unset override (which has already been called in order to get here)
+ this.ootbSecurityConfig.securityConfigOverride = null;
+
+ this.ootbSecurityConfig.configureGlobal(authmanBuilder);
+ }
+
+}
diff --git a/src/main/java/com/activiti/extension/conf/KeycloakExtSpringComponentScanner.java b/src/main/java/com/activiti/extension/conf/KeycloakExtSpringComponentScanner.java
index 2ee46b1..62dceef 100644
--- a/src/main/java/com/activiti/extension/conf/KeycloakExtSpringComponentScanner.java
+++ b/src/main/java/com/activiti/extension/conf/KeycloakExtSpringComponentScanner.java
@@ -4,7 +4,7 @@ import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
-@ComponentScan(basePackages = {"com.inteligr8.activiti.ais"})
+@ComponentScan(basePackages = {"com.inteligr8.activiti"})
public class KeycloakExtSpringComponentScanner {
}
diff --git a/src/main/java/com/inteligr8/activiti/ActivitiSecurityConfigAdapter.java b/src/main/java/com/inteligr8/activiti/ActivitiSecurityConfigAdapter.java
new file mode 100644
index 0000000..87122da
--- /dev/null
+++ b/src/main/java/com/inteligr8/activiti/ActivitiSecurityConfigAdapter.java
@@ -0,0 +1,38 @@
+package com.inteligr8.activiti;
+
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+/**
+ * @author brian@inteligr8.com
+ */
+public interface ActivitiSecurityConfigAdapter extends Comparable {
+
+ /**
+ * Is the adapter enabled? This allows for configurable enablement.
+ *
+ * @return true if enabled; false otherwise
+ */
+ boolean isEnabled();
+
+ /**
+ * The lower the value, the higher the priority. The OOTB security
+ * configuration uses priority 0. Use negative values to supersede it.
+ * Anything with equal priorities should be considered unordered and may
+ * execute in a random order.
+ *
+ * @return A priority; may be negative or positive
+ */
+ int getPriority();
+
+ /**
+ * @see com.activiti.api.security.AlfrescoSecurityConfigOverride
+ */
+ void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService);
+
+ @Override
+ default int compareTo(ActivitiSecurityConfigAdapter adapter) {
+ return Integer.compare(this.getPriority(), adapter.getPriority());
+ }
+
+}
diff --git a/src/main/java/com/inteligr8/activiti/Inteligr8SecurityConfigurationRegistry.java b/src/main/java/com/inteligr8/activiti/Inteligr8SecurityConfigurationRegistry.java
new file mode 100644
index 0000000..020b442
--- /dev/null
+++ b/src/main/java/com/inteligr8/activiti/Inteligr8SecurityConfigurationRegistry.java
@@ -0,0 +1,100 @@
+package com.inteligr8.activiti;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Component;
+
+import com.activiti.api.security.AlfrescoSecurityConfigOverride;
+import com.activiti.domain.idm.Group;
+import com.activiti.domain.idm.Tenant;
+import com.activiti.service.api.GroupService;
+import com.activiti.service.idm.TenantService;
+import com.activiti.service.license.LicenseService;
+
+/**
+ * This class/bean overrides the APS security configuration with a collection
+ * of implementations. The OOTB extension only provides one override. This
+ * uses that extension point, but delegates it out to multiple possible
+ * implementations.
+ *
+ * Order cannot be controlled, so it should not be assumed in any adapter
+ * implementation.
+ *
+ * @author brian@inteligr8.com
+ */
+@Component
+public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityConfigOverride {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Autowired
+ private List adapters;
+
+ @Autowired(required = false)
+ private LicenseService licenseService;
+
+ @Autowired(required = false)
+ private TenantService tenantService;
+
+ @Autowired(required = false)
+ private GroupService groupService;
+
+ @Override
+ public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
+ this.logger.trace("configureGlobal()");
+
+ Collections.sort(this.adapters);
+
+ if (this.logger.isTraceEnabled())
+ this.logGroups();
+
+ for (ActivitiSecurityConfigAdapter adapter : this.adapters) {
+ if (adapter.isEnabled()) {
+ this.logger.info("Security adapter enabled: {}", adapter.getClass());
+ adapter.configureGlobal(authmanBuilder, userDetailsService);
+ break;
+ } else {
+ this.logger.info("Security adapter disabled: {}", adapter.getClass());
+ }
+ }
+ }
+
+ private void logGroups() {
+ Long tenantId = this.findDefaultTenantId();
+ if (tenantId != null) {
+ // not first boot
+ this.logger.trace("Functional groups: {}", this.toGroupNames(this.groupService.getFunctionalGroups(tenantId)));
+ this.logger.trace("System groups: {}", this.toGroupNames(this.groupService.getSystemGroups(tenantId)));
+ }
+ }
+
+ private Long findDefaultTenantId() {
+ String defaultTenantName = this.licenseService.getDefaultTenantName();
+ this.logger.trace("Default Tenant: {}", defaultTenantName);
+
+ List tenants = this.tenantService.findTenantsByName(defaultTenantName);
+ if (tenants == null || tenants.isEmpty()) {
+ this.logger.warn("Default tenant not found");
+ return null;
+ }
+
+ Tenant tenant = tenants.iterator().next();
+ return tenant.getId();
+ }
+
+ private Collection toGroupNames(Collection groups) {
+ List groupNames = new ArrayList<>(groups.size());
+ for (Group group : groups)
+ groupNames.add(group.getName() + " [" + group.getExternalId() + "]");
+ return groupNames;
+ }
+
+}
diff --git a/src/main/java/com/inteligr8/activiti/ais/AbstractIdentityServiceActivitiAuthenticator.java b/src/main/java/com/inteligr8/activiti/ais/AbstractIdentityServiceActivitiAuthenticator.java
deleted file mode 100644
index ccb3d0f..0000000
--- a/src/main/java/com/inteligr8/activiti/ais/AbstractIdentityServiceActivitiAuthenticator.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package com.inteligr8.activiti.ais;
-
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Map.Entry;
-import java.util.Set;
-
-import org.apache.commons.lang3.StringUtils;
-import org.keycloak.KeycloakPrincipal;
-import org.keycloak.KeycloakSecurityContext;
-import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
-import org.keycloak.representations.AccessToken;
-import org.keycloak.representations.AccessToken.Access;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.GrantedAuthority;
-
-import com.inteligr8.activiti.Authenticator;
-
-public abstract class AbstractIdentityServiceActivitiAuthenticator implements Authenticator {
-
- private final Logger logger = LoggerFactory.getLogger(this.getClass());
-
-
-
- protected Set getRoles(Authentication auth) {
- Set authorities = this.toSet(auth.getAuthorities());
- this.logger.debug("Auto-parsed authorities: {}", authorities);
-
- if (authorities.isEmpty()) {
- AccessToken atoken = this.getKeycloakAccessToken(auth);
- if (atoken == null) {
- this.logger.debug("Access token not available");
- return null;
- } else if (atoken.getRealmAccess() == null && atoken.getResourceAccess().isEmpty()) {
- this.logger.debug("Access token has no role information");
- return null;
- } else {
- if (atoken.getRealmAccess() != null) {
- this.logger.debug("Access token realm roles: {}", atoken.getRealmAccess().getRoles());
- authorities.addAll(atoken.getRealmAccess().getRoles());
- }
-
- for (Entry resourceAccess : atoken.getResourceAccess().entrySet()) {
- this.logger.debug("Access token resources '{}' roles: {}", resourceAccess.getKey(), resourceAccess.getValue().getRoles());
- authorities.addAll(resourceAccess.getValue().getRoles());
- }
-
- this.logger.debug("Access token authorities: {}", authorities);
- }
- }
-
- return authorities;
- }
-
- protected AccessToken getKeycloakAccessToken(Authentication auth) {
- KeycloakSecurityContext ksc = this.getKeycloakSecurityContext(auth);
- return ksc == null ? null : ksc.getToken();
- }
-
- @SuppressWarnings("unchecked")
- protected KeycloakSecurityContext getKeycloakSecurityContext(Authentication auth) {
- if (auth.getCredentials() instanceof KeycloakSecurityContext) {
- this.logger.debug("Found keycloak context in credentials");
- return (KeycloakSecurityContext)auth.getCredentials();
- } else if (auth.getPrincipal() instanceof KeycloakPrincipal) {
- this.logger.debug("Found keycloak context in principal: {}", auth.getPrincipal());
- return ((KeycloakPrincipal extends KeycloakSecurityContext>)auth.getPrincipal()).getKeycloakSecurityContext();
- } else if (!(auth instanceof KeycloakAuthenticationToken)) {
- this.logger.warn("Unexpected token: {}", auth.getClass());
- return null;
- }
-
- KeycloakAuthenticationToken ktoken = (KeycloakAuthenticationToken)auth;
- if (ktoken.getAccount() != null) {
- this.logger.debug("Found keycloak context in account: {}", ktoken.getAccount().getPrincipal() == null ? null : ktoken.getAccount().getPrincipal().getName());
- return ktoken.getAccount().getKeycloakSecurityContext();
- } else {
- this.logger.warn("Unable to find keycloak security context");
- this.logger.debug("Principal: {}", auth.getPrincipal());
- this.logger.debug("Account: {}", ktoken.getAccount());
- if (auth.getPrincipal() != null)
- this.logger.debug("Principal type: {}", auth.getPrincipal().getClass());
- return null;
- }
- }
-
- protected Set toSet(Collection extends GrantedAuthority> grantedAuthorities) {
- Set authorities = new HashSet<>(Math.max(grantedAuthorities.size(), 16));
- for (GrantedAuthority grantedAuthority : grantedAuthorities) {
- String authority = StringUtils.trimToNull(grantedAuthority.getAuthority());
- if (authority == null)
- this.logger.warn("The granted authorities include an empty authority!?: '{}'", grantedAuthority.getAuthority());
- authorities.add(authority);
- }
- return authorities;
- }
-}
diff --git a/src/main/java/com/inteligr8/activiti/ais/IdentityServiceAuthenticationProviderAdapter.java b/src/main/java/com/inteligr8/activiti/ais/IdentityServiceAuthenticationProviderAdapter.java
deleted file mode 100644
index f74ef19..0000000
--- a/src/main/java/com/inteligr8/activiti/ais/IdentityServiceAuthenticationProviderAdapter.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.inteligr8.activiti.ais;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.security.authentication.AuthenticationProvider;
-
-import com.activiti.api.security.AlfrescoAuthenticationProviderOverride;
-import com.inteligr8.activiti.Authenticator;
-
-/**
- * FIXME This would be nice, but with AIS enabled, it is never called. The use
- * of this requires a fix from the Alfresco/Activiti team. Their AIS
- * authentication logic appears to have been hastily added, breaking this
- * override possibility. We are instead using the heavier weight
- * `OidcSecurityConfigurationAdapter` and re-implementing the authentication
- * logic discovered in the `activiti-app` project
- * `com.activiti.conf.SecurityConfiguration` class.
- *
- * @author brian.long@yudrio.com
- * @see IdentityServiceSecurityConfigurationAdapter
- */
-//@Component
-public class IdentityServiceAuthenticationProviderAdapter implements AlfrescoAuthenticationProviderOverride {
-
- private final Logger logger = LoggerFactory.getLogger(this.getClass());
-
- @Autowired
- @Qualifier("activiti-app.authenticator")
- private Authenticator authenticator;
-
- @Override
- public AuthenticationProvider createAuthenticationProvider() {
- this.logger.trace("createAuthenticationProvider()");
- return new InterceptingIdentityServiceAuthenticationProvider(this.authenticator);
- }
-
-}
diff --git a/src/main/java/com/inteligr8/activiti/ais/IdentityServiceSecurityConfigurationAdapter.java b/src/main/java/com/inteligr8/activiti/ais/IdentityServiceSecurityConfigurationAdapter.java
index f729886..9e89944 100644
--- a/src/main/java/com/inteligr8/activiti/ais/IdentityServiceSecurityConfigurationAdapter.java
+++ b/src/main/java/com/inteligr8/activiti/ais/IdentityServiceSecurityConfigurationAdapter.java
@@ -1,3 +1,4 @@
+
package com.inteligr8.activiti.ais;
import org.slf4j.Logger;
@@ -11,28 +12,31 @@ import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import com.activiti.api.msmt.MsmtTenantResolver;
-import com.activiti.api.security.AlfrescoSecurityConfigOverride;
import com.activiti.conf.MsmtProperties;
-import com.inteligr8.activiti.Authenticator;
+import com.activiti.security.identity.service.authentication.provider.IdentityServiceAuthenticationProvider;
+import com.inteligr8.activiti.ActivitiSecurityConfigAdapter;
+import com.inteligr8.activiti.auth.Authenticator;
+import com.inteligr8.activiti.auth.InterceptingAuthenticationProvider;
/**
- * This class/bean overrides the AIS authentication provider, enabling a more
- * complete integration with AIS.
+ * This class/bean injects a custom AIS authentication provider into the
+ * security configuration.
*
- * FIXME This is not optimal, but with AIS enabled, we cannot use the proper
- * override.
- *
- * @author brian.long@yudrio.com
- * @see IdentityServiceAuthenticationProviderAdapter
+ * @author brian@inteligr8.com
+ * @see com.activiti.security.identity.service.authentication.provider.IdentityServiceAuthenticationProvider
*/
@Component
-public class IdentityServiceSecurityConfigurationAdapter implements AlfrescoSecurityConfigOverride {
+public class IdentityServiceSecurityConfigurationAdapter implements ActivitiSecurityConfigAdapter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
- @Value("${keycloak.ext.odic.enabled:true}")
+ @Value("${keycloak-ext.ais.enabled:false}")
private boolean enabled;
+ // this assures execution before the OOTB impl (-10 < 0)
+ @Value("${keycloak-ext.ais.priority:-10}")
+ private int priority;
+
@Autowired
protected MsmtProperties msmtProperties;
@@ -40,28 +44,36 @@ public class IdentityServiceSecurityConfigurationAdapter implements AlfrescoSecu
protected MsmtTenantResolver tenantResolver;
@Autowired
- @Qualifier("activiti-app.authenticator")
+ @Qualifier("keycloak-ext.activiti-app.authenticator")
private Authenticator authenticator;
protected Authenticator getAuthenticator() {
return this.authenticator;
}
+
+ @Override
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ @Override
+ public int getPriority() {
+ return this.priority;
+ }
@Override
- public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
+ public void configureGlobal(AuthenticationManagerBuilder auth, UserDetailsService userDetailsService) {
this.logger.trace("configureGlobal()");
- if (this.enabled) {
- this.logger.info("Using Keycloak authentication extension, featuring creation of missing users and authority synchronization");
-
- InterceptingIdentityServiceAuthenticationProvider provider = new InterceptingIdentityServiceAuthenticationProvider(this.getAuthenticator());
- if (this.msmtProperties.isMultiSchemaMultiTenantEnabled())
- provider.setTenantResolver(this.tenantResolver);
- provider.setUserDetailsService(userDetailsService);
- provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
-
- authmanBuilder.authenticationProvider(provider);
- }
+ this.logger.info("Using AIS authentication extension, featuring creation of missing users and authority synchronization");
+
+ IdentityServiceAuthenticationProvider provider = new IdentityServiceAuthenticationProvider();
+ if (this.msmtProperties.isMultiSchemaMultiTenantEnabled())
+ provider.setTenantResolver(this.tenantResolver);
+ provider.setUserDetailsService(userDetailsService);
+ provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
+
+ auth.authenticationProvider(new InterceptingAuthenticationProvider(provider, this.getAuthenticator()));
}
}
diff --git a/src/main/java/com/inteligr8/activiti/Authenticator.java b/src/main/java/com/inteligr8/activiti/auth/Authenticator.java
similarity index 91%
rename from src/main/java/com/inteligr8/activiti/Authenticator.java
rename to src/main/java/com/inteligr8/activiti/auth/Authenticator.java
index 6ef403d..821f42c 100644
--- a/src/main/java/com/inteligr8/activiti/Authenticator.java
+++ b/src/main/java/com/inteligr8/activiti/auth/Authenticator.java
@@ -1,4 +1,4 @@
-package com.inteligr8.activiti;
+package com.inteligr8.activiti.auth;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
diff --git a/src/main/java/com/inteligr8/activiti/ais/InterceptingIdentityServiceAuthenticationProvider.java b/src/main/java/com/inteligr8/activiti/auth/InterceptingAuthenticationProvider.java
similarity index 51%
rename from src/main/java/com/inteligr8/activiti/ais/InterceptingIdentityServiceAuthenticationProvider.java
rename to src/main/java/com/inteligr8/activiti/auth/InterceptingAuthenticationProvider.java
index bde2786..f49b7d2 100644
--- a/src/main/java/com/inteligr8/activiti/ais/InterceptingIdentityServiceAuthenticationProvider.java
+++ b/src/main/java/com/inteligr8/activiti/auth/InterceptingAuthenticationProvider.java
@@ -1,30 +1,35 @@
-package com.inteligr8.activiti.ais;
+package com.inteligr8.activiti.auth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
-import com.activiti.security.identity.service.authentication.provider.IdentityServiceAuthenticationProvider;
-import com.inteligr8.activiti.Authenticator;
-
/**
- * This class/bean extends the APS AIS OOTB authentication provider. It uses
- * an `Authenticator` to pre/post authenticate. The pre-authentication allows
- * us to circumvent the problem with AIS and missing users. The
- * post-authentication allow us to synchronize groups/authorities.
+ * This class/bean provides a pre/post authentication capability to the
+ * Spring AuthenticationProvider. The pre-authentication hook allows us to
+ * circumvent the problem with authenticating missing users. The
+ * post-authentication hook allow us to synchronize groups/authorities.
*
- * @author brian.long@yudrio.com
+ * @author brian@inteligr8.com
*/
-public class InterceptingIdentityServiceAuthenticationProvider extends IdentityServiceAuthenticationProvider {
-
+public class InterceptingAuthenticationProvider implements AuthenticationProvider {
+
private final Logger logger = LoggerFactory.getLogger(this.getClass());
+ private final AuthenticationProvider provider;
private final Authenticator authenticator;
- public InterceptingIdentityServiceAuthenticationProvider(Authenticator authenticator) {
+ public InterceptingAuthenticationProvider(AuthenticationProvider provider, Authenticator authenticator) {
+ this.provider = provider;
this.authenticator = authenticator;
}
+ @Override
+ public boolean supports(Class> authClass) {
+ return this.provider.supports(authClass);
+ }
+
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
this.logger.trace("authenticate({})", auth.getName());
@@ -32,13 +37,9 @@ public class InterceptingIdentityServiceAuthenticationProvider extends IdentityS
this.authenticator.preAuthenticate(auth);
this.logger.debug("Pre-authenticated user: {}", auth.getName());
- auth = super.authenticate(auth);
+ auth = this.provider.authenticate(auth);
this.logger.debug("Authenticated user '{}' with authorities: {}", auth.getName(), auth.getAuthorities());
- // FIXME temporary for debugging
- if (auth.getName().equals("admin@app.activiti.com"))
- return auth;
-
this.authenticator.postAuthenticate(auth);
this.logger.debug("Post-authenticated user: {}", auth.getName());
diff --git a/src/main/java/com/inteligr8/activiti/keycloak/AbstractKeycloakActivitiAuthenticator.java b/src/main/java/com/inteligr8/activiti/keycloak/AbstractKeycloakActivitiAuthenticator.java
new file mode 100644
index 0000000..1667d3d
--- /dev/null
+++ b/src/main/java/com/inteligr8/activiti/keycloak/AbstractKeycloakActivitiAuthenticator.java
@@ -0,0 +1,247 @@
+package com.inteligr8.activiti.keycloak;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.keycloak.KeycloakPrincipal;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessToken.Access;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.util.Pair;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+
+import com.inteligr8.activiti.auth.Authenticator;
+
+public abstract class AbstractKeycloakActivitiAuthenticator implements Authenticator, InitializingBean {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Value("${keycloak-ext.createMissingUser:true}")
+ protected boolean createMissingUser;
+
+ @Value("${keycloak-ext.clearNewUserGroups:true}")
+ protected boolean clearNewUserGroups;
+
+ @Value("${keycloak-ext.createMissingGroup:true}")
+ protected boolean createMissingGroup;
+
+ @Value("${keycloak-ext.syncGroupAdd:true}")
+ protected boolean syncGroupAdd;
+
+ @Value("${keycloak-ext.syncGroupRemove:true}")
+ protected boolean syncGroupRemove;
+
+ @Value("${keycloak-ext.resource.include.regex.patterns:#{null}}")
+ protected String resourceRegexIncludes;
+
+ @Value("${keycloak-ext.group.format.regex.patterns:#{null}}")
+ protected String regexPatterns;
+
+ @Value("${keycloak-ext.group.format.regex.replacements:#{null}}")
+ protected String regexReplacements;
+
+ @Value("${keycloak-ext.group.include.regex.patterns:#{null}}")
+ protected String regexIncludes;
+
+ @Value("${keycloak-ext.group.exclude.regex.patterns:#{null}}")
+ protected String regexExcludes;
+
+ protected final List> groupFormatters = new LinkedList<>();
+ protected final Set resourceIncludes = new HashSet<>();
+ protected final Set groupIncludes = new HashSet<>();
+ protected final Set groupExcludes = new HashSet<>();
+
+ @Override
+ public void afterPropertiesSet() {
+ if (this.regexPatterns != null) {
+ String[] regexPatternStrs = StringUtils.split(this.regexPatterns, ',');
+ String[] regexReplaceStrs = this.regexReplacements == null ? new String[0] : StringUtils.split(this.regexReplacements, ",");
+ for (int i = 0; i < regexPatternStrs.length; i++) {
+ Pattern regexPattern = Pattern.compile(regexPatternStrs[i]);
+ String regexReplace = (i < regexReplaceStrs.length) ? regexReplaceStrs[i] : "";
+ this.groupFormatters.add(Pair.of(regexPattern, regexReplace));
+ }
+ }
+
+ if (this.resourceRegexIncludes != null) {
+ String[] regexPatternStrs = StringUtils.split(this.resourceRegexIncludes, ',');
+ for (int i = 0; i < regexPatternStrs.length; i++)
+ this.resourceIncludes.add(Pattern.compile(regexPatternStrs[i]));
+ }
+
+ if (this.regexIncludes != null) {
+ String[] regexPatternStrs = StringUtils.split(this.regexIncludes, ',');
+ for (int i = 0; i < regexPatternStrs.length; i++)
+ this.groupIncludes.add(Pattern.compile(regexPatternStrs[i]));
+ }
+
+ if (this.regexExcludes != null) {
+ String[] regexPatternStrs = StringUtils.split(this.regexExcludes, ',');
+ for (int i = 0; i < regexPatternStrs.length; i++)
+ this.groupExcludes.add(Pattern.compile(regexPatternStrs[i]));
+ }
+ }
+
+
+
+ protected Map getRoles(Authentication auth) {
+ Map authorities = new HashMap<>();
+
+ AccessToken atoken = this.getKeycloakAccessToken(auth);
+ if (atoken == null) {
+ this.logger.debug("Access token not available");
+ return null;
+ } else if (atoken.getRealmAccess() == null && atoken.getResourceAccess().isEmpty()) {
+ this.logger.debug("Access token has no role information");
+ return null;
+ } else {
+ if (atoken.getRealmAccess() != null) {
+ this.logger.debug("Access token realm roles: {}", atoken.getRealmAccess().getRoles());
+ Collection roles = this.filterRoles(atoken.getRealmAccess().getRoles());
+ Map mappedRoles = this.formatRoles(roles);
+ authorities.putAll(mappedRoles);
+ }
+
+ for (Entry resourceAccess : atoken.getResourceAccess().entrySet()) {
+ if (this.includeResource(resourceAccess.getKey())) {
+ this.logger.debug("Access token resources '{}' roles: {}", resourceAccess.getKey(), resourceAccess.getValue().getRoles());
+ Collection roles = this.filterRoles(resourceAccess.getValue().getRoles());
+ Map mappedRoles = this.formatRoles(roles);
+ authorities.putAll(mappedRoles);
+ }
+ }
+
+ this.logger.debug("Access token authorities: {}", authorities);
+ }
+
+ return authorities;
+ }
+
+ private Collection filterRoles(Collection unfilteredRoles) {
+ if (this.groupIncludes.isEmpty() && this.groupExcludes.isEmpty())
+ return unfilteredRoles;
+
+ Set filteredRoles = new HashSet<>(unfilteredRoles.size());
+
+ for (String role : unfilteredRoles) {
+ boolean doInclude = this.groupIncludes.isEmpty();
+ for (Pattern regex : this.groupIncludes) {
+ Matcher matcher = regex.matcher(role);
+ if (matcher.matches()) {
+ this.logger.debug("Role matched inclusion filter: {}", role);
+ doInclude = true;
+ break;
+ }
+ }
+
+ if (doInclude) {
+ for (Pattern regex : this.groupExcludes) {
+ Matcher matcher = regex.matcher(role);
+ if (matcher.matches()) {
+ this.logger.debug("Role matched exclusion filter: {}", role);
+ doInclude = false;
+ break;
+ }
+ }
+
+ if (doInclude)
+ filteredRoles.add(role);
+ }
+ }
+
+ return filteredRoles;
+ }
+
+ private Map formatRoles(Collection unformattedRoles) {
+ Map formattedRoles = new HashMap<>(unformattedRoles.size());
+
+ for (String unformattedRole : unformattedRoles) {
+ String formattedRole = null;
+
+ for (Pair regex : this.groupFormatters) {
+ Matcher matcher = regex.getFirst().matcher(unformattedRole);
+ if (matcher.matches()) {
+ this.logger.trace("Role matched formatter: {}", unformattedRole);
+ formattedRole = matcher.replaceFirst(regex.getSecond());
+ this.logger.debug("Role formatted: {}", formattedRole);
+ break;
+ }
+ }
+
+ formattedRoles.put(unformattedRole, formattedRole == null ? unformattedRole : formattedRole);
+ }
+
+ return formattedRoles;
+ }
+
+ private boolean includeResource(String resource) {
+ if (this.resourceIncludes.isEmpty())
+ return true;
+
+ for (Pattern resourceInclude : this.resourceIncludes) {
+ Matcher matcher = resourceInclude.matcher(resource);
+ if (matcher.matches())
+ return true;
+ }
+
+ return false;
+ }
+
+ protected AccessToken getKeycloakAccessToken(Authentication auth) {
+ KeycloakSecurityContext ksc = this.getKeycloakSecurityContext(auth);
+ return ksc == null ? null : ksc.getToken();
+ }
+
+ @SuppressWarnings("unchecked")
+ protected KeycloakSecurityContext getKeycloakSecurityContext(Authentication auth) {
+ if (auth.getCredentials() instanceof KeycloakSecurityContext) {
+ this.logger.debug("Found keycloak context in credentials");
+ return (KeycloakSecurityContext)auth.getCredentials();
+ } else if (auth.getPrincipal() instanceof KeycloakPrincipal) {
+ this.logger.debug("Found keycloak context in principal: {}", auth.getPrincipal());
+ return ((KeycloakPrincipal extends KeycloakSecurityContext>)auth.getPrincipal()).getKeycloakSecurityContext();
+ } else if (!(auth instanceof KeycloakAuthenticationToken)) {
+ this.logger.warn("Unexpected token: {}", auth.getClass());
+ return null;
+ }
+
+ KeycloakAuthenticationToken ktoken = (KeycloakAuthenticationToken)auth;
+ if (ktoken.getAccount() != null) {
+ this.logger.debug("Found keycloak context in account: {}", ktoken.getAccount().getPrincipal() == null ? null : ktoken.getAccount().getPrincipal().getName());
+ return ktoken.getAccount().getKeycloakSecurityContext();
+ } else {
+ this.logger.warn("Unable to find keycloak security context");
+ this.logger.debug("Principal: {}", auth.getPrincipal());
+ this.logger.debug("Account: {}", ktoken.getAccount());
+ if (auth.getPrincipal() != null)
+ this.logger.debug("Principal type: {}", auth.getPrincipal().getClass());
+ return null;
+ }
+ }
+
+ protected Set toSet(Collection extends GrantedAuthority> grantedAuthorities) {
+ Set authorities = new HashSet<>(Math.max(grantedAuthorities.size(), 16));
+ for (GrantedAuthority grantedAuthority : grantedAuthorities) {
+ String authority = StringUtils.trimToNull(grantedAuthority.getAuthority());
+ if (authority == null)
+ this.logger.warn("The granted authorities include an empty authority!?: '{}'", grantedAuthority.getAuthority());
+ authorities.add(authority);
+ }
+ return authorities;
+ }
+}
diff --git a/src/main/java/com/inteligr8/activiti/ais/IdentityServiceActivitiAppAuthenticator.java b/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiAppAuthenticator.java
similarity index 78%
rename from src/main/java/com/inteligr8/activiti/ais/IdentityServiceActivitiAppAuthenticator.java
rename to src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiAppAuthenticator.java
index 3c250ad..282d4e2 100644
--- a/src/main/java/com/inteligr8/activiti/ais/IdentityServiceActivitiAppAuthenticator.java
+++ b/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiAppAuthenticator.java
@@ -1,8 +1,9 @@
-package com.inteligr8.activiti.ais;
+package com.inteligr8.activiti.keycloak;
import java.util.Date;
import java.util.List;
-import java.util.Set;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -11,7 +12,6 @@ import org.keycloak.representations.AccessToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@@ -24,7 +24,6 @@ import com.activiti.service.api.GroupService;
import com.activiti.service.api.UserService;
import com.activiti.service.idm.TenantService;
import com.activiti.service.license.LicenseService;
-import com.inteligr8.activiti.Authenticator;
/**
* This class/bean implements an Open ID Connect authenticator for Alfresco
@@ -39,29 +38,14 @@ import com.inteligr8.activiti.Authenticator;
*
* @author brian.long@yudrio.com
*/
-@Component("activiti-app.authenticator")
+@Component("keycloak-ext.activiti-app.authenticator")
@Lazy
-public class IdentityServiceActivitiAppAuthenticator extends AbstractIdentityServiceActivitiAuthenticator implements Authenticator {
+public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAuthenticator {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Pattern emailNamesPattern = Pattern.compile("([A-Za-z]+)[A-Za-z0-9]*\\.([A-Za-z]+)[A-Za-z0-9]*@.*");
private final String externalIdmSource = "ais";
-
- @Value("${keycloak-ext.createMissingUser:true}")
- private boolean createMissingUser;
-
- @Value("${keycloak-ext.clearNewUserGroups:true}")
- private boolean clearNewUserGroups;
-
- @Value("${keycloak-ext.createMissingGroup:true}")
- private boolean createMissingGroup;
-
- @Value("${keycloak-ext.syncGroupAdd:true}")
- private boolean syncGroupAdd;
-
- @Value("${keycloak-ext.syncGroupRemove:true}")
- private boolean syncGroupRemove;
@Autowired
private LicenseService licenseService;
@@ -116,7 +100,7 @@ public class IdentityServiceActivitiAppAuthenticator extends AbstractIdentitySer
User user = this.findUser(auth, tenantId);
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getExternalId());
- this.syncUserAuthorities(user, auth, tenantId);
+ this.syncUserRoles(user, auth, tenantId);
}
private Long findDefaultTenantId() {
@@ -165,50 +149,51 @@ public class IdentityServiceActivitiAppAuthenticator extends AbstractIdentitySer
}
}
- private void syncUserAuthorities(User user, Authentication auth, Long tenantId) {
- Set authorities = this.getRoles(auth);
- if (authorities == null) {
- this.logger.debug("The user authorities could not be determined; skipping sync: {}", user.getEmail());
+ private void syncUserRoles(User user, Authentication auth, Long tenantId) {
+ Map roles = this.getRoles(auth);
+ if (roles == null) {
+ this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail());
return;
}
// check Activiti groups
User userWithGroups = this.userService.findUserByEmailFetchGroups(user.getEmail());
for (Group group : userWithGroups.getGroups()) {
- this.logger.trace("Inspecting group: {} => ", group.getId(), group.getExternalId());
+ this.logger.trace("Inspecting group: {} => ", group.getId(), group.getName());
- if (authorities.remove(group.getExternalId())) {
+ if (group.getExternalId() == null) {
+ // skip APS system groups
+ } else if (roles.remove(group.getExternalId()) != null) {
// all good
} else {
if (this.syncGroupRemove) {
- this.logger.trace("Removing user '{}' from group '{}'", user.getExternalId(), group.getExternalId());
+ this.logger.trace("Removing user '{}' from group '{}'", user.getExternalId(), group.getName());
this.groupService.deleteUserFromGroup(group, userWithGroups);
} else {
- this.logger.debug("User/group membership sync disabled; not removing user from group: {} => {}", user.getExternalId(), group.getExternalId());
+ this.logger.debug("User/group membership sync disabled; not removing user from group: {} => {}", user.getExternalId(), group.getName());
}
}
}
// add remaining authorities into Activiti
- for (String authority : authorities) {
- this.logger.trace("Syncing group membership: {}", authority);
+ for (Entry role : roles.entrySet()) {
+ this.logger.trace("Syncing group membership: {}", role);
- Group group = this.groupService.getGroupByExternalId(authority);
+ Group group = this.groupService.getGroupByExternalId(role.getKey());
if (group == null) {
if (this.createMissingGroup) {
- this.logger.trace("Creating new group: {}", authority);
- String shortAuthority = authority.replaceFirst("[A-Z]+_", "");
- group = this.groupService.createGroupFromExternalStore(shortAuthority, tenantId, Group.TYPE_SYSTEM_GROUP, null, authority, new Date());
+ this.logger.trace("Creating new group: {}", role);
+ group = this.groupService.createGroupFromExternalStore(role.getValue(), tenantId, Group.TYPE_SYSTEM_GROUP, null, role.getKey(), new Date());
} else {
- this.logger.debug("Group does not exist; group creation is disabled: {}", authority);
+ this.logger.debug("Group does not exist; group creation is disabled: {}", role);
}
}
if (group != null && this.syncGroupAdd) {
- this.logger.trace("Adding user '{}' from group '{}'", user.getExternalId(), group.getExternalId());
+ this.logger.trace("Adding user '{}' to group '{}'", user.getExternalId(), group.getName());
this.groupService.addUserToGroup(group, userWithGroups);
} else {
- this.logger.debug("User/group membership sync disabled; not adding user to group: {} => {}", user.getExternalId(), group.getExternalId());
+ this.logger.debug("User/group membership sync disabled; not adding user to group: {} => {}", user.getExternalId(), group.getName());
}
}
}
diff --git a/src/main/java/com/inteligr8/activiti/ais/IdentityServiceActivitiEngineAuthenticator.java b/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiEngineAuthenticator.java
similarity index 76%
rename from src/main/java/com/inteligr8/activiti/ais/IdentityServiceActivitiEngineAuthenticator.java
rename to src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiEngineAuthenticator.java
index fcfffdd..b58cf2a 100644
--- a/src/main/java/com/inteligr8/activiti/ais/IdentityServiceActivitiEngineAuthenticator.java
+++ b/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiEngineAuthenticator.java
@@ -1,7 +1,8 @@
-package com.inteligr8.activiti.ais;
+package com.inteligr8.activiti.keycloak;
import java.util.List;
-import java.util.Set;
+import java.util.Map;
+import java.util.Map.Entry;
import org.activiti.engine.IdentityService;
import org.activiti.engine.identity.Group;
@@ -15,37 +16,23 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
-import com.inteligr8.activiti.Authenticator;
-
/**
* This is an unused implementation for non-APS installation. It is not tested
* and probably pointless.
*
* @author brian.long@yudrio.com
*/
-@Component("activiti.authenticator")
+@Component("keycloak-ext.activiti-engine.authenticator")
@Lazy
-public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentityServiceActivitiAuthenticator implements Authenticator {
+public class KeycloakActivitiEngineAuthenticator extends AbstractKeycloakActivitiAuthenticator {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
- @Value("${keycloak-ext.createMissingUser:true}")
- private boolean createMissingUser;
-
- @Value("${keycloak-ext.clearNewUserGroups:true}")
- private boolean clearNewUserGroups;
-
- @Value("${keycloak-ext.createMissingGroup:true}")
- private boolean createMissingGroup;
-
- @Value("${keycloak-ext.syncGroupAdd:true}")
- private boolean syncGroupAdd;
-
- @Value("${keycloak-ext.syncGroupRemove:true}")
- private boolean syncGroupRemove;
-
@Autowired
private IdentityService identityService;
+
+ @Value("${keycloak-ext.group.prefix:KEYCLOAK_}")
+ private String groupPrefix;
/**
* This method validates that the user exists, if not, it creates the
@@ -85,7 +72,7 @@ public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentity
User user = this.findUser(auth);
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getEmail());
- this.syncUserAuthorities(user, auth);
+ this.syncUserRoles(user, auth);
}
private User findUser(Authentication auth) {
@@ -105,10 +92,10 @@ public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentity
return user;
}
- private void syncUserAuthorities(User user, Authentication auth) {
- Set authorities = this.getRoles(auth);
- if (authorities == null) {
- this.logger.debug("The user authorities could not be determined; skipping sync: {}", user.getEmail());
+ private void syncUserRoles(User user, Authentication auth) {
+ Map roles = this.getRoles(auth);
+ if (roles == null) {
+ this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail());
return;
}
@@ -118,8 +105,11 @@ public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentity
.list();
this.logger.debug("User is currently a member of {} groups", groups.size());
for (Group group : groups) {
+ if (!group.getId().startsWith(this.groupPrefix))
+ continue;
+
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getType());
- if (authorities.remove(group.getName())) {
+ if (roles.remove(group.getId().substring(this.groupPrefix.length())) != null) {
this.logger.trace("Group and membership already exist: {} => {}", user.getEmail(), group.getName());
// already a member of the group
} else {
@@ -132,20 +122,20 @@ public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentity
}
}
- this.logger.debug("Unaddressed OIDC authorities: {}", authorities);
+ this.logger.debug("Unaddressed OIDC roles: {}", roles);
- // check remainder/unaddressed authorities
- for (String authority : authorities) {
- this.logger.trace("Inspecting authority: {}", authority);
+ // check remainder/unaddressed roles
+ for (Entry role : roles.entrySet()) {
+ this.logger.trace("Inspecting role: {}", role);
Group group = this.identityService.createGroupQuery()
- .groupName(authority)
+ .groupId(this.groupPrefix + role.getKey())
.singleResult();
if (group == null) {
if (this.createMissingGroup) {
this.logger.trace("Group does not exist; creating one");
- group = this.identityService.newGroup(authority);
- group.setName(authority);
+ group = this.identityService.newGroup(this.groupPrefix + role.getKey());
+ group.setName(role.getValue());
this.identityService.saveGroup(group);
} else {
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName());
diff --git a/src/main/java/com/inteligr8/activiti/keycloak/KeycloakSecurityConfigurationAdapter.java b/src/main/java/com/inteligr8/activiti/keycloak/KeycloakSecurityConfigurationAdapter.java
new file mode 100644
index 0000000..f3e065b
--- /dev/null
+++ b/src/main/java/com/inteligr8/activiti/keycloak/KeycloakSecurityConfigurationAdapter.java
@@ -0,0 +1,67 @@
+package com.inteligr8.activiti.keycloak;
+
+import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Component;
+
+import com.inteligr8.activiti.ActivitiSecurityConfigAdapter;
+import com.inteligr8.activiti.auth.Authenticator;
+import com.inteligr8.activiti.auth.InterceptingAuthenticationProvider;
+
+/**
+ * This class/bean injects a custom keycloak authentication provider into the
+ * security configuration.
+ *
+ * @author brian@inteligr8.com
+ * @see org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
+ */
+@Component
+public class KeycloakSecurityConfigurationAdapter implements ActivitiSecurityConfigAdapter {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Value("${keycloak-ext.keycloak.enabled:false}")
+ private boolean enabled;
+
+ // this assures execution before the OOTB impl (-10 < 0)
+ @Value("${keycloak-ext.keycloak.priority:-5}")
+ private int priority;
+
+ @Autowired
+ @Qualifier("keycloak-ext.activiti-app.authenticator")
+ private Authenticator authenticator;
+
+ protected Authenticator getAuthenticator() {
+ return this.authenticator;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ @Override
+ public int getPriority() {
+ return this.priority;
+ }
+
+ @Override
+ public void configureGlobal(AuthenticationManagerBuilder auth, UserDetailsService userDetailsService) {
+ this.logger.trace("configureGlobal()");
+
+ this.logger.info("Using Keycloak authentication extension, featuring creation of missing users and authority synchronization");
+
+ KeycloakAuthenticationProvider provider = new KeycloakAuthenticationProvider();
+ provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
+
+ auth.authenticationProvider(new InterceptingAuthenticationProvider(provider, this.getAuthenticator()));
+ }
+
+}