Index: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleFactory.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleFactory.java	(revision 1745339)
+++ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleFactory.java	(revision )
@@ -17,12 +17,12 @@
 package org.apache.jackrabbit.oak.spi.security.authentication.external.impl;
 
 import java.util.Hashtable;
-
 import javax.jcr.Repository;
 import javax.management.MalformedObjectNameException;
 import javax.management.ObjectName;
 import javax.security.auth.spi.LoginModule;
 
+import com.google.common.collect.ImmutableMap;
 import org.apache.felix.jaas.LoginModuleFactory;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -43,8 +43,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.ImmutableMap;
-
 /**
  * Implements a LoginModuleFactory that creates {@link ExternalLoginModule}s and allows to configure login modules
  * via OSGi config.
@@ -56,7 +54,7 @@
         configurationFactory = true
 )
 @Service
-public class ExternalLoginModuleFactory implements LoginModuleFactory {
+public class ExternalLoginModuleFactory implements LoginModuleFactory, SyncHandlerMapping {
 
     private static final Logger log = LoggerFactory.getLogger(ExternalLoginModuleFactory.class);
 
@@ -92,14 +90,14 @@
             label = "Identity Provider Name",
             description = "Name of the identity provider (for example: 'ldap')."
     )
-    public static final String PARAM_IDP_NAME = ExternalLoginModule.PARAM_IDP_NAME;
+    public static final String PARAM_IDP_NAME = SyncHandlerMapping.PARAM_IDP_NAME;
 
     @Property(
             value = "default",
             label = "Sync Handler Name",
             description = "Name of the sync handler."
     )
-    public static final String PARAM_SYNC_HANDLER_NAME = ExternalLoginModule.PARAM_SYNC_HANDLER_NAME;
+    public static final String PARAM_SYNC_HANDLER_NAME = SyncHandlerMapping.PARAM_SYNC_HANDLER_NAME;
 
     @Reference
     private SyncManager syncManager;
@@ -120,6 +118,7 @@
      */
     private Registration mbeanRegistration;
 
+    //----------------------------------------------------< SCR integration >---
     /**
      * Activates the LoginModuleFactory service
      * @param context the component context
\ No newline at end of file
Index: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContext.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContext.java	(revision 1745339)
+++ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContext.java	(revision )
@@ -135,6 +135,11 @@
         }
     }
 
+    @Override
+    protected void applyMembership(@Nonnull Authorizable member, @Nonnull Set<String> groups) throws RepositoryException {
+        log.debug("Dynamic membership sync enabled => omit setting auto-membership for {} ", member.getID());
+    }
+
     /**
      * Recursively collect the principal names of the given declared group
      * references up to the given depth.
\ No newline at end of file
Index: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncHandlerMapping.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncHandlerMapping.java	(revision )
+++ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncHandlerMapping.java	(revision )
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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.apache.jackrabbit.oak.spi.security.authentication.external.impl;
+
+/**
+ * Marker interface identifying classes that map a given
+ * {@link org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler SyncHandler}
+ * to an {@link org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider ExternalIdentityProvider}
+ * where both are identified by their name.
+ *
+ * @see org.apache.jackrabbit.oak.spi.security.authentication.external.SyncManager#getSyncHandler(String)
+ * @see org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler#getName()
+ * @see org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProviderManager#getProvider(String)
+ * @see org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider#getName()
+ * @see ExternalLoginModuleFactory
+ */
+public interface SyncHandlerMapping {
+
+    /**
+     * Name of the parameter that configures the name of the external identity provider.
+     */
+    String PARAM_IDP_NAME = "idp.name";
+
+    /**
+     * Name of the parameter that configures the name of the synchronization handler.
+     */
+    String PARAM_SYNC_HANDLER_NAME = "sync.handlerName";
+
+}
\ No newline at end of file
Index: oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContextTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContextTest.java	(revision 1745339)
+++ oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContextTest.java	(revision )
@@ -18,6 +18,7 @@
 
 import java.util.HashSet;
 import java.util.Set;
+import java.util.UUID;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.jcr.RepositoryException;
@@ -31,6 +32,7 @@
 import com.google.common.collect.Sets;
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
 import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Root;
@@ -416,6 +418,21 @@
 
         assertFalse(gr.hasProperty(ExternalIdentityConstants.REP_EXTERNAL_PRINCIPAL_NAMES));
         assertFalse(r.hasPendingChanges());
+    }
+
+    @Test
+    public void testAutoMembership() throws Exception {
+        Group gr = userManager.createGroup("group" + UUID.randomUUID());
+        r.commit();
+
+        syncConfig.user().setAutoMembership(gr.getID(), "non-existing-group");
+
+        SyncResult result = syncContext.sync(idp.getUser(USER_ID));
+        assertSame(SyncResult.Status.ADD, result.getStatus());
+
+        User u = userManager.getAuthorizable(USER_ID, User.class);
+        assertFalse(gr.isDeclaredMember(u));
+        assertFalse(gr.isMember(u));
     }
 
     private static final class TestUserWithGroupRefs extends TestIdentityProvider.TestIdentity implements ExternalUser {
\ No newline at end of file
Index: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalGroupPrincipalProvider.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalGroupPrincipalProvider.java	(revision 1745339)
+++ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalGroupPrincipalProvider.java	(revision )
@@ -19,12 +19,14 @@
 import java.security.Principal;
 import java.security.acl.Group;
 import java.text.ParseException;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -54,6 +56,7 @@
 import org.apache.jackrabbit.oak.api.Type;
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
 import org.apache.jackrabbit.oak.spi.query.PropertyValues;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncConfig;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
@@ -95,12 +98,18 @@
 
     private final Root root;
     private final NamePathMapper namePathMapper;
+
     private final UserManager userManager;
+    private final AutoMembershipPrincipals autoMembershipPrincipals;
 
-    ExternalGroupPrincipalProvider(Root root, UserConfiguration uc, NamePathMapper namePathMapper) {
+    ExternalGroupPrincipalProvider(@Nonnull Root root, @Nonnull UserConfiguration uc,
+                                   @Nonnull NamePathMapper namePathMapper,
+                                   @Nonnull Map<String, String[]> autoMembershipMapping) {
         this.root = root;
         this.namePathMapper = namePathMapper;
+
         userManager = uc.getUserManager(root, namePathMapper);
+        autoMembershipPrincipals = new AutoMembershipPrincipals(autoMembershipMapping);
     }
 
     //--------------------------------------------------< PrincipalProvider >---
@@ -164,6 +173,15 @@
     }
 
     //------------------------------------------------------------< private >---
+    @CheckForNull
+    private String getIdpName(@Nonnull Tree userTree) {
+        PropertyState ps = userTree.getProperty(REP_EXTERNAL_ID);
+        if (ps != null) {
+            return ExternalIdentityRef.fromString(ps.getValue(Type.STRING)).getProviderName();
+        } else {
+            return null;
+        }
+    }
 
     private Set<Group> getGroupPrincipals(@CheckForNull Authorizable authorizable) throws RepositoryException {
         if (authorizable != null && !authorizable.isGroup()) {
@@ -178,10 +196,14 @@
         if (userTree.exists() && UserUtil.isType(userTree, AuthorizableType.USER) && userTree.hasProperty(REP_EXTERNAL_PRINCIPAL_NAMES)) {
             PropertyState ps = userTree.getProperty(REP_EXTERNAL_PRINCIPAL_NAMES);
             if (ps != null) {
+                // we have an 'external' user that has been synchronized with the dynamic-membership option
                 Set<Group> groupPrincipals = Sets.newHashSet();
                 for (String principalName : ps.getValue(Type.STRINGS)) {
                     groupPrincipals.add(new ExternalGroupPrincipal(principalName));
                 }
+
+                // add existing group principals as defined with the _autoMembership_ option.
+                groupPrincipals.addAll(autoMembershipPrincipals.get(getIdpName(userTree)));
                 return groupPrincipals;
             }
         }
@@ -194,6 +216,10 @@
      * {@link #REP_EXTERNAL_PRINCIPAL_NAMES} properties that match the given
      * name or name hint.
      *
+     * NOTE: ignore any principals listed in the {@link DefaultSyncConfig.User#autoMembership}
+     * because they are expected to exist in the system and thus will be found
+     * by another principal provider instance.
+     *
      * @param nameHint The principal name or name hint to be searched for.
      * @param exactMatch boolean flag indicating if the query should search for
      *                   exact matching.
@@ -407,6 +433,56 @@
                 }
             }
             return null;
+        }
+    }
+
+    private final class AutoMembershipPrincipals {
+
+        private final Map<String, String[]> autoMembershipMapping;
+        private final Map<String, Set<Group>> principalMap;
+
+        private AutoMembershipPrincipals(@Nonnull Map<String, String[]> autoMembershipMapping) {
+            this.autoMembershipMapping = autoMembershipMapping;
+            this.principalMap = new ConcurrentHashMap<String, Set<Group>>(autoMembershipMapping.size());
+        }
+
+        @Nonnull
+        private Collection<Group> get(@CheckForNull String idpName) {
+            if (idpName == null) {
+                return ImmutableSet.of();
+            }
+
+            Set<Group> principals;
+            if (!principalMap.containsKey(idpName)) {
+                String[] vs = autoMembershipMapping.get(idpName);
+                if (vs == null) {
+                    principals = ImmutableSet.of();
+                } else {
+                    ImmutableSet.Builder<Group> builder = ImmutableSet.builder();
+                    for (String groupId : autoMembershipMapping.get(idpName)) {
+                        try {
+                            Authorizable gr = userManager.getAuthorizable(groupId);
+                            if (gr != null && gr.isGroup()) {
+                                Principal grPrincipal = gr.getPrincipal();
+                                if (grPrincipal instanceof Group) {
+                                    builder.add((Group) grPrincipal);
+                                } else {
+                                    log.warn("Principal of group {} is not of type java.security.acl.Group -> Ignoring", groupId);
+                                }
+                            } else {
+                                log.warn("Configured auto-membership group {} does not exist -> Ignoring", groupId);
+                            }
+                        } catch (RepositoryException e) {
+                            log.debug("Failed to retrieved 'auto-membership' group with id {}", groupId, e);
+                        }
+                    }
+                    principals = builder.build();
+                }
+                principalMap.put(idpName, principals);
+            } else {
+                principals = principalMap.get(idpName);
+            }
+            return principals;
         }
     }
 }
\ No newline at end of file
Index: oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleAutoMembershipTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleAutoMembershipTest.java	(revision )
+++ oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleAutoMembershipTest.java	(revision )
@@ -0,0 +1,438 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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.apache.jackrabbit.oak.spi.security.authentication.external;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.jcr.SimpleCredentials;
+import javax.jcr.ValueFactory;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncConfig;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncConfigImpl;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncHandler;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalLoginModule;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.SyncHandlerMapping;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
+import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
+import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class ExternalLoginModuleAutoMembershipTest extends ExternalLoginModuleTestBase {
+
+    private static final String NON_EXISTING_NAME = "nonExisting";
+
+    @Rule
+    public final OsgiContext context = new OsgiContext();
+
+    private Root r;
+    private UserManager userManager;
+    private ValueFactory valueFactory;
+
+    private ExternalSetup setup1;
+    private ExternalSetup setup2;
+    private ExternalSetup setup3;
+    private ExternalSetup setup4;
+    private ExternalSetup setup5;
+
+    @Override
+    public void before() throws Exception {
+        super.before();
+
+        r = getSystemRoot();
+        userManager = getUserManager(r);
+        valueFactory = getValueFactory(r);
+
+        syncConfig.user().setDynamicMembership(true);
+
+        // register the ExternalPrincipal configuration in order to have it's
+        // activate method invoked.
+        context.registerInjectActivateService(externalPrincipalConfiguration);
+
+        // first configuration based on test base-setup with
+        // - dynamic membership = true
+        // - auto-membership = 'gr_default' and 'nonExisting'
+        syncConfig.user().setDynamicMembership(true);
+        setup1 = new ExternalSetup(idp, syncConfig, WhiteboardUtils.getService(whiteboard, SyncHandler.class), "gr" + UUID.randomUUID());
+
+        // second configuration with different IDP ('idp2') and
+        // - dynamic membership = true
+        // - auto-membership = 'gr_name2' and 'nonExisting'
+        DefaultSyncConfig sc2 = new DefaultSyncConfig();
+        sc2.setName("name2").user().setDynamicMembership(true);
+        setup2 = new ExternalSetup(new TestIdentityProvider("idp2"), sc2);
+
+        // third configuration with different IDP  ('idp3') and
+        // - dynamic membership = false
+        // - auto-membership = 'gr_name3' and 'nonExisting'
+        DefaultSyncConfig sc3 = new DefaultSyncConfig();
+        sc3.setName("name3");
+        setup3 = new ExternalSetup(new TestIdentityProvider("idp3"), sc3);
+
+        // forth configuration based on different IDP ('idp4') but re-using
+        // sync-handler configuration (sc2)
+        setup4 = new ExternalSetup(new TestIdentityProvider("idp4"), sc2);
+
+        // fifth configuration with different IDP ('idp5') and
+        // - dynamic membership = true
+        // - auto-membership => nothing configured
+        DefaultSyncConfig sc5 = new DefaultSyncConfig();
+        sc5.setName("name5").user().setDynamicMembership(true);
+        setup5 = new ExternalSetup(new TestIdentityProvider("idp5"), sc5, new DefaultSyncHandler(sc5), null);
+    }
+
+    @Override
+    public void after() throws Exception {
+        try {
+            syncConfig.user().setAutoMembership().setExpirationTime(0);
+
+            setup1.close();
+            setup2.close();
+            setup3.close();
+            setup4.close();
+        } finally {
+            super.after();
+        }
+    }
+
+    @Override
+    protected Configuration getConfiguration() {
+        return new Configuration() {
+            @Override
+            public AppConfigurationEntry[] getAppConfigurationEntry(String s) {
+                AppConfigurationEntry[] entries = new AppConfigurationEntry[5];
+                int i = 0;
+                for (ExternalSetup setup : new ExternalSetup[] {setup1, setup2, setup3, setup4, setup5}) {
+                    entries[i++] = setup.asConfigurationEntry();
+                }
+                return entries;
+            }
+        };
+    }
+
+    private static void registerSyncHandlerMapping(@Nonnull OsgiContext ctx, @Nonnull ExternalSetup setup) {
+        String syncHandlerName = setup.sc.getName();
+        Map props = ImmutableMap.of(
+                DefaultSyncConfigImpl.PARAM_NAME, syncHandlerName,
+                DefaultSyncConfigImpl.PARAM_USER_DYNAMIC_MEMBERSHIP, setup.sc.user().getDynamicMembership(),
+                DefaultSyncConfigImpl.PARAM_GROUP_AUTO_MEMBERSHIP, setup.sc.user().getAutoMembership());
+        ctx.registerService(SyncHandler.class, setup.sh, props);
+
+        Map mappingProps = ImmutableMap.of(
+                SyncHandlerMapping.PARAM_IDP_NAME, setup.idp.getName(),
+                SyncHandlerMapping.PARAM_SYNC_HANDLER_NAME, syncHandlerName);
+        ctx.registerService(SyncHandlerMapping.class, new SyncHandlerMapping() {}, mappingProps);
+    }
+
+    @Test
+    public void testLoginSyncAutoMembershipSetup1() throws Exception {
+        ContentSession cs = null;
+        try {
+            cs = login(new SimpleCredentials(USER_ID, new char[0]));
+
+            // the login must set the existing auto-membership principals to the subject
+            Set<Principal> principals = cs.getAuthInfo().getPrincipals();
+            assertTrue(principals.contains(setup1.gr.getPrincipal()));
+
+            assertFalse(principals.contains(new PrincipalImpl(NON_EXISTING_NAME)));
+            assertFalse(principals.contains(setup2.gr.getPrincipal()));
+            assertFalse(principals.contains(setup3.gr.getPrincipal()));
+
+            // however, the existing auto-membership group must _not_ have changed
+            // and the test user must not be a stored member of this group.
+            root.refresh();
+            UserManager uMgr = getUserManager(root);
+
+            User user = uMgr.getAuthorizable(USER_ID, User.class);
+            Group gr = uMgr.getAuthorizable(setup1.gr.getID(), Group.class);
+
+            assertFalse(gr.isDeclaredMember(user));
+            assertFalse(gr.isMember(user));
+        } finally {
+            options.clear();
+            if (cs != null) {
+                cs.close();
+            }
+        }
+    }
+
+    @Test
+    public void testLoginAfterSyncSetup1() throws Exception {
+        setup1.sync(USER_ID, false);
+
+        ContentSession cs = null;
+        try {
+            cs = login(new SimpleCredentials(USER_ID, new char[0]));
+
+            // the login must set the configured + existing auto-membership principals
+            // to the subject; non-existing auto-membership entries must be ignored.
+            Set<Principal> principals = cs.getAuthInfo().getPrincipals();
+            assertTrue(principals.contains(setup1.gr.getPrincipal()));
+
+            assertFalse(principals.contains(new PrincipalImpl(NON_EXISTING_NAME)));
+            assertFalse(principals.contains(setup2.gr.getPrincipal()));
+            assertFalse(principals.contains(setup3.gr.getPrincipal()));
+
+            // however, the existing auto-membership group must _not_ have changed
+            // and the test user must not be a stored member of this group.
+            root.refresh();
+            UserManager uMgr = getUserManager(root);
+
+            User user = uMgr.getAuthorizable(USER_ID, User.class);
+            Group gr = uMgr.getAuthorizable(setup1.gr.getID(), Group.class);
+
+            assertFalse(gr.isDeclaredMember(user));
+            assertFalse(gr.isMember(user));
+        } finally {
+            options.clear();
+            if (cs != null) {
+                cs.close();
+            }
+        }
+    }
+
+    @Test
+    public void testLoginAfterSyncSetup2() throws Exception {
+        setup2.sync(USER_ID, false);
+
+        ContentSession cs = null;
+        try {
+            cs = login(new SimpleCredentials(USER_ID, new char[0]));
+
+            // the login must set the existing auto-membership principals to the subject
+            Set<Principal> principals = cs.getAuthInfo().getPrincipals();
+            assertTrue(principals.contains(setup2.gr.getPrincipal()));
+
+            assertFalse(principals.contains(new PrincipalImpl(NON_EXISTING_NAME)));
+            assertFalse(principals.contains(setup1.gr.getPrincipal()));
+            assertFalse(principals.contains(setup3.gr.getPrincipal()));
+
+            // however, the existing auto-membership group must _not_ have changed
+            // and the test user must not be a stored member of this group.
+            root.refresh();
+            UserManager uMgr = getUserManager(root);
+
+            User user = uMgr.getAuthorizable(USER_ID, User.class);
+            Group gr = uMgr.getAuthorizable(setup2.gr.getID(), Group.class);
+
+            assertFalse(gr.isDeclaredMember(user));
+            assertFalse(gr.isMember(user));
+        } finally {
+            options.clear();
+            if (cs != null) {
+                cs.close();
+            }
+        }
+    }
+
+    @Test
+    public void testLoginAfterSyncSetup3() throws Exception {
+        setup3.sync(USER_ID, false);
+
+        ContentSession cs = null;
+        try {
+            cs = login(new SimpleCredentials(USER_ID, new char[0]));
+
+            // the login must set the existing auto-membership principals to the subject
+            Set<Principal> principals = cs.getAuthInfo().getPrincipals();
+            assertTrue(principals.contains(setup3.gr.getPrincipal()));
+
+            assertFalse(principals.contains(new PrincipalImpl(NON_EXISTING_NAME)));
+            assertFalse(principals.contains(setup1.gr.getPrincipal()));
+            assertFalse(principals.contains(setup2.gr.getPrincipal()));
+
+            // however, the existing auto-membership group must _not_ have changed
+            // and the test user must not be a stored member of this group.
+            root.refresh();
+            UserManager uMgr = getUserManager(root);
+
+            User user = uMgr.getAuthorizable(USER_ID, User.class);
+            Group gr = uMgr.getAuthorizable(setup3.gr.getID(), Group.class);
+
+            assertTrue(gr.isDeclaredMember(user));
+            assertTrue(gr.isMember(user));
+        } finally {
+            options.clear();
+            if (cs != null) {
+                cs.close();
+            }
+        }
+    }
+
+    @Test
+    public void testLoginAfterSyncSetup4() throws Exception {
+        setup4.sync(USER_ID, false);
+
+        ContentSession cs = null;
+        try {
+            cs = login(new SimpleCredentials(USER_ID, new char[0]));
+
+            // the login must set the existing auto-membership principals to the subject
+            Set<Principal> principals = cs.getAuthInfo().getPrincipals();
+            assertTrue(principals.contains(setup4.gr.getPrincipal()));
+            assertTrue(principals.contains(setup2.gr.getPrincipal()));
+
+            assertFalse(principals.contains(new PrincipalImpl(NON_EXISTING_NAME)));
+            assertFalse(principals.contains(setup1.gr.getPrincipal()));
+            assertFalse(principals.contains(setup3.gr.getPrincipal()));
+
+            // however, the existing auto-membership group must _not_ have changed
+            // and the test user must not be a stored member of this group.
+            root.refresh();
+            UserManager uMgr = getUserManager(root);
+
+            User user = uMgr.getAuthorizable(USER_ID, User.class);
+            Group gr = uMgr.getAuthorizable(setup4.gr.getID(), Group.class);
+
+            assertFalse(gr.isDeclaredMember(user));
+            assertFalse(gr.isMember(user));
+        } finally {
+            options.clear();
+            if (cs != null) {
+                cs.close();
+            }
+        }
+    }
+
+    @Test
+    public void testLoginAfterSyncSetup5() throws Exception {
+        setup5.sync(USER_ID, false);
+
+        ContentSession cs = null;
+        try {
+            cs = login(new SimpleCredentials(USER_ID, new char[0]));
+
+            // the login must not set any auto-membership principals to the subject
+            // as auto-membership is not configured on this setup.
+            Set<Principal> principals = cs.getAuthInfo().getPrincipals();
+
+            Set<Principal> expected = ImmutableSet.of(EveryonePrincipal.getInstance(), userManager.getAuthorizable(USER_ID).getPrincipal());
+            assertEquals(expected, principals);
+
+            assertFalse(principals.contains(new PrincipalImpl(NON_EXISTING_NAME)));
+            assertFalse(principals.contains(setup1.gr.getPrincipal()));
+            assertFalse(principals.contains(setup2.gr.getPrincipal()));
+            assertFalse(principals.contains(setup3.gr.getPrincipal()));
+            assertFalse(principals.contains(setup4.gr.getPrincipal()));
+        } finally {
+            options.clear();
+            if (cs != null) {
+                cs.close();
+            }
+        }
+    }
+
+    private final class ExternalSetup {
+
+        private final ExternalIdentityProvider idp;
+        private final Registration idpRegistration;
+
+        private final DefaultSyncConfig sc;
+        private final SyncHandler sh;
+        private final Registration shRegistration;
+
+        private final Group gr;
+
+        private SyncContext ctx;
+
+        private ExternalSetup(@Nonnull ExternalIdentityProvider idp, @Nonnull DefaultSyncConfig sc) throws Exception {
+            this(idp, sc, new DefaultSyncHandler(sc), "gr_" + sc.getName());
+        }
+
+        private ExternalSetup(@Nonnull ExternalIdentityProvider idp, @Nonnull DefaultSyncConfig sc, @Nonnull SyncHandler sh, @CheckForNull String groupId) throws Exception {
+            this.idp = idp;
+            this.sc = sc;
+            this.sh = sh;
+
+            if (groupId != null) {
+                Group g = userManager.getAuthorizable(groupId, Group.class);
+                if (g != null) {
+                    gr = g;
+                } else {
+                    gr = userManager.createGroup(groupId);
+                }
+                r.commit();
+
+                sc.user().setAutoMembership(gr.getID(), NON_EXISTING_NAME).setExpirationTime(Long.MAX_VALUE);
+            } else {
+                gr = null;
+            }
+
+            idpRegistration = whiteboard.register(ExternalIdentityProvider.class, idp, Collections.<String, Object>emptyMap());
+            shRegistration = whiteboard.register(SyncHandler.class, sh, ImmutableMap.of(
+                            DefaultSyncConfigImpl.PARAM_NAME, sh.getName(),
+                            DefaultSyncConfigImpl.PARAM_USER_DYNAMIC_MEMBERSHIP, sc.user().getDynamicMembership(),
+                            DefaultSyncConfigImpl.PARAM_GROUP_AUTO_MEMBERSHIP, sc.user().getAutoMembership()));
+            registerSyncHandlerMapping(context, this);
+        }
+
+        private void sync(@Nonnull String id, boolean isGroup) throws Exception {
+            ctx = sh.createContext(idp, userManager, valueFactory);
+            ExternalIdentity exIdentity = (isGroup) ? idp.getGroup(id) : idp.getUser(id);
+            assertNotNull(exIdentity);
+
+            SyncResult res = ctx.sync(exIdentity);
+            assertEquals(idp.getName(), res.getIdentity().getExternalIdRef().getProviderName());
+            assertSame(SyncResult.Status.ADD, res.getStatus());
+            r.commit();
+        }
+
+        private void close() {
+            if (ctx != null) {
+                ctx.close();
+            }
+            if (idpRegistration != null) {
+                idpRegistration.unregister();
+            }
+            if (shRegistration != null) {
+                shRegistration.unregister();
+            }
+        }
+
+        private AppConfigurationEntry asConfigurationEntry() {
+            return new AppConfigurationEntry(
+                    ExternalLoginModule.class.getName(),
+                    AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT,
+                    ImmutableMap.<String, String>of(
+                            SyncHandlerMapping.PARAM_SYNC_HANDLER_NAME, sh.getName(),
+                            SyncHandlerMapping.PARAM_IDP_NAME, idp.getName()
+                    ));
+        }
+    }
+}
\ No newline at end of file
Index: oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalGroupPrincipalProviderTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalGroupPrincipalProviderTest.java	(revision 1745339)
+++ oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalGroupPrincipalProviderTest.java	(revision )
@@ -54,7 +54,7 @@
 
 public class ExternalGroupPrincipalProviderTest extends AbstractPrincipalTest {
 
-    private void syncWithMembership(@Nonnull ExternalUser externalUser, long depth) throws Exception {
+    void syncWithMembership(@Nonnull ExternalUser externalUser, long depth) throws Exception {
         DefaultSyncConfig sc = new DefaultSyncConfig();
         sc.user().setMembershipNestingDepth(depth);
 
@@ -69,7 +69,11 @@
         root.refresh();
     }
 
-    private Set<Principal> getDeclaredGroupPrincipals(@Nonnull String userId) throws ExternalIdentityException {
+    Set<Principal> getExpectedGroupPrincipals(@Nonnull String userId) throws Exception {
+        return getDeclaredGroupPrincipals(userId);
+    }
+
+    Set<Principal> getDeclaredGroupPrincipals(@Nonnull String userId) throws Exception {
         Set<Principal> principals = ImmutableSet.copyOf(Iterables.transform(idp.getUser(userId).getDeclaredGroups(), new Function<ExternalIdentityRef, Principal>() {
             @Nullable
             @Override
@@ -77,16 +81,24 @@
                 try {
                     return new PrincipalImpl(idp.getIdentity(input).getPrincipalName());
                 } catch (ExternalIdentityException e) {
-                    fail(e.getMessage());
-                    return null;
+                    throw new RuntimeException(e);
                 }
-            }
-
-            ;
+            };
         }));
         return principals;
     }
 
+    void collectExpectedPrincipals(Set<Principal> grPrincipals, @Nonnull Iterable<ExternalIdentityRef> declaredGroups, long depth) throws Exception {
+        if (depth <= 0) {
+            return;
+        }
+        for (ExternalIdentityRef ref : declaredGroups) {
+            ExternalIdentity ei = idp.getIdentity(ref);
+            grPrincipals.add(new PrincipalImpl(ei.getPrincipalName()));
+            collectExpectedPrincipals(grPrincipals, ei.getDeclaredGroups(), depth - 1);
+        }
+    }
+
     @Test
     public void testGetPrincipalLocalUser() throws Exception {
         assertNull(principalProvider.getPrincipal(getTestUser().getPrincipal().getName()));
@@ -234,7 +246,7 @@
         Authorizable user = getUserManager(root).getAuthorizable(USER_ID);
         assertNotNull(user);
 
-        Set<Principal> expected = getDeclaredGroupPrincipals(USER_ID);
+        Set<Principal> expected = getExpectedGroupPrincipals(USER_ID);
 
         Set<? extends Principal> principals = principalProvider.getGroupMembership(user.getPrincipal());
         assertEquals(expected, principals);
@@ -275,17 +287,6 @@
         assertEquals(expectedGrPrincipals, principals);
     }
 
-    private void collectExpectedPrincipals(Set<Principal> grPrincipals, @Nonnull Iterable<ExternalIdentityRef> declaredGroups, long depth) throws ExternalIdentityException {
-        if (depth <= 0) {
-            return;
-        }
-        for (ExternalIdentityRef ref : declaredGroups) {
-            ExternalIdentity ei = idp.getIdentity(ref);
-            grPrincipals.add(new PrincipalImpl(ei.getPrincipalName()));
-            collectExpectedPrincipals(grPrincipals, ei.getDeclaredGroups(), depth - 1);
-        }
-    }
-
     @Test
     public void testGetGroupMembershipExternalGroup() throws Exception {
         Authorizable group = getUserManager(root).getAuthorizable("secondGroup");
@@ -314,7 +315,7 @@
     @Test
     public void testGetPrincipalsExternalUser() throws Exception {
         Set<? extends Principal> principals = principalProvider.getPrincipals(USER_ID);
-        assertEquals(getDeclaredGroupPrincipals(USER_ID), principals);
+        assertEquals(getExpectedGroupPrincipals(USER_ID), principals);
     }
 
     @Test
\ No newline at end of file
Index: oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AbstractPrincipalTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AbstractPrincipalTest.java	(revision 1745339)
+++ oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AbstractPrincipalTest.java	(revision )
@@ -17,9 +17,11 @@
 package org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal;
 
 import java.security.Principal;
+import java.util.Set;
 import java.util.UUID;
 import javax.annotation.Nonnull;
 
+import com.google.common.collect.ImmutableMap;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
@@ -63,7 +65,9 @@
 
     @Nonnull
     PrincipalProvider createPrincipalProvider() {
-        return new ExternalGroupPrincipalProvider(root, getSecurityProvider().getConfiguration(UserConfiguration.class), NamePathMapper.DEFAULT);
+        Set<String> autoMembership = syncConfig.user().getAutoMembership();
+        return new ExternalGroupPrincipalProvider(root, getSecurityProvider().getConfiguration(UserConfiguration.class),
+                NamePathMapper.DEFAULT, ImmutableMap.of(idp.getName(), autoMembership.toArray(new String[autoMembership.size()])));
     }
 
     @Override
\ No newline at end of file
Index: oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java	(revision 1745339)
+++ oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java	(revision )
@@ -37,10 +37,19 @@
 
     public static final String ID_EXCEPTION = "throw!";
 
+    public static final String DEFAULT_IDP_NAME = "test";
+
     private final Map<String, ExternalGroup> externalGroups = new HashMap<String, ExternalGroup>();
     private final Map<String, ExternalUser> externalUsers = new HashMap<String, ExternalUser>();
 
+    private final String idpName;
+
     public TestIdentityProvider() {
+        this(DEFAULT_IDP_NAME);
+    }
+    public TestIdentityProvider(@Nonnull String idpName) {
+        this.idpName = idpName;
+
         addGroup(new TestGroup("aa", getName()));
         addGroup(new TestGroup("aaa", getName()));
         addGroup(new TestGroup("a", getName()).withGroups("aa", "aaa"));
@@ -82,7 +91,7 @@
     @Nonnull
     @Override
     public String getName() {
-        return "test";
+        return idpName;
     }
 
     @Override
@@ -151,11 +160,11 @@
         private final Map<String, Object> props = new HashMap<String, Object>();
 
         public TestIdentity() {
-            this("externalId", "principalName", "test");
+            this("externalId", "principalName", DEFAULT_IDP_NAME);
         }
 
         public TestIdentity(@Nonnull String userId) {
-            this(userId, userId, "test");
+            this(userId, userId, DEFAULT_IDP_NAME);
         }
 
         public TestIdentity(@Nonnull String userId, @Nonnull String principalName, @Nonnull String idpName) {
\ No newline at end of file
Index: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalPrincipalConfiguration.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalPrincipalConfiguration.java	(revision 1745339)
+++ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalPrincipalConfiguration.java	(revision )
@@ -18,6 +18,8 @@
 
 import java.security.Principal;
 import java.security.acl.Group;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -26,8 +28,11 @@
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -46,6 +51,8 @@
 import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncConfigImpl;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalLoginModuleFactory;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.SyncHandlerMapping;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalConfiguration;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalManagerImpl;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider;
@@ -54,6 +61,8 @@
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceReference;
 import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Implementation of the {@code PrincipalConfiguration} interface that provides
@@ -73,7 +82,10 @@
 @Service({PrincipalConfiguration.class, SecurityConfiguration.class})
 public class ExternalPrincipalConfiguration extends ConfigurationBase implements PrincipalConfiguration {
 
+    private static final Logger log = LoggerFactory.getLogger(ExternalPrincipalConfiguration.class);
+
     private SyncConfigTracker syncConfigTracker;
+    private SyncHandlerMappingTracker syncHandlerMappingTracker;
 
     @SuppressWarnings("UnusedDeclaration")
     public ExternalPrincipalConfiguration() {
@@ -96,7 +108,7 @@
     public PrincipalProvider getPrincipalProvider(Root root, NamePathMapper namePathMapper) {
         if (dynamicMembershipEnabled()) {
             UserConfiguration uc = getSecurityProvider().getConfiguration(UserConfiguration.class);
-            return new ExternalGroupPrincipalProvider(root, uc, namePathMapper);
+            return new ExternalGroupPrincipalProvider(root, uc, namePathMapper, syncConfigTracker.getAutoMembership());
         } else {
             return EmptyPrincipalProvider.INSTANCE;
         }
@@ -132,8 +144,13 @@
     @Activate
     private void activate(BundleContext bundleContext, Map<String, Object> properties) {
         setParameters(ConfigurationParameters.of(properties));
-        syncConfigTracker = new SyncConfigTracker(bundleContext);
+        syncHandlerMappingTracker = new SyncHandlerMappingTracker(bundleContext);
+        syncHandlerMappingTracker.open();
+
+        syncConfigTracker = new SyncConfigTracker(bundleContext, syncHandlerMappingTracker);
         syncConfigTracker.open();
+
+
     }
 
     @SuppressWarnings("UnusedDeclaration")
@@ -142,7 +159,10 @@
         if (syncConfigTracker != null) {
             syncConfigTracker.close();
         }
+        if (syncHandlerMappingTracker != null) {
+            syncHandlerMappingTracker.close();
-    }
+        }
+    }
 
     //------------------------------------------------------------< private >---
 
@@ -196,11 +216,14 @@
      */
     private static final class SyncConfigTracker extends ServiceTracker {
 
+        private final SyncHandlerMappingTracker mappingTracker;
+
         private Set<ServiceReference> enablingRefs = new HashSet<ServiceReference>();
         private boolean isEnabled = false;
 
-        public SyncConfigTracker(BundleContext context) {
+        public SyncConfigTracker(@Nonnull BundleContext context, @Nonnull SyncHandlerMappingTracker mappingTracker) {
             super(context, SyncHandler.class.getName(), null);
+            this.mappingTracker = mappingTracker;
         }
 
         @Override
@@ -233,6 +256,84 @@
 
         private static boolean hasDynamicMembership(ServiceReference reference) {
             return PropertiesUtil.toBoolean(reference.getProperty(DefaultSyncConfigImpl.PARAM_USER_DYNAMIC_MEMBERSHIP), DefaultSyncConfigImpl.PARAM_USER_DYNAMIC_MEMBERSHIP_DEFAULT);
+        }
+
+        private Map<String, String[]> getAutoMembership() {
+            Map<String, String[]> autoMembership = new HashMap<String, String[]>();
+            for (ServiceReference ref : enablingRefs) {
+                String syncHandlerName = PropertiesUtil.toString(ref.getProperty(DefaultSyncConfigImpl.PARAM_NAME), DefaultSyncConfigImpl.PARAM_NAME_DEFAULT);
+                String[] membership = PropertiesUtil.toStringArray(ref.getProperty(DefaultSyncConfigImpl.PARAM_GROUP_AUTO_MEMBERSHIP), new String[0]);
+
+                for (String idpName : mappingTracker.getIdpNames(syncHandlerName)) {
+                    String[] previous = autoMembership.put(idpName, membership);
+                    if (previous != null) {
+                        String msg = (Arrays.equals(previous, membership)) ? "Duplicate" : "Colliding";
+                        log.debug(msg + " auto-membership configuration for IDP '{}'; replacing previous values {} by {} defined by SyncHandler '{}'",
+                                idpName, Arrays.toString(previous), Arrays.toString(membership), syncHandlerName);
+                    }
+                }
+            }
+            return autoMembership;
+        }
+    }
+
+    /**
+     * {@code ServiceTracker} to detect any {@link SyncHandler} that has
+     * dynamic membership enabled.
+     */
+    private static final class SyncHandlerMappingTracker extends ServiceTracker {
+
+        private Map<ServiceReference, String[]> referenceMap = new HashMap<ServiceReference, String[]>();
+
+        public SyncHandlerMappingTracker(@Nonnull BundleContext context) {
+            super(context, SyncHandlerMapping.class.getName(), null);
+        }
+
+        @Override
+        public Object addingService(ServiceReference reference) {
+            addMapping(reference);
+            return super.addingService(reference);
+        }
+
+        @Override
+        public void modifiedService(ServiceReference reference, Object service) {
+            addMapping(reference);
+            super.modifiedService(reference, service);
+        }
+
+        @Override
+        public void removedService(ServiceReference reference, Object service) {
+            referenceMap.remove(reference);
+            super.removedService(reference, service);
+        }
+
+        private void addMapping(ServiceReference reference) {
+            String idpName = PropertiesUtil.toString(reference.getProperty(ExternalLoginModuleFactory.PARAM_IDP_NAME), null);
+            String syncHandlerName = PropertiesUtil.toString(reference.getProperty(ExternalLoginModuleFactory.PARAM_SYNC_HANDLER_NAME), null);
+
+            if (idpName != null && syncHandlerName != null) {
+                referenceMap.put(reference, new String[]{syncHandlerName, idpName});
+            } else {
+                log.warn("Ignoring SyncHandlerMapping with incomplete mapping of IDP '{}' and SyncHandler '{}'", idpName, syncHandlerName);
+            }
+        }
+
+        private Iterable<String> getIdpNames(@Nonnull final String syncHandlerName) {
+            return Iterables.filter(Iterables.transform(referenceMap.values(), new Function<String[], String>() {
+                        @Nullable
+                        @Override
+                        public String apply(@Nullable String[] input) {
+                            if (input != null && input.length == 2) {
+                                if (syncHandlerName.equals(input[0])) {
+                                    return input[1];
+                                } // else: different sync-handler
+                            } else {
+                                log.warn("Unexpected value of reference map. Expected String[] with length = 2");
+                            }
+                            return null;
+                        }
+                    }
+            ), Predicates.notNull());
         }
     }
 }
\ No newline at end of file
Index: oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTestBase.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTestBase.java	(revision 1745339)
+++ oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTestBase.java	(revision )
@@ -57,10 +57,10 @@
 
         testIdpReg = whiteboard.register(ExternalIdentityProvider.class, idp, Collections.<String, Object>emptyMap());
 
-        options.put(ExternalLoginModule.PARAM_SYNC_HANDLER_NAME, "default");
-        options.put(ExternalLoginModule.PARAM_IDP_NAME, idp.getName());
-
         setSyncConfig(syncConfig);
+
+        options.put(ExternalLoginModule.PARAM_SYNC_HANDLER_NAME, syncConfig.getName());
+        options.put(ExternalLoginModule.PARAM_IDP_NAME, idp.getName());
     }
 
     @After
\ No newline at end of file
Index: oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/PrincipalProviderAutoMembershipTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/PrincipalProviderAutoMembershipTest.java	(revision )
+++ oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/PrincipalProviderAutoMembershipTest.java	(revision )
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal;
+
+import java.security.Principal;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+import org.apache.jackrabbit.api.security.principal.PrincipalManager;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncConfig;
+import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Extension of the {@link ExternalGroupPrincipalProviderTest} with 'automembership'
+ * configured in the {@link DefaultSyncConfig}.
+ */
+public class PrincipalProviderAutoMembershipTest extends ExternalGroupPrincipalProviderTest {
+
+    private static final String AUTO_MEMBERSHIP_GROUP_ID = "testGroup" + UUID.randomUUID();
+    private static final String AUTO_MEMBERSHIP_GROUP_PRINCIPAL_NAME = "p" + AUTO_MEMBERSHIP_GROUP_ID;
+    private static final String NON_EXISTING_GROUP_ID = "nonExistingGroup";
+
+    private Group autoMembershipGroup;
+
+    @Override
+    public void before() throws Exception {
+        super.before();
+
+        autoMembershipGroup = getUserManager(root).createGroup(AUTO_MEMBERSHIP_GROUP_ID, new PrincipalImpl(AUTO_MEMBERSHIP_GROUP_PRINCIPAL_NAME), null);
+        root.commit();
+    }
+
+    @Override
+    protected DefaultSyncConfig createSyncConfig() {
+        DefaultSyncConfig syncConfig = super.createSyncConfig();
+        syncConfig.user().setAutoMembership(AUTO_MEMBERSHIP_GROUP_ID, NON_EXISTING_GROUP_ID);
+
+        return syncConfig;
+    }
+
+    @Override
+    Set<Principal> getExpectedGroupPrincipals(@Nonnull String userId) throws Exception {
+        return ImmutableSet.<Principal>builder()
+                .addAll(super.getExpectedGroupPrincipals(userId))
+                .add(autoMembershipGroup.getPrincipal()).build();
+    }
+
+    @Override
+    void collectExpectedPrincipals(Set<Principal> grPrincipals, @Nonnull Iterable<ExternalIdentityRef> declaredGroups, long depth) throws Exception {
+        super.collectExpectedPrincipals(grPrincipals, declaredGroups, depth);
+        grPrincipals.add(autoMembershipGroup.getPrincipal());
+    }
+
+    @Test
+    public void testGetAutoMembershipPrincipal() throws Exception {
+        assertNull(principalProvider.getPrincipal(autoMembershipGroup.getPrincipal().getName()));
+        assertNull(principalProvider.getPrincipal(AUTO_MEMBERSHIP_GROUP_PRINCIPAL_NAME));
+        assertNull(principalProvider.getPrincipal(AUTO_MEMBERSHIP_GROUP_ID));
+        assertNull(principalProvider.getPrincipal(NON_EXISTING_GROUP_ID));
+    }
+
+    @Test
+    public void testGetGroupPrincipals() throws Exception {
+        ExternalUser externalUser = idp.getUser(USER_ID);
+        syncWithMembership(externalUser, 1);
+
+        Set<Principal> expected = getExpectedGroupPrincipals(USER_ID);
+
+        Authorizable user = getUserManager(root).getAuthorizable(USER_ID);
+
+        Set<java.security.acl.Group> result = principalProvider.getGroupMembership(user.getPrincipal());
+        assertTrue(result.contains(autoMembershipGroup.getPrincipal()));
+        assertEquals(expected, result);
+    }
+
+    @Test
+    public void testGetPrincipals() throws Exception {
+        ExternalUser externalUser = idp.getUser(USER_ID);
+        syncWithMembership(externalUser, 1);
+
+        Set<Principal> expected = getExpectedGroupPrincipals(USER_ID);
+
+        Set<? extends Principal> result = principalProvider.getPrincipals(USER_ID);
+        assertTrue(result.contains(autoMembershipGroup.getPrincipal()));
+        assertEquals(expected, result);
+    }
+
+    @Test
+    public void testFindPrincipalsByHint() throws Exception {
+        List<String> hints = ImmutableList.of(
+                AUTO_MEMBERSHIP_GROUP_PRINCIPAL_NAME,
+                AUTO_MEMBERSHIP_GROUP_ID,
+                AUTO_MEMBERSHIP_GROUP_PRINCIPAL_NAME.substring(1, 6));
+
+        for (String hint : hints) {
+            Iterator<? extends Principal> res = principalProvider.findPrincipals(hint, PrincipalManager.SEARCH_TYPE_GROUP);
+
+            assertFalse(Iterators.contains(res, autoMembershipGroup.getPrincipal()));
+            assertFalse(Iterators.contains(res, new PrincipalImpl(NON_EXISTING_GROUP_ID)));
+        }
+    }
+
+    @Test
+    public void testFindPrincipalsByTypeGroup() throws Exception {
+        Iterator<? extends Principal> res = principalProvider.findPrincipals(PrincipalManager.SEARCH_TYPE_GROUP);
+
+        assertFalse(Iterators.contains(res, autoMembershipGroup.getPrincipal()));
+        assertFalse(Iterators.contains(res, new PrincipalImpl(NON_EXISTING_GROUP_ID)));
+    }
+}
\ No newline at end of file
Index: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java	(revision 1745339)
+++ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java	(revision )
@@ -70,12 +70,12 @@
     /**
      * Name of the parameter that configures the name of the external identity provider.
      */
-    public static final String PARAM_IDP_NAME = "idp.name";
+    public static final String PARAM_IDP_NAME = SyncHandlerMapping.PARAM_IDP_NAME;
 
     /**
      * Name of the parameter that configures the name of the synchronization handler.
      */
-    public static final String PARAM_SYNC_HANDLER_NAME = "sync.handlerName";
+    public static final String PARAM_SYNC_HANDLER_NAME = SyncHandlerMapping.PARAM_SYNC_HANDLER_NAME;
 
     private ExternalIdentityProviderManager idpManager;
 
\ No newline at end of file
