Permission Sensitive Caching in AEM (1/7)
CUGs: Access Control on Publish
Permission Sensitive Caching in AEM (1/7)
CUGs: Access Control on Publish

Part 1 of 7 on protecting content behind a login on AEM
- Part 1 — CUGs: Access Control on Publish
- Part 2 — Setting up the Dispatcher and CDN
- Part 3 — Implementing a permission check servlet on Publish
- Part 4 — More CDN considerations
- Part 5 — Referencing Private Content
- Part 6 — Using OIDC as Single Sign On (planned)
- Part 7 — Permission-sensitive caching in the CDN (planned)
In this part, I want to share my experience with Login — Protection and Closed User Groups (CUG) to make a site — or parts of a site — private.
The Mission
You have a semi-public or private website hosted on AEM. This can be either an intranet website that requires a login to access all content, or a public website that requires a login for only parts of the site. Think about a “members only” area.
Login and Access Control
In general, we distinguish two “levels” of Access Control:
- Authentication — A user successfully logs in, i.e., he or she can prove they are the person they claim to be. They verify their identity by providing certain credentials (e.g., a password, passkey, smart card, …)
- Authorization — After the identity of the users has been established, we can differentiate what resources they have access to. E.g., we could differentiate silver-, gold-, and platinum members. Or we could differentiate end customers and business customers.
- Note: This differentiation is not always necessary. Sometimes it is sufficient to distinguish between anonymous and logged-in users. In this case, all authenticated users are also considered authorized to access resources. CUGs are not required in this case. Only login.
Implementation Approaches of Access Control on Publish
There are different types of access control that can be implemented in AEM:
- Regular Access Control Entries (ACEs) — this is what you probably already know from being used on Author. In theory, this could be used on Publish. But it does not make much sense — and I have never seen it implemented. Also, this is not the subject here.
- Access control in a proxy layer — In this scenario, AEM would see the whole website as public, but you would restrict access on a proxy layer in front of the Publish system. This approach was popular some 15 years ago. Nowadays, I consider it quite outdated and impractical: it does not support fine-grained per-resource access control, and it does not work well with AEMaaCS, where the outer layer of the HTTP stack is assumed to be the CDN. It still makes some sense to password-protect a site with basic authentication pre-go live. But here, we want to focus on protecting internal sites post-go-live.
- Fully custom Access Control in AEM — you could build your own access control schema based on Servlet Filters and/or Custom Authentication handlers. There are valid use cases for that approach… but this is outside the scope of this article series.
- Closed User Groups (CUGs) and Authentication Handlers — CUGs are a “simplified version” of ACEs that control read permission only. CUGs have been purpose-built for the use case in question. And this is what this article series is about.
CUG — Closed User Groups
CUGs, much like ACEs, are built into AEM. However, unlike ACEs, they are intended to operate only on the Publish instance. You can think of them as simplified “allow-read” permission for a specific set of groups. When you add a CUG to a resource, it implicitly denies access to any user who is not a member of one of the specified groups. In other words, you can only control read access (not write) and only in an allow capacity.
- You can not allow
/x/yand then create an exception by denying/x/y/z,as is possible with ACEs on Author. - CUGs are inherited from parent resources: If
/x/yis protected by a CUG A, then/x/y/zalso requires membership in CUG A. - CUGs are additive at the resource-tree level and disjunctive at group level: If
/x/yis protected by a CUG A, and/x/y/zis protected by a CUG B, then access to/x/y/zrequires membership in either A OR B.
Login Requirement
CUGs are implemented in two steps. First, you mark a subtree as having an Authentication Requirement (“Login Requirement”). This defines that a user must be logged in to access that area. Second, within that subtree, you use CUG permissions to determine which specific user groups have read access.

Note that the login URL that performs the authentication flow must be publicly accessible. (This can be a page/servlet in AEM or an external service.)
CUG vs Login Requirement
By default, logging in is implicit: When users open any resource below an Authentication Required resource without being logged in, they are redirected to the login page.
AEM handles the login and the CUG validation separately. CUG validation simply checks whether a user is a member of at least one CUG assigned to the resource tree. It does not verify the user's identity. This leads to two interesting edge cases:
- Login protection without CUG
- CUG without previous login
Login without CUG
This is trivial: You only want to grant access to logged-in users. And you do not differentiate between subgroups. In the diagram above, Page P is protected by a simple login. Users do not require any additional permissions.
CUG without Login

At first glance, this looks funny: Earlier, I said that the login is implicit. When opening a page marked "Authentication Required,” users are redirected to a login page. In the example above, when unauthenticated users open the CUG-protected page P, they immediately receive a 404 response, meaning “Nothing here for you.” Logged-out anonymous users are in no CUG, so the denial is correct.
You can set the log-protection to a different page, though. In the example above, the page “Auth req” is marked as Authentication Required. This can be used as an explicit login. Users must log in explicitly first before they can open any CUG-protected page. This is what you could use when there is a “Login” button in your main navigation.
Where to define CUGs and the Login Requirement
By default, the Authentication Requirement is set on the Advanced tab of the Page Properties dialog:

Note that you can also specify the location of the login page. If this is not specified here, AEM will revert to an OSGi configuration — see further below.
CUGs are added in the Permissions tab.

Don’t forget to publish the pages after changing the page properties. The settings are relevant only to and evaluated on the Publish system.
Best Practices
Local Testing in Different Browser Profiles
I recommend you get familiar with Publish-side authentication by using the WKND tutorial. WKND already has some basic login support built in. This is not the most sophisticated production-ready solution, but it helps illustrate a few basic principles by observing the login conversation in the browser’s developer console before you start your own implementation.
The most basic case is adding Authentication Required = enabled to a page, e.g.,
→ http://localhost:4502/sites.html/content/wknd/us/en/adventures/private-tours
publishing the page, and opening it from the Publish system in another browser tab, e.g. at
→ http://localhost:4503/content/wknd/us/en/adventures/private-tours.html
As this page now requires a login, you should be redirected to the login page configured in the OSGi config (see below)…
… but there is a 50% chance this will not happen. 😲
This is a common pitfall. I wanted you to step into that pitfall early so you are not confused later, when you have a more complex setup.
Here is what could happen:
It is not unusual for you to have logged in to Publish with your admin account to inspect the CRX/DE or the System Console. In this case, you already have a login session that AEM sees as sufficient to grant you access to the login-protected content on your site. Hence, you will not be redirected. I recommend you make it a habit to use one browser profile for administrative work on Author and Publish with the admin account, and another for testing user Authentication on Publish with user accounts.
See also: Testing Users and Groups with Chrome Profiles
Separate CUGs from Groups
When creating CUGs, make sure they are stored in a separate folder to distinguish Author- and Publish groups.
Example:
/home/groups/cugs
You can’t define folders for groups in the UI. But I consider it good practice to create groups using tools such as the AC tool anyway, which lets you define custom group folders. Or you can use a *repoinit* script.
Then create ACEs on the Author to allow regular users read-only access in the cug folder.
CUG Naming Convention
On the “Permissions” dialog, when adding a CUG, AEM auto-completes the group name you enter. The auto-completion is executed against all groups below /home/groups. If your user account has read-access to all groups, you see all groups in a long list — CUGs and Author groups. A simple workaround is to prefix all CUGs with cug-. Now, when you type cug…, you only see the CUG’s.

Taking a look under the hood
Usually, whatever you store from an AEM dialog is stored as properties in the jcr:content node. This is not the case for CUGs and the Authentication Required properties, though. In fact, it took me some time to find the settings on the node.
It is important to understand how this works because those details will matter in Part 3 — Implementing the permission check of this article series.
Storing the Auth Requirement
When observing the POST request on the properties dialog, all looks normal. There is a property in the request:

But you won’t find this property in CRX/DE.
Instead, AEM creates a mixin granite:AuthenticationRequired when it sees this property:

Storing the CUGs
You also won’t find the CUGs in the dialog's POST request: The CUGs have been stored already before, when you closed the “Edit Closed User Group” sub-dialog within the Permissions tab, a servlet is called on the resource, which was bound to the selector cugpolicy :

The settings are stored as mixin rep:CugMixin,and a separate node rep:cugPolicy below the page node:

Caveats
Out-of-the-box CUGs come with a couple of caveats:
- I find the Authentication Required property's location a bit misplaced in the Advanced tab of the page properties. This is not optimal from a UI perspective as it can easily be overlooked by authors. You might consider customizing the dialog and moving this property to the Permissions tab. Or maybe you do not want to surface this property at all in the dialog. If there is only one unchangeable private area on the site, you might as well configure it in an OSGi config (see below).
- ACEs and CUGs are managed on the same tab. I would argue that administrators manage privileges on the Author, whereas end-user privileges are the editor’s responsibility. Managing authoring privileges in the page properties dialog looks convenient at first glance — but only for smaller sites (which rarely require an enterprise CMS). I usually advocate using tool-based ACE management, like ACtool, as it lets you test Access Control groups in lower environments. You can then hide the option to Add Permissions on the Permissions tab and let editors focus on CUGs.
- To add CUGs to a page, editors need the privilege
jcr:modifyAccessControl. This is a major caveat: This allows editors to modify privileges. They could use this to escalate their own privileges within a resource tree. I.e., grant themselves publish privileges when they did not have them before. They could also grant privileges to other groups. This effectively yields administration privileges to mere editors. I can’t imagine any circumstance where this could be intended. Consequently, you should not allow content editors to change CUGs, but reserve this for dedicated administrative roles. This can inhibit the autonomy of editing teams and the flow of content creation, depending on the granularity of the CUGs and how often they need to change. - Important Note: Earlier, I proposed removing the Add Permissions feature from the dialog. This does not solve the underlying problem with possible privilege escalation. Casual editors might not have the obvious option to escalate privileges in the UI. But more advanced users can easily come around the UI restriction using curl. I leave it to your own judgment how strictly privilege escalation must be prevented.
OSGi Configurations
3rd Party Single Sign On
A production-ready AEM Publish system does not authenticate users itself; instead, it integrates with a third-party identity provider (IDP) that serves as a Single Sign-On (SSO) system.
The two most prevalent protocols to integrate an external IDP are
- SAML and
- OIDC
Those can be seen as “industry standards” and are implemented by most IDPs. Just search for either term, and you’ll find a host of commercial and open source solutions.
On the AEM side, these protocols are already implemented using the AEM AuthenticationProvider interface. The interface provides means to augment AEM with different authentication methods. You can also implement your own provider using this interface. You can also use different provider types and provider endpoints in parallel — e.g., for different sites or subtrees.
The built-in providers are configured via OSGi config files (in “publish” run mode).
I will explain how to integrate an OIDC server in a later article of this series. If you want to jump ahead, please refer to the official documentation: OpenID Connect Support for AEM as a Cloud Service on Publish Tier.
Simple Authentication
To get started in this tutorial, we use the authentication configured in WKND. Authentication is spread over three different OSGi configurations:
- **org.apache.sling.engine.impl.auth.SlingAuthenticator**
- **com.day.cq.auth.impl.LoginSelectorHandler**
- **com.day.crx.security.token.impl.impl.TokenAuthenticationHandler**
Sling Authenticator
In this configuration (on publish), we can set which areas require authentication and how authentication works in general. If we add the path +/content/wknd/us/en/adventures/private-tours to the Authentication Required list, AEM now redirects to the login page when the URL is called — even when there is no Authentication Required on the page itself. This can be used if there is only one or a few private areas on your site.

Important Note: You cannot read this property from within your custom code. You cannot use the requirement’s values in your own permission-check servlet, which we will see in Part 3. If you set this property, replicate its value in the configuration of your own services. Or use the Authentication Required property/mixin on the page as described further above.
LoginSelectorHandler
When AEM detects that you request a resource that requires authentication, this service redirects you to the corresponding login page. As mentioned earlier, we can implement multiple authentication services on a single AEM instance:

WKND uses this service to demonstrate how different regional sites can use different login pages:
E.g., all login-protected pages below /content/wknd/us would redirect to
/content/wknd/us/en/errors/sign-in to login.
WKNDs login page provides a simple HTML form that POSTs
j_username and j_password to
http://localhost:4503/system/sling/login/j_security_check
This built-in Authenticator sets the login token as a cookie in return:

TokenAuthenticationHandler
This authentication handler reads the token cookie set above and grants or denies access.
CUG Evaluation
Evaluating the CUGs does not require configuration in OSGi. It just works. When a user U is a member of a CUG group G that protects resource R, U is granted access after login.
Conclusion
I hope this article gave you a brief overview of what CUGs are and how AEM handles them in the context of a log-in.
Buy me a coffee
Thanks for reading — if you’d like to support me, you can buy me a virtual coffee here: https://buymeacoffee.com/achimkoch
메타데이터
- post_id
- ecf17e1abea9
- slug
- permission-sensitive-caching-in-aem-1-6-ecf17e1abea9
- url
- https://medium.com/@achimkoch/permission-sensitive-caching-in-aem-1-6-ecf17e1abea9
- canonical_url
- https://medium.com/@achimkoch/permission-sensitive-caching-in-aem-1-6-ecf17e1abea9
- author_url
- https://medium.com/@achimkoch
- status
- ok
- fetched_at
- 2026-07-14 04:48:47