Migrating to External Identity and Dynamic Group Membership migrating-to-external-identity
Overview overview
When Data Synchronization is enabled in AEM as a Cloud Service, the SAML Authentication Handler can be configured to automatically migrate to external identities with dynamic group membership when it manages user and group creation. If your project uses custom code to create users or groups, you must update it to create external users and groups, as opposed to local users and groups.
Why External Users and Groups Are Required why-external-required
Migrating from local users and groups to external identities with dynamic group membership is essential for several critical reasons:
Performance Optimization:
- Reduced Repository Writes: Traditional local group membership requires writing membership relationships to the repository in a single multi valued property of the group node. With dynamic group membership, users have a single
rep:externalPrincipalNamesproperty containing all group principals, eliminating the need for synchronizing the group node - Faster Synchronization: When synchronizing users across publish tier nodes, external users with dynamic group membership require significantly less data transfer and fewer write operations compared to local users with traditional group memberships
- Scalability: Systems with large numbers of users and groups benefit dramatically from reduced repository overhead. Dynamic group membership scales efficiently even with very large groups.
This document provides technical guidance for:
- Understanding the external identity model
- Modifying custom code to create external users and groups
- Migrating existing local users and groups to the external identity model
Understanding External Identity understanding-external-identity
External Users external-users
External users are identified by the rep:externalId property, which links the user to an external identity provider. The format is:
userId;idpName
For example: john.doe;saml-idp.
idpName refers to the name of the Oak Identity Provider (Idp) as defined in the Authentication Handler configuration. For SAML integrations, this is the value set for the idpIdentifier attribute in the SAML Authentication Handler.Key Properties:
rep:externalId: Required property that marks a user as external (e.g.,john.doe;saml-idp)rep:externalPrincipalNames: Multi-valued property containing external group principals for dynamic membershiprep:lastSynced: Timestamp of last synchronizationrep:lastDynamicSync: Timestamp of last dynamic group membership sync
External Groups external-groups
External groups are also identified by the rep:externalId property and use a principal name format:
groupId;idpName
For example: content-authors;saml-idp
Dynamic Group Membership dynamic-group-membership
Instead of direct user-to-group relationships stored in the repository, dynamic group membership uses the rep:externalPrincipalNames property on the user node. When a user has an external principal name that matches an external group’s ID, they become a member of that group automatically. For more information, see the Apache Oak documentation.
Benefits:
- Reduced repository writes (no group membership nodes are modified when users are added/removed from groups)
- Faster synchronization across publish tier nodes
- Scalable group membership management
- Compatible with Data Synchronization requirements
Service User Configuration service-user-configuration
All operations that create or modify external users and groups should be performed using a service user that is properly configured to bypass the default protection on the rep:externalId and rep:externalPrincipalNames properties.
Why is a Service User Required why-is-a-service-user-required
By default, Oak security prevents regular sessions from modifying protected properties like:
rep:externalId- Marks users/groups as externalrep:externalPrincipalNames- Stores dynamic group membership principals
Only a properly configured service user can modify these properties.
Service User Configuration and Mapping service-user-configuration-mapping
Setting up a service user to manage external identities requires three coordinated configurations:
- Create the service user via
repoinit - Configure
ExternalPrincipalprotection - Map the service user to your application bundle.
See below for an extensive descripton of these steps.
Step 1: Create the Service User via Repoinit create-the-serviice-user-via-repoinit
This step details the creation of the service user with necessary permissions using a repoinit script.
Configuration File: org.apache.sling.jcr.repoinit.RepositoryInitializer~group-provisioner.cfg.json
Exemplary location: ui.config/src/main/content/jcr_root/apps/yourproject/osgiconfig/config.publish/
{
"scripts": [
"create service user group-provisioner with path system/yourproject",
"set ACL for group-provisioner\n allow jcr:read,jcr:readAccessControl,jcr:modifyAccessControl,rep:userManagement,rep:write on /home/users\n allow jcr:read,jcr:readAccessControl,jcr:modifyAccessControl,rep:userManagement,rep:write on /home/groups\nend"
]
}
Permissions Overview
jcr:read: Read users and groupsjcr:readAccessControl: Read ACLsjcr:modifyAccessControl: Modify ACLs (needed for setting properties)rep:userManagement: Create and manage users/groupsrep:write: Write properties includingrep:externalIdandrep:externalPrincipalNames
/home/users/system/yourproject to keep it organized with other system users.Step 2: Configure ExternalPrincipal Protection configure-externalprincipal-protection
Below is an example configuration for whitelisting the service user so it can bypass protection applied to external identity properties.
Configuration file name: org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal.ExternalPrincipalConfiguration.cfg.json
Example location: ui.config/src/main/content/jcr_root/apps/yourproject/osgiconfig/config.publish/
{
"protectExternalIdentities": "Warn",
"systemPrincipalNames": [
"group-provisioner",
"saml-migration-service"
]
}
Configuration Properties:
-
protectExternalIdentities: Controls the level of protection for external identity properties:"Strict": Only system principals in the whitelist can modify external properties. This is the level recommended for production."Warn": Logs warnings but allows modifications. Useful for development/testing."None": No protection. Not recommended.
-
systemPrincipalNames: List of service user names allowed to modifyrep:externalIdandrep:externalPrincipalNames. Include all service users that need to manage external identities (e.g.,group-provisioner,saml-migration-service).
systemPrincipalNames must exactly match the service user IDs created in the repoinit script.Step 3: Service User Mapping service-user-mapping
Map the service user to your application bundle so your code can use it.
Configuration File: org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~group-provisioner.cfg.json
Location: ui.config/src/main/content/jcr_root/apps/yourproject/osgiconfig/config.publish/
{
"user.mapping": [
"yourproject.core:group-provisioner=[group-provisioner]"
]
}
Mapping Format:
yourproject.core: The symbolic bundle name (found inpom.xml<Bundle-SymbolicName>)group-provisioner(before=): The subservice name you will use in code[group-provisioner](after=): The actual service user ID created in repoinit
Using the Service User in Code using-the-service-user-in-code
When opening a session to perform migration or user/group creation operations, you must use the service user:
import org.apache.sling.jcr.api.SlingRepository;
@Reference
private SlingRepository repository;
// Login as the service user
Session serviceSession = repository.loginService("group-provisioner", null);
try {
UserManager userManager = ((JackrabbitSession) serviceSession).getUserManager();
// Perform operations...
serviceSession.save();
} finally {
if (serviceSession != null && serviceSession.isLive()) {
serviceSession.logout();
}
}
rep:externalId or rep:externalPrincipalNames will fail with permission errors. Ensure your service user is properly configured in the ExternalPrincipal configuration before attempting migration.Complete Configuration Example complete-configuration-example
Below you will find a complete working example showing all three configurations together:
File Structure file-structure
ui.config/src/main/content/jcr_root/apps/yourproject/osgiconfig/
└── config.publish/
├── org.apache.sling.jcr.repoinit.RepositoryInitializer~group-provisioner.cfg.json
├── org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal.ExternalPrincipalConfiguration.cfg.json
└── org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~group-provisioner.cfg.json
Modifying Custom Code modifying-custom-code
Creating External Users creating-external-users
Before (Local User):
UserManager userManager = ((JackrabbitSession) session).getUserManager();
User user = userManager.createUser(userId, password);
After (External User):
import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
UserManager userManager = ((JackrabbitSession) session).getUserManager();
ValueFactory valueFactory = session.getValueFactory();
// Create user with principal
Principal userPrincipal = new Principal() {
@Override
public String getName() {
return userId;
}
};
User user = userManager.createUser(userId, null, userPrincipal, null);
// Set rep:externalId
ExternalIdentityRef externalRef = new ExternalIdentityRef(userId, idpName);
String externalId = externalRef.getString(); // Format: userId;idpName
user.setProperty("rep:externalId", valueFactory.createValue(externalId));
// Set sync timestamps to far future (workaround for OAK-12079)
// Set to 10 years in the future to prevent premature cleanup of external group memberships
// See: https://issues.apache.org/jira/browse/OAK-12079
java.util.Calendar future = java.util.Calendar.getInstance();
future.add(java.util.Calendar.YEAR, 10);
user.setProperty("rep:lastSynced", valueFactory.createValue(future));
user.setProperty("rep:lastDynamicSync", valueFactory.createValue(future));
session.save();
Creating External Groups creating-external-groups
Before (Local Group):
UserManager userManager = ((JackrabbitSession) session).getUserManager();
Group group = userManager.createGroup(groupId);
After (External Group):
import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
UserManager userManager = ((JackrabbitSession) session).getUserManager();
ValueFactory valueFactory = session.getValueFactory();
// Create group with principal
Principal groupPrincipal = new Principal() {
@Override
public String getName() {
return groupId;
}
};
Group group = userManager.createGroup(groupPrincipal);
// Set rep:externalId
ExternalIdentityRef externalRef = new ExternalIdentityRef(groupId, idpName);
String externalId = externalRef.getString(); // Format: groupId;idpName
group.setProperty("rep:externalId", valueFactory.createValue(externalId));
session.save();
Assigning Dynamic Group Membership assigning-dynamic-membership
Before (Direct Membership):
Group group = (Group) userManager.getAuthorizable(groupId);
User user = (User) userManager.getAuthorizable(userId);
group.addMember(user);
After (Dynamic Membership):
User user = (User) userManager.getAuthorizable(userId);
ValueFactory valueFactory = session.getValueFactory();
// Get existing external principal names
Value[] existingValues = user.getProperty("rep:externalPrincipalNames");
List<String> principalNames = new ArrayList<>();
if (existingValues != null) {
for (Value value : existingValues) {
principalNames.add(value.getString());
}
}
// Add new principal name (format: groupId;idpName)
String dynamicGroupPrincipal = groupId + ";" + idpName;
if (!principalNames.contains(dynamicGroupPrincipal)) {
principalNames.add(dynamicGroupPrincipal);
// Create new Value array
Value[] newValues = new Value[principalNames.size()];
for (int i = 0; i < principalNames.size(); i++) {
newValues[i] = valueFactory.createValue(principalNames.get(i));
}
// Set the property
user.setProperty("rep:externalPrincipalNames", newValues);
// Update sync timestamps to far future (workaround for OAK-12079)
// Set to 10 years in the future to prevent premature cleanup of external group memberships
// See: https://issues.apache.org/jira/browse/OAK-12079
java.util.Calendar future = java.util.Calendar.getInstance();
future.add(java.util.Calendar.YEAR, 10);
user.setProperty("rep:lastDynamicSync", valueFactory.createValue(future));
user.setProperty("rep:lastSynced", valueFactory.createValue(future));
}
session.save();
Migration Process migration-process
Migrating existing local users and groups to the external identity is not required when the custom code was updated before enabling Data Synchronization Services.
If local users and groups have already been persisted in the repository and the environment is actively used, we recommend you perform a multistep migration like the following, in order to avoid disruptions or inconsistencies.
group-provisioner) that has been granted permissions to bypass protection on rep:externalId and rep:externalPrincipalNames properties. See Service User Configuration for more details.Step 1: Create External Group Structure step-1-create-external-group-structure
For each local group that needs to be migrated:
- Create a corresponding external group with principal name:
<localGroupId>;<idpName>. Use a naming convention that helps linking external groups with local groups - Set the
rep:externalIdproperty on the external group with values:<localGroupId>;<idpName> - Add the external group as a member of the original local group.
Validation
- You can validate the results by checking if every local group has a corresponding external group. Additionally, every external group is member of the corresponding local group.
Example Servlet Endpoint:
@SlingServletPaths("/bin/migration/step1")
public class MigrationStep1Servlet extends SlingAllMethodsServlet {
@Override
protected void doPost(SlingHttpServletRequest request,
SlingHttpServletResponse response) {
String groupPath = request.getParameter("groupPath");
String idpName = request.getParameter("idpName");
// Check if the caller is authorized to run the servlet
isAuthorizedCaller(request, response);
// Get local group
Authorizable localGroupAuth = userManager.getAuthorizableByPath(groupPath);
Group localGroup = (Group) localGroupAuth;
String localGroupId = localGroup.getID();
// Create external group
String externalGroupPrincipalName = localGroupId + ";" + idpName;
// The function createExternalGroup performs the following steps:
// 1. Creates a new external group with the given principal name (format: "<localGroupId>;<idpName>").
// 2. Sets the 'rep:externalId' property on the group to mark it as an external group (value: "<localGroupId>;<idpName>").
// 3. Sets the 'rep:principalName' property for the group if required.
// 4. Assigns any other required group metadata, such as a title or description, if needed.
// 5. Persists the new group node in the repository at the appropriate path under /home/groups.
// 6. Returns the created Group object so it can be used for further operations, such as membership assignment.
Group externalGroup = createExternalGroup(externalGroupPrincipalName, localGroupId, idpName);
// Add external group to local group
localGroup.addMember(externalGroup);
session.save();
}
}
Usage:
curl -X POST "http://localhost:4503/bin/migration/step1?groupPath=/home/groups/c/content-authors&idpName=saml-idp"
Step 2: Convert Users and Assign Dynamic Membership step-2-convert-users-and-assign-dynamic-membership
For each user that is a member of a local group:
- Ensure it has
rep:externalIdset (convert to external user if needed). - For each group membership, add the corresponding external group principal to
rep:externalPrincipalNames - Update sync timestamps.
Example Servlet Endpoint:
@SlingServletPaths("/bin/migration/step2")
public class MigrationStep2Servlet extends SlingAllMethodsServlet {
@Override
protected void doPost(SlingHttpServletRequest request,
SlingHttpServletResponse response) {
String userId = request.getParameter("userId");
String idpName = request.getParameter("idpName");
// Check if the caller is authorized to run the servlet
isAuthorizedCaller(request, response);
// Login as the service user
Session serviceSession = repository.loginService("group-provisioner", null);
try {
UserManager userManager = ((JackrabbitSession) serviceSession).getUserManager();
User user = (User) userManager.getAuthorizable(userId);
// Ensure user has rep:externalId
Value[] externalIdValues = user.getProperty("rep:externalId");
if (externalIdValues == null || externalIdValues.length == 0) {
ExternalIdentityRef externalRef = new ExternalIdentityRef(userId, idpName);
user.setProperty("rep:externalId",
valueFactory.createValue(externalRef.getString()));
}
// Get all group memberships
Iterator<Group> groupIterator = user.declaredMemberOf();
List<String> principalNames = new ArrayList<>();
while (groupIterator.hasNext()) {
Group group = groupIterator.next();
String groupId = group.getID();
// Skip system groups
if ("everyone".equals(groupId)) {
continue;
}
// Add dynamic group principal
String dynamicGroupPrincipal = groupId + ";" + idpName;
principalNames.add(dynamicGroupPrincipal);
}
// Set rep:externalPrincipalNames
if (!principalNames.isEmpty()) {
Value[] newValues = new Value[principalNames.size()];
for (int i = 0; i < principalNames.size(); i++) {
newValues[i] = valueFactory.createValue(principalNames.get(i));
}
user.setProperty("rep:externalPrincipalNames", newValues);
}
// Update timestamps to far future (workaround for OAK-12079)
// Set to 10 years in the future to prevent premature cleanup of external group memberships
// See: https://issues.apache.org/jira/browse/OAK-12079
java.util.Calendar future = java.util.Calendar.getInstance();
future.add(java.util.Calendar.YEAR, 10);
user.setProperty("rep:lastDynamicSync", valueFactory.createValue(future));
user.setProperty("rep:lastSynced", valueFactory.createValue(future));
// Perform operations...
serviceSession.save();
} finally {
if (serviceSession != null && serviceSession.isLive()) {
serviceSession.logout();
}
} }
}
Usage:
curl -X POST "http://localhost:4503/bin/migration/step2?userId=john.doe&idpName=saml-idp"
Validation
You can validate this by checking that every user has the rep:externalId and rep:externalPrincipalName attributes with the principalName of every external group. The users are member of the local groups and the of the external groups.
Step 3: Remove Direct User Memberships step-3-remove-direct-user-memberships
After users have dynamic group membership configured:
- Remove direct user memberships from local groups
- Keep group-to-group memberships (including the external group membership)
Example Servlet Endpoint:
@SlingServletPaths("/bin/migration/step3")
public class MigrationStep3Servlet extends SlingAllMethodsServlet {
@Override
protected void doPost(SlingHttpServletRequest request,
SlingHttpServletResponse response) {
// Check if the caller is authorized to run the servlet
isAuthorizedCaller(request, response);
String groupPath = request.getParameter("groupPath");
Authorizable localGroupAuth = userManager.getAuthorizableByPath(groupPath);
Group localGroup = (Group) localGroupAuth;
// Process each member
Iterator<Authorizable> members = localGroup.getDeclaredMembers();
while (members.hasNext()) {
Authorizable member = members.next();
// Remove only user members, keep group members
if (!member.isGroup()) {
localGroup.removeMember(member);
}
}
session.save();
}
}
Usage:
curl -X POST "http://localhost:4503/bin/migration/step3?groupPath=/home/groups/c/content-authors"
Validation
- You can validate this by cheecking that every local group has only the corresponding external group, or other groups, as member.
Migration Workflow migration-workflow
Pre-Migration Checklist pre-migration-checklist
- Configure Service User: Create and configure the service user (for example,
group-provisioner) with proper permissions - Verify ExternalPrincipal Configuration: Ensure the service user is configured to bypass protection on
rep:externalIdandrep:externalPrincipalNames - Test Service User Permissions: Verify the service user can set external identity properties in development
- Identify all custom code that creates users or groups
- Review and update custom code to use external identity model
- Test updated code in development environment
- Inventory all existing local users and groups to migrate
- Test migration process in lower environments
Execution Steps execution-steps
-
Deploy Updated Code: Deploy custom code changes to create external users/groups
-
Create External Groups (for each local group):
code language-bash curl -X POST "http://localhost:4503/bin/migration/step1?groupPath=/home/groups/g/my-group&idpName=saml-idp" -
Migrate Users (for each user):
code language-bash curl -X POST "http://localhost:4503/bin/migration/step2?userId=username&idpName=saml-idp" -
Cleanup (for each migrated group):
code language-bash curl -X POST "http://localhost:4503/bin/migration/step3?groupPath=/home/groups/g/my-group" -
Verify: Check user group memberships and test access permissions
-
Enable Data Synchronization: Contact Customer Support to enable the feature
Post-Migration Validation post-migration-validation
Verify the migration:
-
Check User Properties:
On user nodes verify presence of:
rep:externalId: Format should beuserId;idpNamerep:externalPrincipalNames: Array of group principals in formatgroupId;idpNamerep:lastSynced: Timestamp set to far future (approximately 10 years from migration date)rep:lastDynamicSync: Timestamp set to far future (approximately 10 years from migration date)
Note: The timestamps are intentionally set to a far future date as a workaround for OAK-12079. This is expected behavior.
-
Check Group Properties:
On local group nodes verify presence of:
- External group member with format
groupId;idpName - No direct user members (only after Step 3)
- External group member with format
-
Test User Login: Verify users can log in and have correct permissions
-
Test Access Control: Verify users can access content protected by CUGs/ACLs
Troubleshooting troubleshooting
Common Issues common-issues
Issue: Permission errors when setting rep:externalId or rep:externalPrincipalNames
Error Messages:
javax.jcr.AccessDeniedException: Access deniedOakAccess0000: Access deniedCannot set property 'rep:externalId'
Solution: The session must be opened using a properly configured service user that has been granted permissions to bypass protection on external identity properties.
Steps to resolve:
- Verify service user exists: Ensure the service user (e.g.,
group-provisioner) is created via repoinit - Check service user mapping: Verify the servlet or service is using
repository.loginService("group-provisioner", null) - Verify ExternalPrincipal configuration: Ensure
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal.ExternalPrincipalConfigurationis properly configured - Check service user permissions: The service user needs
rep:writeandrep:userManagementpermissions on/home/usersand/home/groups
See Service User Configuration for complete setup instructions.
Issue: OakConstraint0072: Property 'rep:externalPrincipalNames' requires 'rep:externalId' to be present
Solution: Users must have rep:externalId set before setting rep:externalPrincipalNames. Ensure Step 2 converts users to external users first.
Issue: Users lose group memberships after migration
Solution: Verify that:
- External group was created with correct principal name format (
groupId;idpName) - External group was added as member of local group (Step 1)
- User has correct external principal names in
rep:externalPrincipalNames(Step 2) - Step 3 cleanup was performed only after Steps 1 and 2 were completed
Issue: External group memberships are unexpectedly removed after user login (OAK-12079)
Problem: Due to Oak bug OAK-12079, the Oak synchronization mechanism may prematurely clean up external group memberships based on the rep:lastSynced and rep:lastDynamicSync timestamps.
Solution: Set rep:lastSynced and rep:lastDynamicSync timestamps to a far future date (10 years from now) instead of the current time. This prevents the synchronization cleanup process from removing the external group memberships.
Implementation:
// Workaround for OAK-12079
// Set to 10 years in the future to prevent premature cleanup
// See: https://issues.apache.org/jira/browse/OAK-12079
java.util.Calendar future = java.util.Calendar.getInstance();
future.add(java.util.Calendar.YEAR, 10);
user.setProperty("rep:lastSynced", valueFactory.createValue(future));
user.setProperty("rep:lastDynamicSync", valueFactory.createValue(future));
Why this works: By setting the timestamps to a far future date, the Oak synchronization logic treats these users as “recently synchronized” and does not trigger the cleanup process that would remove the external principal names and group memberships.
Note: This is a temporary workaround until OAK-12079 is resolved in a future Oak release. All code examples in this document already include this workaround.
Issue: System group “everyone” causes errors
Solution: Always skip the “everyone” system group during user migration (Step 2). This group is automatically managed by AEM.
Rollback Procedure rollback-procedure
If migration encounters issues:
- Stop migration process
- Restore from backup if critical data was affected
- Rollback the changes on the code to create external users and groups with dynamic group membership
- Review and fix issues before reattempting migration.
Best Practices best-practices
- Test Thoroughly: Always test migration in development and staging environments before production
- Batch Processing: For large user bases, process migrations in batches to avoid timeout issues
- Monitor Performance: Watch repository performance during migration
- Maintain Audit Trail: Log all migration operations for troubleshooting
- Service User Permissions: Ensure migration servlets use appropriate service users with required permissions. The service user must be configured in the ExternalPrincipal configuration to bypass protection on
rep:externalIdandrep:externalPrincipalNamesproperties - Idempotent Operations: Design migration code to be safely re-runnable
- Validate at Each Step: Check results after each migration step before proceeding
Securing Migration Servlets securing-migration-servlets
The migration servlets have elevated privileges to create and modify users and groups. It is critical to restrict access to these endpoints to prevent unauthorized access.
Recommended Approach: IMS Technical Account Authentication recommended-approach-ims-technical-account
The recommended approach is to secure these servlets using Adobe IMS integration, allowing only an authorized technical account to access them.
Step 1: Create a Technical Account in AEM Developer Console create-a-technical-account-in-aem-developer-console
-
Navigate to Experience Manager and then Cloud Manager
-
Select your program, then click on the environment where you want to create the technical account
-
Click Developer Console in the environment’s ellipsis menu
-
In the AEM Developer Console, go to the Integrations tab
-
Click Create new technical account
-
Provide a name for the integration (e.g., “Migration Service Account”)
-
Click Create
-
Note the following values from the created integration:
- Client ID
- Client Secret
- Technical Account ID (this will be the user ID accessing your servlets - format:
XXXXXXXXXXXXXXXXXXXXXXXX@techacct.adobe.com)
For detailed instructions, see Generating Access Tokens for Server-Side APIs documentation.
Sample code to check if the caller is authorized:
private boolean isAuthorizedCaller(SlingHttpServletRequest request,
SlingHttpServletResponse response) {
Session session = request.getResourceResolver().adaptTo(Session.class);
String callerId = session != null ? session.getUserID() : null;
if (!ALLOWED_TECHNICAL_ACCOUNT.equals(callerId)) {
LOG.warn("Unauthorized access attempt by user: '{}' (expected: '{}')", callerId, ALLOWED_TECHNICAL_ACCOUNT);
response.setStatus(SlingHttpServletResponse.SC_FORBIDDEN);
return false;
}
return true;
}
Defense in Depth: IP-Based Restrictions defense-in-depth-ip-based-restrictions
As an additional layer of security, you can configure CDN rules to restrict access to migration endpoints by IP address. This is useful when migrations are run from known infrastructure.
Security Checklist security-checklist
Before deploying migration servlets to production:
- Create IMS integration in AEM Developer Console
- Configure servlets to validate the technical account ID
- Test authentication flow in development/staging environments
- Consider additional IP-based restrictions at CDN level
- Plan to disable or remove migration servlets after migration is complete
- Audit and log all access to migration endpoints