Creating autologin functionality for local development

When working on Optimizely websites for different clients it is sometimes cumbersome to keep track of all the credentials to use to login to the CMS. Especially when using different databases to test several scenarios, being up-to-date with the latest production data or if the website is using Azure B2C in production. For more information about Azure B2C authentication see our previous blogpost here.

Of course you can make use of a password manager for these situations but for fun’s sake we ignore that these solutions exist 🙂

With the situation described above in mind I created Owin middleware that logs you in the the Optimizely CMS when you navigate to the /episerver url for local development.

How the Owin middleware works

The middleware performs a few checks to verify that auto logging in the current site visitor should proceed:

  • The request is to /episerver.
  • The visitor is not logged in.
  • The request is a local network request (e.g.: when developing locally).

When all these conditions are met the current visitor is logged in as follows:

  • A list of all available users is retrieved from the database.
  • The user is converted into an identity.
  • The first identity with the role ‘WebAdmins’ is selected.
  • When a valid identity is found then the AuthenticationManger from the OwinContext is used to sign in the selected identity.
  • When this is done the visiting user is redirected to the CMS using the EPiServer.Configuration.Settings.Instance.UIUrl setting to make sure the authentication cookie is used.
using Marvelous.EpiServer.AutoLogin;

using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(Marvelous.EpiServer.AutoLogin.Startup))]

namespace Marvelous.EpiServer.AutoLogin;

public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseAutoLogin();
}
}
using System;

using Owin;

namespace Marvelous.EpiServer.AutoLogin;

public static class AutoLoginExtensions
{
public static IAppBuilder UseAutoLogin(this IAppBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}

app.Use(typeof(AutoLoginMiddleware));

return app;
}
}
using System;

using System.Linq;
using System.Net;
using System.Threading.Tasks;
using EPiServer.Cms.UI.AspNetIdentity;
using EPiServer.Security;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;

namespace Marvelous.EpiServer.AutoLogin;

public class AutoLoginMiddleware : OwinMiddleware
{
public AutoLoginMiddleware(OwinMiddleware next)
: base(next)
{
}

public override async Task Invoke(IOwinContext context)
{
if (context.Request.Uri.ToString().IndexOf("episerver", 0, StringComparison.InvariantCultureIgnoreCase) == -1 &&
context.Request.Uri.ToString().IndexOf("cms", 0, StringComparison.InvariantCultureIgnoreCase) == -1 &&
context.Request.Uri.ToString().IndexOf("/util/login.aspx", 0, StringComparison.InvariantCultureIgnoreCase) == -1)
{
await Next.Invoke(context);
return;
}

if (PrincipalInfo.Current.Principal.Identity.IsAuthenticated)
{
await Next.Invoke(context);
return;
}

if (!RequestIsLocal(context.Request))
{
await Next.Invoke(context);
return;
}

if (!PrincipalInfo.Current.Principal.Identity.IsAuthenticated)
{
var userManager = context.GetUserManager<ApplicationUserManager<ApplicationUser>>(); // Get the user manager.

foreach (var persistedUser in userManager.Users) // To through all users.
{
var persistedIdentity = await userManager.CreateIdentityAsync(persistedUser, DefaultAuthenticationTypes.ApplicationCookie); // Create an identity with the current user.
if (persistedIdentity.Claims.Any(x =>
x.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" &&
x.Value == "WebAdmins")) // If the user is a 'webadmin', the user with the most rights, also edit rights then use this user.
{
var authenticationManager = context.Authentication; // Get the authentication manager.
authenticationManager.SignIn(new AuthenticationProperties { IsPersistent = true }, persistedIdentity); // Sign in with the created identity.

context.Response.Redirect(EPiServer.Configuration.Settings.Instance.UIUrl.OriginalString.TrimStart('~')); // Redirect to the requested login to be sure the cookie has been set and when used the EpiServer vignette is shown.
break;
}
}
}

await Next.Invoke(context);
}

private bool RequestIsLocal(IOwinRequest request)
{
var remoteAddress = request.RemoteIpAddress;

if (IsLocal(request) ||
remoteAddress != null && remoteAddress.StartsWith("192.168.", StringComparison.OrdinalIgnoreCase) || // Local IP range.
remoteAddress != null && remoteAddress.StartsWith("172.16.", StringComparison.OrdinalIgnoreCase) || // Local IP range.
remoteAddress != null && remoteAddress.StartsWith("10.", StringComparison.OrdinalIgnoreCase)) // Local IP range.
{
return true;
}

return false;
}

public bool IsLocal(IOwinRequest request)
{
if (!string.IsNullOrEmpty(request.RemoteIpAddress) && !string.IsNullOrEmpty(request.LocalIpAddress))
{
if (request.RemoteIpAddress.Equals(request.LocalIpAddress)) // Is local is same as remote, then we are local.
{
return true;
}

return IPAddress.IsLoopback(IPAddress.Parse(request.RemoteIpAddress)); // Is local when the remote is a loopback address.
}

return false;
}
}

Disclamer

The code provided herewith is offered as-is, without any warranties or guarantees of any kind, expressed or implied. It is intended for educational and experimental purposes only and should not be used in any production or critical system.

By utilizing the code, you acknowledge and agree that:

  • The code may contain errors, bugs, or other defects that could lead to system failures or data loss.
  • No warranty or support is provided, and the code is not guaranteed to be fit for any particular purpose.
  • The author and any contributors to the code are not liable for any damages, losses, or liabilities arising out of the use or inability to use the code.
  • Using the code in production environments, critical systems, or any context where failure could result in harm or financial loss is strictly discouraged. It is recommended to thoroughly review, test, and understand the code before considering any deployment.

Remember that programming is inherently risky, and caution should always be exercised when using code from any source, including this one.

By using this code, you accept all associated risks and responsibilities. If you do not agree with these terms, refrain from using the provided code.