Force Login to Optimizely DXP Environments using an Authorization Filter
When working with sites deployed to the Optimizely DXP, you may want to restrict access to the site in a particular environment to only authenticated users. This is useful when you're working on a site that's not yet live, and you don't want outside visitors to see the "work in progress" version of the site. I also see this requirement quite often for the non-production environments (integration and preproduction), which are commonly used for testing or training.
One way to lock down these environments is by simply updating the website's access rights in the environment's Settings (also known as Admin Mode), which is stored in the database tied to that environment. However, if you often synchronize the content from the production environment to the non-production environments, these settings will get overwritten and you'll continually have to remember to update them again.
A better way to achieve this is by creating an authorization filter that forces users to log in before they can access the site, and to use this filter in the DXP environments where you want to restrict access.
Creating the Authorization Filter
To create the authorization filter, you'll need to create a class that implements the IAuthorizationFilter
interface. This interface has a single method, OnAuthorization
, that needs to be implemented.
We'll also take this a step further by allowing the filter to optionally only allow certain roles.
public class EnvironmentAuthorizationFilter : IAuthorizationFilter
{
private readonly string[] _roleNames;
public EnvironmentAuthorizationFilter()
: this(string.Empty)
{
}
public EnvironmentAuthorizationFilter(string roleName)
: this(new string[] { roleName })
{
}
public EnvironmentAuthorizationFilter(string[] roleNames)
{
_roleNames = roleNames.Where(str => !string.IsNullOrEmpty(str)).ToArray();
}
public void OnAuthorization(AuthorizationFilterContext context)
{
/*
* Depending on why they failed authorization, the authorization filter will return
* one of two different types of IActionResult:
*
- ChallengeResult — This indicates that the user was not authorized to execute
the action because they weren't yet logged in.
- ForbidResult — This indicates that the user was logged in but didn't meet
the requirements to execute the action. They didn't have a required
claim, for example.
*/
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (controllerActionDescriptor != null)
{
// Allow controllers or actions with [AllowAnonymous] to bypass the check
if (controllerActionDescriptor.ControllerTypeInfo.IsDefined(typeof(AllowAnonymousAttribute), false) ||
controllerActionDescriptor.MethodInfo.IsDefined(typeof(AllowAnonymousAttribute), false))
{
return;
}
// EPiServer.Cms.Shell.UI.Controllers.Internal.AccountController is the out-of-the-box login screen.
// If you are using another account login controller, you'll need to update this,
// otherwise it will result in an infinite redirect loop.
if (controllerActionDescriptor.ControllerTypeInfo.Equals(typeof(AccountController)))
{
return;
}
}
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new ChallengeResult();
return;
}
if (_roleNames.Length == 0)
{
return;
}
foreach (var roleName in _roleNames)
{
if (context.HttpContext.User.IsInRole(roleName))
{
return;
}
}
context.Result = new ForbidResult();
}
}
In the authorization filter, we're allowing controllers or actions with the [AllowAnonymous]
attribute to bypass the check. We needed to do this for custom API endpoints that we wanted to be accessible without authentication.
The filter also bypasses the check when the built-in Optimizely login screen is accessed. This is important because if the user isn't authenticated, they'll be redirected to the login screen. If the login screen is also protected, it will result in an infinite redirect loop.
Checking for the DXP Environment
To determine if the site is running in a specific DXP environment, we can use the IHostEnvironment
or IWebHostEnviroment
service, or we can get the environment name from the environment variables.
(Note: The IWebHostEnviroment
service is an extension of the IHostEnvironment
service, so it all works the same. I'm mentioning this because you'll commonly see the IWebHostEnviroment
service used in the Startup.cs
class, which is where the authorization filter will be registered.)
Here's some examples of how you can check for the environment:
// Get the environment name from the IHostEnvironment service
var environmentName = hostEnvironment.EnvironmentName;
// Get the environment name from the environment variables
var environmentName = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
// Check if the environment name is "Development"
hostEnvironment.IsEnvironment("Development");
// Check if the environment name is "Development" or "Production"
hostEnvironment.IsDevelopment();
hostEnvironment.IsProduction();
Let's take this one step further and create some extension methods to make it easier to check for specific DXP environments.
First, let's create an enum to represent the DXP environments:
public enum DxpEnvironment
{
None,
Integration,
Preproduction,
Production
}
Next, let's create some helper methods and some extension methods:
public static class DxpEnvironmentExtensions
{
public static bool IsDxpEnvironment()
{
return GetDxpEnvironment() != DxpEnvironment.None;
}
public static bool IsDxpEnvironment(DxpEnvironment dxpEnvironment)
{
return GetDxpEnvironment() == dxpEnvironment;
}
public static bool IsDxpEnvironment(this IWebHostEnvironment webHostEnvironment)
{
return webHostEnvironment.GetDxpEnvironment() != DxpEnvironment.None;
}
public static bool IsDxpEnvironment(this IWebHostEnvironment webHostEnvironment, DxpEnvironment dxpEnvironment)
{
return webHostEnvironment.GetDxpEnvironment() == dxpEnvironment;
}
public static DxpEnvironment GetDxpEnvironment()
{
DxpEnvironment dxpEnvironment;
var environmentName = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (!Enum.TryParse(environmentName, true, out dxpEnvironment))
{
dxpEnvironment = DxpEnvironment.None;
}
return dxpEnvironment;
}
public static DxpEnvironment GetDxpEnvironment(this IWebHostEnvironment webHostEnvironment)
{
DxpEnvironment dxpEnvironment;
if (!Enum.TryParse(webHostEnvironment.EnvironmentName, true, out dxpEnvironment))
{
dxpEnvironment = DxpEnvironment.None;
}
return dxpEnvironment;
}
}
Now that we have a way to check for the DXP environment, we can use this while registering the authorization filter in our Startup.cs
class.
Registering the Authorization Filter
To register the authorization filter, we need to configure the MvcOptions
to add the filter in the ConfigureServices
method in the Startup.cs
class.
If we only want to use this in the DXP's non-production environments, we'll check for the DXP environment by name before adding the filter:
// Locks down access to low-level DXP environments
if (_webHostEnvironment.IsDxpEnvironment(DxpEnvironment.Integration) || _webHostEnvironment.IsDxpEnvironment(DxpEnvironment.Preproduction))
{
services.Configure<MvcOptions>(options =>
{
// Only allow authorized users
options.Filters.Add(new EnvironmentAuthorizationFilter());
});
}
Or, if we want to lock down access to all DXP environments, we can just use the IsDxpEnvironment
extension method:
// Locks down access to all DXP environments
if (_webHostEnvironment.IsDxpEnvironment())
{
services.Configure<MvcOptions>(options =>
{
// Only allow authorized users
options.Filters.Add(new EnvironmentAuthorizationFilter());
});
}
Let's not forget that we can also optionally pass in a role name or an array of role names to the EnvironmentAuthorizationFilter
constructor. This will allow us to only allow users with specific roles to access the site.
// Only allow authorized users with the "WebAdmins" role
options.Filters.Add(new EnvironmentAuthorizationFilter("WebAdmins"));
// Only allow authorized users with the "WebAdmins" or "WebEditors" role
options.Filters.Add(new EnvironmentAuthorizationFilter(new string[] { "WebAdmins", "WebEditors" }));
And that's it! Now, only authenticated users will be able to access the site in the your DXP environments.