OAK-11984 Support UserId Change for External Users (#2581)

* OAK-11984 Support UserId Change for External Users

* Removed unused change

* Update oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java

Co-authored-by: Alejandro Moratinos <Amoratinos@users.noreply.github.com>

* Update oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java

Co-authored-by: Alejandro Moratinos <Amoratinos@users.noreply.github.com>

* moving constants

* Added FF

* Added tests for FF

* Added debug log

---------

Co-authored-by: Alejandro Moratinos <Amoratinos@users.noreply.github.com>
Co-authored-by: angela <anchela@adobe.com>
diff --git a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalIdentityConstants.java b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalIdentityConstants.java
index 4ce1746..89257f3 100644
--- a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalIdentityConstants.java
+++ b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalIdentityConstants.java
@@ -36,7 +36,12 @@
      * @see DefaultSyncContext#REP_EXTERNAL_ID
      */
     String REP_EXTERNAL_ID = DefaultSyncContext.REP_EXTERNAL_ID;
-
+    
+    /**
+     * Name of the attribute storing the external identifier in Credentials
+     */
+    String EXTERNAL_ID_ATTRIBUTE = ":externalId";
+    
     /**
      * Name of the property storing the date of the last synchronization of an
      * external identity.
diff --git a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
index ad9d9b7..447cb45 100644
--- a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
+++ b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
@@ -16,6 +16,7 @@
  */
 package org.apache.jackrabbit.oak.spi.security.authentication.external.impl;
 
+import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.oak.api.AuthInfo;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
@@ -58,6 +59,7 @@
 import javax.security.auth.login.LoginException;
 import java.security.Principal;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -65,6 +67,7 @@
 import java.util.stream.Stream;
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static org.apache.jackrabbit.oak.spi.security.authentication.AuthenticationConstants.SHARED_ATTRIBUTE_EXTERNAL_ID;
 
 /**
  * {@code ExternalLoginModule} implements a {@code LoginModule} that uses an
@@ -228,7 +231,25 @@
             // before into the repository.
             UserManager userManager = getUserManager();
             SyncedIdentity sId = getSyncedIdentity(userId, userManager);
-
+            if (Boolean.parseBoolean(System.getProperty("FT_GRANITE-61684")) && 
+                    sId ==null && userManager != null && creds != null) {
+                log.debug("FT_GRANITE-61684 is enabled and user is not found by userId. Trying to find external user by externalId attribute.");
+                // Check if the external user was registered with a different userId, and the same externalId
+                Object externalAttribute = credentialsSupport.getAttributes(creds).get(SHARED_ATTRIBUTE_EXTERNAL_ID);
+                if (externalAttribute != null ) {
+                    @NotNull Iterator<Authorizable> authIterator = userManager.findAuthorizables(ExternalIdentityConstants.REP_EXTERNAL_ID, externalAttribute + ";" + idp.getName(), UserManager.SEARCH_TYPE_USER);
+                    if (authIterator.hasNext()) {
+                        log.debug("Found existing user by externalId attribute: {}", externalAttribute);
+                        //modify credentials to reflect the login name stored in oak
+                        Authorizable authorizable = authIterator.next();
+                        sId = getSyncedIdentity(authorizable.getID(), userManager);
+                        Map<String, ?> attributes = credentialsSupport.getAttributes(creds);
+                        HashMap<String, Object> newAttributes = new HashMap<>(attributes);
+                        newAttributes.put(SHARED_KEY_LOGIN_NAME, authorizable.getID());
+                        credentialsSupport.setAttributes(creds, newAttributes);
+                    }
+                }
+            }
             // if there exists an authorizable with the given userid (syncedIdentity != null),
             // ignore it if any of the following conditions is met:
             // - identity is local (i.e. not an external identity)
diff --git a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/package-info.java b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/package-info.java
index 112ef6a..ed2f9b3 100644
--- a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/package-info.java
+++ b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/package-info.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@Version("2.4.0")
+@Version("2.5.0")
 package org.apache.jackrabbit.oak.spi.security.authentication.external;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleTest.java b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleTest.java
index 851bb7d..b6f3f81 100644
--- a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleTest.java
+++ b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleTest.java
@@ -25,6 +25,8 @@
 import org.apache.jackrabbit.oak.api.ContentRepository;
 import org.apache.jackrabbit.oak.api.ContentSession;
 import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
 import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
 import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials;
@@ -69,9 +71,11 @@
 import java.util.Map;
 import java.util.Set;
 
+import static java.util.Map.of;
 import static org.apache.jackrabbit.oak.api.CommitFailedException.OAK;
 import static org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule.SHARED_KEY_ATTRIBUTES;
 import static org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule.SHARED_KEY_PRE_AUTH_LOGIN;
+import static org.apache.jackrabbit.oak.spi.security.authentication.AuthenticationConstants.SHARED_ATTRIBUTE_EXTERNAL_ID;
 import static org.apache.jackrabbit.oak.spi.security.authentication.external.TestIdentityProvider.DEFAULT_IDP_NAME;
 import static org.apache.jackrabbit.oak.spi.security.authentication.external.TestIdentityProvider.ID_EXCEPTION;
 import static org.apache.jackrabbit.oak.spi.security.authentication.external.TestIdentityProvider.ID_TEST_USER;
@@ -253,6 +257,173 @@
         verifyNoInteractions(monitor);
     }
 
+    private void createExternalIdIndex(Tree rootTree) {
+        // Navigate to or create the /oak:index node
+        Tree index = rootTree.getChild("oak:index");
+        if (!index.exists()) {
+            index = rootTree.addChild("oak:index");
+            index.setProperty("jcr:primaryType", "oak:Unstructured", Type.NAME);
+        }
+
+        Tree externalIdIndex = index.getChild("externalId");
+        if (!externalIdIndex.exists()) {
+            externalIdIndex = index.addChild("externalId");
+            externalIdIndex.setProperty("jcr:primaryType", "oak:QueryIndexDefinition", Type.NAME); // Correct node type
+            externalIdIndex.setProperty("type", "property", Type.STRING);
+            externalIdIndex.setProperty("propertyNames", Collections.singletonList("rep:externalId"), Type.NAMES);
+            externalIdIndex.setProperty("reindex", true, Type.BOOLEAN);
+            externalIdIndex.setProperty("unique", true, Type.BOOLEAN);
+        }
+
+    }
+    @Test
+    public void testLoginUserIdentifiedByExternalId() throws Exception {
+        System.setProperty("FT_GRANITE-61684", "true");
+
+        String idpName = "testExternalId";
+        // UserId (PrincipalName) already present in oak (old UserId for the user)
+        String oldPrincipalName = "test3OldValue";
+        // ExternalId present in oak and returned by the Idp, and already configured in Oak
+        String externalId = "extId123";
+        // New UserId returned by the ExternalIdp
+        String newPrincipalName = "newUserId";
+
+        TestExternalUserIdCredentials creds = new TestExternalUserIdCredentials(newPrincipalName);
+        creds.setAttribute(SHARED_ATTRIBUTE_EXTERNAL_ID, externalId);
+
+        // We need to create an index, or we have an exception with the search       
+        createExternalIdIndex(root.getTree("/"));
+        UserManager userManager = getUserManager(root);
+        userManager.createUser(oldPrincipalName, null).setProperty(REP_EXTERNAL_ID, getValueFactory().createValue(externalId + ";" + idpName));
+        root.commit();
+
+        ExternalIdentityProvider idp = new TestExternalUserIdIdentityProvider(idpName);
+
+        when(extIPMgr.getProvider(DEFAULT_IDP_NAME)).thenReturn(idp);
+        when(syncManager.getSyncHandler("syncHandler")).thenReturn(new DefaultSyncHandler(new DefaultSyncConfigImpl().setName("syncHandler")));
+
+        wb.register(ExternalIdentityProviderManager.class, extIPMgr, Collections.emptyMap());
+        wb.register(SyncManager.class, syncManager, Collections.emptyMap());
+
+        CallbackHandler cbh = createCallbackHandler(wb, getContentRepository(), getSecurityProvider(), creds);
+
+        Map<String, Object> sharedState = new HashMap<>();
+
+        loginModule.initialize(new Subject(), cbh, sharedState, of(PARAM_IDP_NAME, DEFAULT_IDP_NAME, PARAM_SYNC_HANDLER_NAME, "syncHandler"));
+        assertTrue(loginModule.login());
+        assertTrue(loginModule.commit());
+        // The original PrincipalId is used, even if the Idp initially sent a new UderId.
+        assertEquals(creds.getUserId(), oldPrincipalName);
+        assertTrue(loginModule.logout());
+    }
+
+    @Test(expected = LoginException.class)
+    public void testLoginUserIdentifiedByExternalIdIssue() throws Exception {
+        System.setProperty("FT_GRANITE-61684", "false");
+
+        String idpName = "testExternalId";
+        // UserId (PrincipalName) already present in oak (old UserId for the user)
+        String oldPrincipalName = "test3OldValue";
+        // ExternalId present in oak and returned by the Idp, and already configured in Oak
+        String externalId = "extId123";
+        // New UserId returned by the ExternalIdp
+        String newPrincipalName = "newUserId";
+
+        TestExternalUserIdCredentials creds = new TestExternalUserIdCredentials(newPrincipalName);
+        creds.setAttribute(SHARED_ATTRIBUTE_EXTERNAL_ID, externalId);
+
+        // We need to create an index, or we have an exception with the search       
+        createExternalIdIndex(root.getTree("/"));
+        UserManager userManager = getUserManager(root);
+        userManager.createUser(oldPrincipalName, null).setProperty(REP_EXTERNAL_ID, getValueFactory().createValue(externalId + ";" + idpName));
+        root.commit();
+
+        ExternalIdentityProvider idp = new TestExternalUserIdIdentityProvider(idpName);
+
+        when(extIPMgr.getProvider(DEFAULT_IDP_NAME)).thenReturn(idp);
+        when(syncManager.getSyncHandler("syncHandler")).thenReturn(new DefaultSyncHandler(new DefaultSyncConfigImpl().setName("syncHandler")));
+
+        wb.register(ExternalIdentityProviderManager.class, extIPMgr, Collections.emptyMap());
+        wb.register(SyncManager.class, syncManager, Collections.emptyMap());
+
+        CallbackHandler cbh = createCallbackHandler(wb, getContentRepository(), getSecurityProvider(), creds);
+
+        Map<String, Object> sharedState = new HashMap<>();
+
+        loginModule.initialize(new Subject(), cbh, sharedState, of(PARAM_IDP_NAME, DEFAULT_IDP_NAME, PARAM_SYNC_HANDLER_NAME, "syncHandler"));
+        assertTrue(loginModule.login());
+        assertTrue(loginModule.commit());
+        // The original PrincipalId is used, even if the Idp initially sent a new UderId.
+        assertEquals(creds.getUserId(), oldPrincipalName);
+        assertTrue(loginModule.logout());
+    }
+
+    // Test if the user is not found by the externalId, even if it is present in oak.
+    // This is the default case for users that did not modify his userId
+    @Test
+    public void testLoginUserIdentifiedByExternalIdNotFound() throws Exception {
+        System.setProperty("FT_GRANITE-61684", "true");
+
+        String idpName = "testExternalId";
+        // UserId (PrincipalName) already present in oak (UserId for the user)
+        String principalName =  "test4";
+
+        // We need to create an index, or we have an exception with the search       
+        createExternalIdIndex(root.getTree("/"));
+        root.commit();
+        
+        TestExternalUserIdCredentials creds = new TestExternalUserIdCredentials(principalName);
+        creds.setAttribute(SHARED_ATTRIBUTE_EXTERNAL_ID, principalName);
+
+        ExternalIdentityProvider idp = new TestExternalUserIdIdentityProvider(idpName);
+
+        when(extIPMgr.getProvider(DEFAULT_IDP_NAME)).thenReturn(idp);
+        when(syncManager.getSyncHandler("syncHandler")).thenReturn(new DefaultSyncHandler(new DefaultSyncConfigImpl().setName("syncHandler")));
+
+        wb.register(ExternalIdentityProviderManager.class, extIPMgr, Collections.emptyMap());
+        wb.register(SyncManager.class, syncManager, Collections.emptyMap());
+
+        CallbackHandler cbh = createCallbackHandler(wb, getContentRepository(), getSecurityProvider(), creds);
+
+        Map<String,Object> sharedState = new HashMap<>();
+
+        loginModule.initialize(new Subject(), cbh, sharedState, of(PARAM_IDP_NAME, DEFAULT_IDP_NAME, PARAM_SYNC_HANDLER_NAME, "syncHandler"));
+        assertTrue(loginModule.login());
+        assertTrue(loginModule.commit());
+        // The original PrincipalId is used, even if the Idp initially sent a new UderId.
+        assertEquals(creds.getUserId(), principalName);
+        assertTrue(loginModule.logout());
+    }
+
+    @Test
+    public void testLoginUserIdentifiedByExternalIdMissingCredentials() throws Exception {
+        System.setProperty("FT_GRANITE-61684", "true");
+        String idpName = "testExternalId";
+
+        // We need to create an index, or we have an exception with the search       
+        createExternalIdIndex(root.getTree("/"));
+        root.commit();
+        
+        ExternalIdentityProvider idp = new TestExternalUserIdIdentityProvider(idpName);
+
+        when(extIPMgr.getProvider(DEFAULT_IDP_NAME)).thenReturn(idp);
+        when(syncManager.getSyncHandler("syncHandler")).thenReturn(new DefaultSyncHandler(new DefaultSyncConfigImpl().setName("syncHandler")));
+
+        wb.register(ExternalIdentityProviderManager.class, extIPMgr, Collections.emptyMap());
+        wb.register(SyncManager.class, syncManager, Collections.emptyMap());
+
+        CallbackHandler cbh = createCallbackHandler(wb, getContentRepository(), getSecurityProvider(), null);
+
+        Map<String,Object> sharedState = new HashMap<>();
+        sharedState.put(SHARED_KEY_PRE_AUTH_LOGIN, new PreAuthenticatedLogin(ID_TEST_USER));
+        sharedState.put(SHARED_KEY_ATTRIBUTES, Collections.singletonMap("att", "value"));
+
+        loginModule.initialize(new Subject(), cbh, sharedState, of(PARAM_IDP_NAME, DEFAULT_IDP_NAME, PARAM_SYNC_HANDLER_NAME, "syncHandler"));
+        assertFalse(loginModule.login());
+        assertFalse(loginModule.commit());
+        assertFalse(loginModule.logout());
+    }
+
     @Test
     public void testLoginCommitUpdatesSubject() throws Exception {
         when(extIPMgr.getProvider(DEFAULT_IDP_NAME)).thenReturn(new TestIdentityProvider());
@@ -631,4 +802,5 @@
             verify(monitor).loginError();
         }
     }
+
 }
\ No newline at end of file
diff --git a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/TestExternalUserIdCredentials.java b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/TestExternalUserIdCredentials.java
new file mode 100644
index 0000000..dcb88d5
--- /dev/null
+++ b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/TestExternalUserIdCredentials.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+import org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule;
+import org.apache.jackrabbit.oak.spi.security.authentication.credentials.AbstractCredentials;
+import org.jetbrains.annotations.NotNull;
+
+class TestExternalUserIdCredentials extends AbstractCredentials {
+    public TestExternalUserIdCredentials(String originalUserId) {
+        super(originalUserId);
+    }
+
+    @Override
+    public @NotNull String getUserId() {
+        Object loginName = getAttribute(AbstractLoginModule.SHARED_KEY_LOGIN_NAME);
+        if ( loginName != null) {
+            return (String) loginName;
+        }
+        return userId;
+    }
+
+}
diff --git a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/TestExternalUserIdIdentityProvider.java b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/TestExternalUserIdIdentityProvider.java
new file mode 100644
index 0000000..1065382
--- /dev/null
+++ b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/TestExternalUserIdIdentityProvider.java
@@ -0,0 +1,290 @@
+/*
+ * 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;
+
+import org.apache.jackrabbit.oak.spi.security.authentication.AuthenticationConstants;
+import org.apache.jackrabbit.oak.spi.security.authentication.credentials.AbstractCredentials;
+import org.apache.jackrabbit.oak.spi.security.authentication.credentials.CredentialsSupport;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalGroup;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityException;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider;
+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.token.TokenConstants;
+import org.jetbrains.annotations.NotNull;
+
+import javax.jcr.Credentials;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public class TestExternalUserIdIdentityProvider implements ExternalIdentityProvider, CredentialsSupport {
+    
+    private final String name;
+
+    public TestExternalUserIdIdentityProvider(final String name) {
+        this.name = name;
+    }
+
+
+    //-------------------------------------< ExternalIdentityProvider >---
+
+    @Override
+    public ExternalUser authenticate(@NotNull Credentials credentials) {
+        if (credentials instanceof TestExternalUserIdCredentials) {
+            TestExternalUserIdCredentials oAuthCredentials = (TestExternalUserIdCredentials) credentials;
+            return new TestExternalUser(oAuthCredentials);
+        }
+        return null;
+    }
+
+    @Override
+    public ExternalGroup getGroup(@NotNull String name) throws ExternalIdentityException {
+        return null;
+    }
+
+    @Override
+    public ExternalIdentity getIdentity(@NotNull ExternalIdentityRef ref) throws ExternalIdentityException {
+        if (isForeignRef(ref)) {
+            return null;
+        } else if (ref instanceof TestGroupExternalIdentityRef) {
+            return new TestExternalUserIdExternalGroup(ref);
+        }
+        
+        return null;
+    }
+
+    @Override
+    public @NotNull String getName() {
+        return name;
+    }
+
+    @Override
+    public ExternalUser getUser(@NotNull String userId) throws ExternalIdentityException {
+        return null;
+    }
+
+    @Override
+    public @NotNull Iterator<ExternalUser> listUsers() throws ExternalIdentityException {
+        //return an empty iterator
+        return Collections.emptyIterator();
+    }
+
+    @Override
+    public @NotNull Iterator<ExternalGroup> listGroups() throws ExternalIdentityException {
+        //return an empty iterator
+        return Collections.emptyIterator();
+    }
+
+    //-----------------------------------------    PRIVATE METHODS---
+    
+    private boolean isForeignRef(ExternalIdentityRef ref) {
+        if (ref == null) {
+            return false;
+        }
+        
+        String provider = ref.getProviderName();
+        // the part that supports null or empty provider strings is taken from the LDAP idp code
+        return !(provider == null || provider.isEmpty() || getName().equals(ref.getProviderName()));
+    }
+    
+    //-----------------------------------------< CredentialsSupport >---
+
+    @Override
+    public @NotNull Set<Class> getCredentialClasses() {
+        return Collections.singleton(TestExternalUserIdCredentials.class);
+    }
+    
+    @Override
+    public String getUserId(@NotNull Credentials credentials) {
+        if (credentials instanceof TestExternalUserIdCredentials) {
+            return ((TestExternalUserIdCredentials)credentials).getUserId();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public @NotNull Map<String, ?> getAttributes(@NotNull Credentials credentials) {
+        if (credentials instanceof TestExternalUserIdCredentials) {
+            HashMap<String, Object> attrs = new HashMap<>();
+            attrs.put(TokenConstants.TOKEN_ATTRIBUTE, "");
+            attrs.put(AuthenticationConstants.SHARED_ATTRIBUTE_EXTERNAL_ID, ((TestExternalUserIdCredentials) credentials).getAttribute(AuthenticationConstants.SHARED_ATTRIBUTE_EXTERNAL_ID));
+            attrs.put(AuthenticationConstants.SHARED_KEY_LOGIN_NAME, ((TestExternalUserIdCredentials) credentials).getAttribute(AuthenticationConstants.SHARED_KEY_LOGIN_NAME));
+            return attrs;
+        } else {
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public boolean setAttributes(@NotNull Credentials credentials, @NotNull Map<String, ?> attributes) {
+        if (credentials instanceof TestExternalUserIdCredentials) {
+            ((TestExternalUserIdCredentials)credentials).setAttributes((Map<String, Object>) attributes);
+            return true;
+        } else {
+            return false;
+        }
+    }
+    
+    //-----------------------------------------< OAuthExternalUser >---
+    
+    class TestExternalUser implements ExternalUser {
+        
+        final AbstractCredentials externalCredentials;
+
+        TestExternalUser(AbstractCredentials externalCredentials) {
+            this.externalCredentials = externalCredentials;
+        }
+
+        @Override
+        public @NotNull ExternalIdentityRef getExternalId() {
+            return new ExternalIdentityRef((String) Objects.requireNonNull(externalCredentials.getAttribute(AuthenticationConstants.SHARED_ATTRIBUTE_EXTERNAL_ID)),
+                    getName());
+        }
+
+        @Override
+        public @NotNull String getId() {
+            return externalCredentials.getUserId();
+        }
+
+        @Override
+        public @NotNull String getPrincipalName() {
+            return externalCredentials.getUserId();
+        }
+
+        /* 
+         * The intermediate path is extracted 
+         * from the id. It tries to be backward 
+         * compatible with the previous AEM 
+         * OAuth implementation
+         */
+        @Override
+        public String getIntermediatePath() {
+            String id = getId();
+            
+            if (id.length() > 4) {
+                id = id.substring(id.indexOf("-")+1);
+                if (id.length() > 4) {
+                  return id.substring(0,4);  
+                } else {
+                    return id;
+                }
+            } 
+            return id;
+        }
+
+        @Override
+        public @NotNull Iterable<ExternalIdentityRef> getDeclaredGroups()
+                throws ExternalIdentityException {
+            Iterable<ExternalIdentityRef> groups = getGroups();
+            if (groups!= null) {
+                List<ExternalIdentityRef> list = new ArrayList<>();
+                for (ExternalIdentityRef ref:groups) {
+                    list.add(new TestGroupExternalIdentityRef(ref.getId(), getName()));
+                }
+                return Collections.unmodifiableList(list);
+            } else {
+                return Collections.emptyList();
+            }
+        }
+
+        @Override
+        public @NotNull Map<String, ?> getProperties() {
+            return externalCredentials.getAttributes();
+        }
+        
+        Iterable<ExternalIdentityRef> getGroups() {
+            Collection<?> values = getProperties().values();
+            for (Object o: values) {
+                if (o instanceof ExternalUser) {
+                    ExternalUser externalUser = (ExternalUser) o;
+                    if (getExternalId().getId().equals(externalUser.getExternalId().getId())) {
+                        try {
+                            return externalUser.getDeclaredGroups();
+                        } catch (ExternalIdentityException e) {
+                            return null;
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+    }
+    
+    //-----------------------------------------< OAuthExternalGroup >---
+    
+    static class TestExternalUserIdExternalGroup implements ExternalGroup {
+
+        final ExternalIdentityRef ref;
+        
+        public TestExternalUserIdExternalGroup(ExternalIdentityRef ref) {
+            this.ref = ref;
+        }
+
+        @Override
+        public @NotNull ExternalIdentityRef getExternalId() {
+            return ref;
+        }
+
+        @Override
+        public @NotNull String getId() {
+            return ref.getId();
+        }
+
+        @Override
+        public @NotNull String getPrincipalName() {
+            return ref.getId();
+        }
+
+        @Override
+        public String getIntermediatePath() {
+            return null;
+        }
+
+        @Override
+        public @NotNull Iterable<ExternalIdentityRef> getDeclaredGroups()
+                throws ExternalIdentityException {
+            //not supporting nested groups for now
+            return Collections.emptyList();
+        }
+
+        @Override
+        public @NotNull Map<String, ?> getProperties() {
+            return Collections.emptyMap();
+        }
+
+        @Override
+        public @NotNull Iterable<ExternalIdentityRef> getDeclaredMembers() {
+            return Collections.emptyList();
+        }
+    }
+    
+    static class TestGroupExternalIdentityRef extends ExternalIdentityRef {
+        public TestGroupExternalIdentityRef(String id, String providerName) {
+            super(id, providerName);
+        }
+    }
+
+}
diff --git a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModule.java b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModule.java
index 4434d8c..952dabc 100644
--- a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModule.java
+++ b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AbstractLoginModule.java
@@ -150,25 +150,25 @@
      * Key of the sharedState entry referring to validated Credentials that is
      * shared between multiple login modules.
      */
-    public static final String SHARED_KEY_CREDENTIALS = "org.apache.jackrabbit.credentials";
+    public static final String SHARED_KEY_CREDENTIALS = AuthenticationConstants.SHARED_KEY_CREDENTIALS;
 
     /**
      * Key of the sharedState entry referring to a valid login ID that is shared
      * between multiple login modules.
      */
-    public static final String SHARED_KEY_LOGIN_NAME = "javax.security.auth.login.name";
+    public static final String SHARED_KEY_LOGIN_NAME = AuthenticationConstants.SHARED_KEY_LOGIN_NAME;
 
     /**
      * Key of the sharedState entry referring to public attributes that are shared
      * between multiple login modules.
      */
-    public static final String SHARED_KEY_ATTRIBUTES = "javax.security.auth.login.attributes";
+    public static final String SHARED_KEY_ATTRIBUTES = AuthenticationConstants.SHARED_KEY_ATTRIBUTES;
 
     /**
      * Key of the sharedState entry referring to pre authenticated login information that is shared
      * between multiple login modules.
      */
-    public static final String SHARED_KEY_PRE_AUTH_LOGIN = PreAuthenticatedLogin.class.getName();
+    public static final String SHARED_KEY_PRE_AUTH_LOGIN = AuthenticationConstants.SHARED_KEY_PRE_AUTH_LOGIN;
 
     protected Subject subject;
     protected CallbackHandler callbackHandler;
diff --git a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AuthenticationConstants.java b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AuthenticationConstants.java
new file mode 100644
index 0000000..eb7032b
--- /dev/null
+++ b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/AuthenticationConstants.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+/**
+ * Constants used by the authentication framework.
+ */
+public interface AuthenticationConstants {
+
+    /**
+     * Key of the sharedState entry referring to a valid login ID that is shared
+     * between multiple login modules.
+     */
+    String SHARED_KEY_LOGIN_NAME = "javax.security.auth.login.name";
+
+    /**
+     * Key of the sharedState entry referring to validated Credentials that is
+     * shared between multiple login modules.
+     */
+    String SHARED_KEY_CREDENTIALS = "org.apache.jackrabbit.credentials";
+
+    /**
+     * Key of the sharedState entry referring to public attributes that are shared
+     * between multiple login modules.
+     */
+    String SHARED_KEY_ATTRIBUTES = "javax.security.auth.login.attributes";
+
+    /**
+     * Key of the sharedState entry referring to pre authenticated login information that is shared
+     * between multiple login modules.
+     */
+    String SHARED_KEY_PRE_AUTH_LOGIN = PreAuthenticatedLogin.class.getName();
+
+    /**
+     * Name of the attribute storing the external identifier in Credentials
+     */
+    String SHARED_ATTRIBUTE_EXTERNAL_ID = ":externalId";
+
+}
\ No newline at end of file
diff --git a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/package-info.java b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/package-info.java
index 163f8ae..362747c 100644
--- a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/package-info.java
+++ b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/package-info.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@Version("1.6.0")
+@Version("1.7.0")
 package org.apache.jackrabbit.oak.spi.security.authentication;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenConstants.java b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenConstants.java
index 4aab444..bed1a20 100644
--- a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenConstants.java
+++ b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenConstants.java
@@ -19,6 +19,7 @@
 import java.util.Set;
 
 import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.security.authentication.AuthenticationConstants;
 
 public interface TokenConstants {
 
@@ -34,11 +35,14 @@
     String TOKENS_NT_NAME = NodeTypeConstants.NT_REP_UNSTRUCTURED;
 
     String TOKEN_NT_NAME = "rep:Token";
+    
 
     Set<String> RESERVED_ATTRIBUTES = Set.of(
             TOKEN_ATTRIBUTE,
             TOKEN_ATTRIBUTE_EXPIRY,
-            TOKEN_ATTRIBUTE_KEY);
+            TOKEN_ATTRIBUTE_KEY,
+            AuthenticationConstants.SHARED_KEY_LOGIN_NAME,
+            AuthenticationConstants.SHARED_ATTRIBUTE_EXTERNAL_ID);
 
     Set<String> TOKEN_PROPERTY_NAMES = Set.of(TOKEN_ATTRIBUTE_EXPIRY, TOKEN_ATTRIBUTE_KEY);
 
diff --git a/oak-security-spi/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenConstantsTest.java b/oak-security-spi/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenConstantsTest.java
index c6f9497..fdf5045 100644
--- a/oak-security-spi/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenConstantsTest.java
+++ b/oak-security-spi/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/token/TokenConstantsTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.jackrabbit.oak.spi.security.authentication.token;
 
+import org.apache.jackrabbit.oak.spi.security.authentication.AuthenticationConstants;
 import org.junit.Test;
 
 import java.util.Set;
@@ -29,7 +30,7 @@
 
     @Test
     public void testReservedAttributes() {
-        assertEquals(Set.of(TOKEN_ATTRIBUTE, TOKEN_ATTRIBUTE_EXPIRY, TOKEN_ATTRIBUTE_KEY), TokenConstants.RESERVED_ATTRIBUTES);
+        assertEquals(Set.of(TOKEN_ATTRIBUTE, TOKEN_ATTRIBUTE_EXPIRY, TOKEN_ATTRIBUTE_KEY, AuthenticationConstants.SHARED_ATTRIBUTE_EXTERNAL_ID, AuthenticationConstants.SHARED_KEY_LOGIN_NAME), TokenConstants.RESERVED_ATTRIBUTES);
     }
 
     @Test