Sanjay Kumar
OMVPOpal Tools CertificationConfigured Commerce Developer Certification +3
Aug 28, 2026
visibility 642
star star star star star
(3 votes)

Beyond [Authorize]: Function-Level Permissions in Optimizely

Why basic role checks fall short and how Permissions to Functions unlock audience-based access control

The Problem with Basic Authorization

ASP.NET Core's [Authorize] attribute is the right starting point for access control. It answers a simple question: is this user allowed into the application at all?

For many endpoints, that is enough. But as business rules grow more specific, [Authorize] starts to show its limits:

  • It only checks whether the user is authenticated or belongs to a role or policy
  • It does not easily support dynamic business rules
  • It is not ideal for audience-based or context-based permissions
  • It becomes hard to manage when access depends on function-level rules

A familiar scenario

A user may be fully authenticated and even hold a valid CMS role yet still should not be allowed to perform a specific action.

Some features need access for a specific audience, not just a role name baked into code. That gap is where many Optimizely implementations start looking for a better pattern.

The Limitation of Built-In Permission Checks Alone

Optimizely exposes Permissions to Functions through PermissionType and PermissionService. That is a strong foundation: permissions are discoverable in the admin UI and can be assigned to users and roles.

However, relying only on the built-in authorization path has a gap of its own:

  • Role and group assignment works well
  • Visitor group / audience-based evaluation is not wired into a simple declarative attribute out of the box
  • Controllers end up with duplicated authorization logic, or hard-coded role lists that drift from CMS configuration

The result is often a mix of [Authorize(Roles = "...")] strings scattered across controllers, difficult to audit and painful to change when the business audience evolves.

The Solution: AuthorizePermission with Permissions to Functions

Instead of relying only on [Authorize], use a custom authorization attribute backed by Optimizely's Permissions to Functions model.

This approach lets you:

  • Control access at the function level
  • Evaluate business rules dynamically
  • Support audience-based permission checks (users, roles, and visitor groups)
  • Keep authorization logic reusable and centralized

Key difference at a glance

Approach What it checks  Best for
[Authorize] Authentication, roles, policies App-level and coarse-grained access
Permissions to Functions CMS-managed audience per permission Function-level, business-aligned access

 

[Authorize] = basic, framework-level access control
CustomAuthorizePermission = custom, function-level permission control

How It Works in Practice

The pattern has three parts:

  • Define permission types - discovered by Optimizely and shown under Config → Permissions to functions
  • Register virtual roles (optional) - map permissions to virtual roles for broader CMS integration
  • Apply a custom filter attribute - evaluate the current user against the permission audience at request time

Step 1: Define permission types

Create a static class decorated with [PermissionTypes]. Each PermissionType represents a function your application exposes, for example, viewing reports, managing settings, or accessing an admin area.

[PermissionTypes]
public static class AppPermissions
{
    public const string GroupName = "MyApplication";

    static AppPermissions()
    {
        ViewReports = new PermissionType(GroupName, nameof(ViewReports));
        ManageSettings = new PermissionType(GroupName, nameof(ManageSettings));
        AccessAdmin = new PermissionType(GroupName, nameof(AccessAdmin));
    }

    public static PermissionType ViewReports { get; private set; }
    public static PermissionType ManageSettings { get; private set; }
    public static PermissionType AccessAdmin { get; private set; }
}

Once deployed, these permissions appear in the Optimizely admin UI (CMS --> Settings --> Permissions for Functions). Admins can assign users and roles to each function without redeploying code.

Add localization so friendly names and descriptions appear in  Permissions for functions instead of raw type names. Create or update a language XML file (for example Resources/Localization/Views_en.xml):

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<languages>
  <language name="English" id="en">
    <admin>
      <permissiontype>
        <groups>
          <AppPermissions>
            <description>Functions in the application</description>
            <permissions>
              <ViewReports>Allows users to view reports</ViewReports>
              <ManageSettings>Allows users to manage application settings</ManageSettings>
              <AccessAdmin>Allows users to access the admin area</AccessAdmin>
            </permissions>
          </AppPermissions>
        </groups>
      </permissiontype>
    </admin>
  </language>
</languages>

Step 2: Register virtual roles (optional)

If you need permissions to participate in Optimizely's role system, for example, in content access rules or visitor group criteria, register a virtual role for each permission:


[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class AppVirtualRoleInitialization : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        var virtualRoleRepository = context.Locate.Advanced
            .GetInstance();

        virtualRoleRepository.Register("ViewReportsVirtualRole", new PermissionRole
        {
            Permission = AppPermissions.ViewReports
        });

        virtualRoleRepository.Register("ManageSettingsVirtualRole", new PermissionRole
        {
            Permission = AppPermissions.ManageSettings
        });

        // Register additional permissions as needed
    }

    public void Uninitialize(InitializationEngine context) { }
}

This step is optional. Skip it if you only need controller-level authorization via a custom attribute.

Step 3: Apply a custom authorization attribute

Create an IAsyncAuthorizationFilter that resolves the permission audience at request time. A typical implementation supports two evaluation paths:

By PermissionType - calls PermissionService.IsPermitted for direct function-level checks against users and roles assigned in the CMS

By PermissionName - resolves visitor groups assigned to the permission via PermissionRepository and evaluates them with IVisitorGroupRole.IsMatch


namespace MyApplication.Attributes
{
    using System;
    using System.Linq;
    using System.Threading.Tasks;

    using EPiServer.DataAbstraction;
    using EPiServer.Find.Helpers.Text;
    using EPiServer.Personalization.VisitorGroups;
    using EPiServer.Security;

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Filters;
    using Microsoft.Extensions.DependencyInjection;

    using MyApplication.Infrastructure.Security;

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
    public class AuthorizePermissionAttribute : ActionFilterAttribute, IAsyncAuthorizationFilter
    {
        public string PermissionName { get; set; }

        public PermissionType? PermissionType { get; set; }

        public AuthorizePermissionAttribute()
        {
        }

        public AuthorizePermissionAttribute(string name)
        {
            this.PermissionName = name;
        }

        public AuthorizePermissionAttribute(string groupName, string name)
        {
            this.PermissionType = new PermissionType(groupName, name);
        }

        public async Task OnAuthorizationAsync(AuthorizationFilterContext filterContext)
        {
            if (this.PermissionType == null && this.PermissionName.IsNullOrEmpty())
            {
                return;
            }

            if (this.PermissionName.IsNotNullOrEmpty())
            {
                await this.AuthorizeByPermissionNameAsync(filterContext);
                return;
            }

            this.AuthorizeByPermissionType(filterContext);
        }

        private async Task AuthorizeByPermissionNameAsync(AuthorizationFilterContext filterContext)
        {
            var httpContext = filterContext.HttpContext;
            var user = httpContext.User;

            var permissionRepository = httpContext.RequestServices.GetRequiredService();
            var visitorGroupRoleRepository = httpContext.RequestServices.GetRequiredService();

            var permissionType = GetPermissionType(this.PermissionName);
            var audiences = await permissionRepository.GetPermissionsAsync(permissionType);

            var visitorGroupNames = audiences
                .Where(x => x.EntityType == SecurityEntityType.VisitorGroup)
                .Select(x => x.Name)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();

            foreach (var visitorGroupName in visitorGroupNames)
            {
                var hasRole = visitorGroupRoleRepository.TryGetRole(visitorGroupName, out var visitorGroupRole);
                if (!hasRole)
                {
                    continue;
                }

                var isMatch = visitorGroupRole.IsMatch(user, httpContext);
                if (isMatch)
                {
                    return;
                }
            }

            filterContext.Result = new ChallengeResult();
        }

        private void AuthorizeByPermissionType(AuthorizationFilterContext filterContext)
        {
            var httpContext = filterContext.HttpContext;
            var user = httpContext.User;

            var permissionService = httpContext.RequestServices.GetRequiredService();
            var isPermitted = permissionService.IsPermitted(user, this.PermissionType);

            if (isPermitted)
            {
                return;
            }

            var isAuthenticated = user.Identity != null && user.Identity.IsAuthenticated;
            filterContext.Result = GetUnauthorizedResult(isAuthenticated);
        }

        private static IActionResult GetUnauthorizedResult(bool isAuthenticated)
        {
            if (!isAuthenticated)
            {
                return new ChallengeResult();
            }

            return new ForbidResult();
        }

        private static PermissionType? GetPermissionType(string permissionName)
        {
            switch (permissionName)
            {
                case nameof(AppPermissions.ViewReports):
                    return AppPermissions.ViewReports;
                case nameof(AppPermissions.ManageSettings):
                    return AppPermissions.ManageSettings;
                case nameof(AppPermissions.AccessAdmin):
                    return AppPermissions.AccessAdmin;
                default:
                    return null;
            }
        }
    }
} 

Apply the attribute on any controller or action that should be gated by a CMS-managed audience:


// By permission name evaluates visitor groups assigned to the permission
[AuthorizePermission(nameof(AppPermissions.ViewReports))]
public class ReportsController : Controller
{
    // Only users matching the ViewReports visitor group audience can access this
}

// By permission type evaluates users and roles via PermissionService
[AuthorizePermission("AppPermissions", nameof(AppPermissions.ManageSettings))]
public class SettingsController : Controller
{
    // Only users in the ManageSettings permission audience can access this
}

The permission group name and function name must match what you defined in Step 1. The audience users, roles, and optionally visitor groups is managed entirely in the Optimizely admin UI. 

 When to Use Each Approach

Scenario Recommended approach
Public or anonymous endpoints

[Authorize]

Any authenticated CMS user

[Authorize(Roles = "...")]

Fixed role-based access with no CMS audience

AuthorizePermission + PermissionTypes

Function-level access managed in Optimizely admin

AuthorizePermission with permission name resolution

Audience includes visitor groups

Custom attribute + hybrid auth filter

Benefits

More flexible than [Authorize]

Roles and policies are static contracts. Permissions to Functions are business contracts managed in the CMS and mapped to real audiences.

Easier to scale

As new features ship, add a PermissionType and an attribute. No more hunting through controllers for magic role strings.

Better alignment with business needs

Marketing, operations, and support teams can reason about who can access Orders or who can change organization context through the admin UI not through source code.

Cleaner separation of concerns

  • Authentication establishes identity
  • Authorization evaluates function-level audience membership
  • Controllers stay focused on business logic
Aug 28, 2026

Comments

error Please login to comment.
Latest blogs
Unit-Level Promotion Exclusions: Updated Behaviour and Developer Guide

Introduction Since Commerce 13, Optimizely Commerce Connect has supported two modes for promotion exclusion: order-level and unit-level. Order-leve...

Viet Anh Nguyen | Oct 23, 2026

Virtual Opticon 2026: Where AI Can Improve DXP Delivery

Consider a campaign whose first draft takes ten minutes to generate but then waits five days for review. Faster drafting has helped one task. The...

Augusto Davalos | Oct 21, 2026

The Low-Level API Is Not Automatically Fast. Part 4: Counting Round Trips

The low-level Commerce DTO API is not automatically fast: counting the database round trips per imported row, and four fixes I would apply first.

Stanisław Szołkowski | Oct 21, 2026 |

The Migration Bugs That Actually Caught Us Out

The less obvious issues we hit migrating Optimizely CMS 11 to CMS 12 and .NET 8, including routing changes, JSON model binding, package dependency...

Dom Reilly | Oct 20, 2026 |

Camp Opticon 26 (London): AI requires a different operating model

Introduction Last week I attended Camp Opticon 26 in London and although it was far from my first time at Opticon, it was in fact my first time tha...

WilliamP | Oct 20, 2026

Migrating ContentArea.FilteredItems in Optimizely CMS 13

If you have

Adnan Zameer | Oct 19, 2026 |