Protecting Content with Closed User Groups (CUG)
Learn how to restrict access to pages on your Edge Delivery site so that only authenticated users (optionally belonging to specific groups) can view them.
Overview
Closed User Groups (CUG) let you restrict access to pages on your site so that only authenticated users (optionally belonging to specific groups) can view them. You define the access rules in a spreadsheet, and when you apply them, the rules take effect at the CDN edge. The authoring step differs depending on whether your site runs on AEM Authoring or Document Authoring (DA), and the enforcement step differs depending on your CDN (Cloudflare or Akamai). This guide covers both.
Quick Start
Limiting access to pages using CUG starts with creating a spreadsheet and defining access rules in it. Once defined, the rules need to be applied so they become CUG headers that the CDN needs to enforce at the edge (see the section Applying Page Access below).
1. Create the CUG spreadsheet
Create a spreadsheet named closed-user-groups at your site root (the same level as your index page).
2. Add columns and rows
Example:
| url | cug-required | cug-groups | cug-login-path |
/members/** |
true |
||
/partners/** |
true |
gold,silver | https://idp.partners.example.com/login |
The spreadsheet has four columns. When the page access is applied, each row is translated into CUG headers:
| Column | Required | Description | CUG header |
url |
Yes | Path pattern to protect (must start with /) | |
cug-required |
No | Whether login is required: true or false |
x-aem-cug-required |
cug-groups |
No | Comma-separated list of allowed groups | x-aem-cug-groups |
cug-login-path |
No | Login page path (e.g. /login) or absolute URL (e.g. https://idp.example.com/login) to redirect unauthenticated visitors to | x-aem-cug-login-path |
Wildcard Patterns
Use wildcards in the url column to protect multiple pages at once:
| Pattern | Matches | Does NOT match |
/members/* |
/members/page1 |
/members/sub/page2 |
/members/** |
/members/page1, /members/sub/page2 |
/other/page |
*matches a single path segment**matches multiple path segments (the whole subtree)
Examples
The examples below build on each other, using a hypothetical site with a members area, a partners area, and some internal pages.
Step 1: Require login for a section
Any authenticated user should be able to access pages under /members/, but visitors must be logged in.
| url | cug-required | cug-groups | cug-login-path |
/members/** |
true |
Any authenticated user can access pages under /members/.
Step 2: Restrict a section to specific groups
Now add a partners area that only the gold and silver groups should be able to reach.
| url | cug-required | cug-groups | cug-login-path |
/partners/** |
true |
gold,silver |
Only users in the gold or silver group can access pages under /partners/.
Step 3: Send a group to its own login page
Partners should log in through their own identity provider instead of the default login flow.
| url | cug-required | cug-groups | cug-login-path |
/partners/** |
true |
gold,silver | https://idp.partners.example.com/login |
Only users in the gold or silver group who have logged in via https://idp.partners.example.com/login can access pages under /partners/.
Putting it all together
Combining tiered access within /members (separate gold and silver sub-groups, plus a free sub-section with authentication explicitly disabled), the partners rule from steps 2–3 (including its custom cug-login-path), and an internal employees-only area gives the following sheet:
| url | cug-required | cug-groups | cug-login-path |
/members/** |
true |
||
/members/gold/** |
true |
gold | |
/members/silver/** |
true |
silver | |
/members/free/** |
false |
||
/partners/** |
true |
gold,silver | https://idp.partners.example.com/login |
/internal/** |
true |
employees |
These mixed levels translate to the following behavior.
| Visitor requests… | Result |
/members/page |
Login required, any authenticated user |
/members/gold/page |
Login required, must be in gold group |
/members/silver/page |
Login required, must be in silver group |
/members/free/page |
Public (auth explicitly disabled) |
/partners/page |
Login required via idp.partners.example.com, must be in gold or silver group |
/internal/docs |
Login required, must be in employees group |
/public/page |
Public (no CUG rule applies) |
Inheritance
Child pages inherit the access rules of their parent path. For example:
- If
/members/**requires authentication, then/members/gold/pagealso requires authentication. - You can add group restrictions on a child path that inherits from a parent.
- Any restrictions added to a child path override what exists on the parent.
Important Rules
- The sheet must be at the site root. Place
closed-user-groupsat the same level as your index page. Sheets at deeper paths are ignored. - Paths must start with /. For example,
/partners/**is valid;partners/**is not. - No duplicate paths. If the same path appears more than once, only the first row is used. Later rows are silently ignored, not merged.
- Empty URLs are skipped. Rows without a
urlvalue are ignored. - Applying page access enforces the rules. Changes to the sheet only take effect once page access is applied.
- Removing page access removes all protection. See the section Removing Page Access below.
Applying Page Access
Applying the page access defined in the closed-user-groups sheet translates each row into CUG headers and sends them to the Config Service. The CDN needs to enforce them on every request. How you apply page access depends on your authoring platform:
For AEM Authoring
Applying page access is a built-in, out-of-the-box capability. No custom tooling is required. Simply publish the closed-user-groups sheet as you would any other page.
Note: Publishing the CUG sheet does not make it visible on Edge Delivery Services as a regular sheet. The sheet content (i.e., closed-user-groups.json) is not accessible on preview or live. Only the derived CUG headers are applied to the Config Service.
For Document Authoring (DA)
Document Authoring (DA) supports applying page access via the CUG plugin, an Adobe-supported AEM app. Once installed, it reads the closed-user-groups sheet, transforms each row into the corresponding x-aem-cug-required / x-aem-cug-groups / x-aem-cug-login-path headers, and posts them to the Config Service.
To use it in your project, add the plugin to the prepare sheet of your project config:
| title | path | icon |
| Protected Pages | https://main--aem-apps--adobe-rnd.aem.live/tools/plugins/cug/cug.html | https://da.live/img/icons/s2-icon-key-20-n.svg#icon |
The plugin then appears in your project's DA sidebar, letting you apply or remove page access for the closed-user-groups sheet directly from the authoring UI. See Removing Page Access below for the removal side.
Removing Page Access
When you remove page access defined in the closed-user-groups sheet:
- All CUG headers are removed from the Config Service for your site.
- Every previously protected page becomes publicly accessible.
- Non-CUG headers (e.g., CORS headers) are preserved. Only the
x-aem-cug-required,x-aem-cug-groups, andx-aem-cug-login-pathheaders are removed.
To re-enable protection, simply apply the page access again.
Warning: Removing page access affects the entire site at once. There is no way to selectively remove protection for individual paths. Either all rules are active or none are. To remove protection for a specific path, delete or modify that row in the sheet and re-apply page access.
For AEM Authoring
Removing page access is a built-in, out-of-the-box capability. No custom tooling is required. Simply unpublish the closed-user-groups sheet as you would any other page.
For Document Authoring (DA)
For Document Authoring (DA), removing page access is handled by the same CUG plugin referenced under the section Applying Page Access above. Use it to remove page access directly from the authoring UI.
CDN Requirement
CUG headers are set by AEM or DA in the Config Service, but enforcement happens at the CDN edge. Your CDN must have an edge worker (or equivalent) configured to:
- Read the
x-aem-cug-requiredheader. If it istrue, verify the visitor is logged in. - Read the
x-aem-cug-groupsheader. If it is present, verify the visitor belongs to at least one of the listed groups. - Read the
x-aem-cug-login-pathheader. If it is present, redirect unauthenticated visitors to that path instead of a default login page. - Redirect unauthenticated visitors to a login page, or return a 403/401 for unauthorized visitors.
- Strip the
x-aem-cug-*headers before the response reaches the browser. They must never leak to the client.
The reference and sample implementations below cover Cloudflare and Akamai. If you're on a different CDN, adapt the same pattern to your platform's edge-worker equivalent.
Edge Enforcement on Cloudflare
Reference implementation: adobe-rnd/aem-cloudflare-worker-with-cug is a minimal starter template for Cloudflare Workers. It strips the x-aem-cug-* headers before they reach the browser, exposes a single isAuthorized(request, env, allowedGroups) extension point for plugging in your identity provider (it returns 401 by default until you implement it), and sets Cache-Control: private, no-store on authorized responses.
For a full working implementation, two sample workers are available:
- Sample implementation: aemsites/summit-portal/workers/cloudflare/cug-adobe-oauth-worker: This is a complete Adobe IMS OAuth 2.0 + PKCE login flow, with a signed JWT session cookie and KV-backed PKCE state. Groups are derived from the visitor's email domain.
- Sample implementation: aem-edge-cug/workers/cloudflare/frescopa-b2b-worker: This uses the same CUG header enforcement logic, but using a simple username/password login instead of Adobe IMS. This is useful as a starting point if you're not using SSO.
Edge Enforcement on Akamai
Sample implementation: aem-edge-cug/workers/akamai/cug-akamai-worker is a sample EdgeWorker implementing the same Adobe IMS OAuth 2.0 + PKCE flow as the Cloudflare sample above.
Note the key architectural difference from Cloudflare: an Akamai EdgeWorker's httpRequest can only reach hostnames served by the same Akamai property, so the IMS token exchange must be routed through a Property Manager rule rather than called directly. PKCE state is carried in a signed JWT state parameter instead of a key-value store.
Troubleshooting
| Problem | Solution |
| Protection not applied after editing the sheet | Make sure you applied the page access defined in the closed-user-groups sheet |
| Sheet is ignored / has no effect | Verify the sheet is at the site root (same level as index) |
| Specific rows have no effect | Check that the url starts with / and is not a duplicate |
| All content became public | The page access defined in the closed-user-groups sheet may have been removed. Re-apply it. |
| Protected pages still accessible | Verify your CDN edge worker is configured and reading the x-aem-cug-* headers |
| Edited a row but the change has no effect | Check whether that url appears in an earlier row too. Only the first occurrence of a duplicate path is used |
Getting Help
Please reach out to your Adobe team on Slack or Teams for assistance.