A year ago, one of our Episerver CMS customers decided to move their websites from on-premise hosting to Episerver DXC Azure hosting. They wanted to use their own Azure AD as authentication and authorization for content editors and admins. We, as a service provider, wanted to use Aspnet Identity for troubleshooting and support purposes. So the mixed mode authentication (Azure AD + Aspnet Identity) was the need.
Environment details:
CMS: Episerver multisite solution (10 websites - 13 languages)
Website Hosting: Episerver DXC Azure (single web app and single app pool serving 10 sites)
Azure AD: Customer had their own separate Azure cloud synched with their AD
Requirements:
Authentication & Authorization: Azure AD & Aspnet Identity
Preproduction and production should use customer's Azure AD
Integration and development environment could use temporary Microsoft accounts for testing.
http://local.alloy1.com/episerver should use Azure AD login
http://local.alloy1.com/cmslogin should use Aspnet identity login
Few things to note:
Without further ado, lets see it in action.
"replyUrlsWithType": [
{
"url": "http://local.alloy1.com:80//EPiServer",
"type": "Web"
},
{
"url": "https://local.alloy1.com:443//EPiServer",
"type": "Web"
},
{
"url": "http://local.alloy1.com:80",
"type": "Web"
},
{
"url": "http://local.alloy2.com:80//EPiServer",
"type": "Web"
},
{
"url": "https://local.alloy2.com:443//EPiServer",
"type": "Web"
},
{
"url": "http://local.alloy2.com:80",
"type": "Web"
}
]
"appRoles": [- Assign roles to AD users. The free test Azure account doesn't supports assigning Active Directory groups to users. So instead of assigning Roles to Groups and Groups to users, we will directly assign Roles to the user. Under "Azure Active Directory" > "Enterprise applications", click our newly registered app "MixedAuth-Local". Then click "Users and groups" and click "Add user".
{
"allowedMemberTypes": [
"User"
],
"description": "AzureWebAdmins can admin the sites",
"displayName": "AzureWebAdmins",
"id": "157180e6-f96d-46d0-91a7-b62d5b0ba6cb",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "AzureWebAdmins"
},
{
"allowedMemberTypes": [
"User"
],
"description": "AzureWebEditors can only see CMS editing module but they still need site specific edit permissions",
"displayName": "AzureWebEditors",
"id": "36d19f07-e418-4acd-9b7e-2b2680b9d308",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "AzureWebEditors"
},
{
"allowedMemberTypes": [
"User"
],
"description": "AzureAllSitesWebEditors can edit all websites",
"displayName": "AzureAllSitesWebEditors",
"id": "e0cb40bb-76e6-457d-a4a1-c85a023d7fb4",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "AzureAllSitesWebEditors"
},
{
"allowedMemberTypes": [
"User"
],
"description": "AzureVisitorGroup can manage visitor groups",
"displayName": "AzureVisitorGroup",
"id": "b2907ad2-9c58-43b6-86d6-7e02498a2338",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "AzureVisitorGroup"
},
{
"allowedMemberTypes": [
"User"
],
"description": "AzureSearchAdmins can manage and configure the Epi Find features",
"displayName": "AzureSearchAdmins",
"id": "b27307d7-d27e-45c5-b50f-4d83f4c38df5",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "AzureSearchAdmins"
},
{
"allowedMemberTypes": [
"User"
],
"description": "AzureAlloy1WebEditors can edit only Alloy1 site",
"displayName": "AzureAlloy1WebEditors",
"id": "fdb47c37-489e-43a0-9d8c-eb4c1a7ff339",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "AzureAlloy1WebEditors"
},
{
"allowedMemberTypes": [
"User"
],
"description": "AzureAlloy2WebEditors can edit only Alloy2 site",
"displayName": "AzureAlloy2WebEditors",
"id": "5e633fbd-61bd-4789-a9a1-68d64dbf5d13",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "AzureAlloy2WebEditors"
}
]
Microsoft.AspNet.Identity.Owin- Web.config changes. Modify your web.config as per below:
Microsoft.AspNet.Identity.Core
Microsoft.AspNet.Identity.EntityFramework
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security.Cookies
Microsoft.Owin.Security.OAuth
Microsoft.Owin.Security.OpenIdConnect
Microsoft.Owin
Microsoft.IdentityModel.Tokens
Microsoft.IdentityModel.Protocols.OpenIdConnect
Microsoft.IdentityModel.Logging
System.IdentityModel.Tokens.Jwt
<configuration>
<appSettings>
<add key="owin:AutomaticAppStartup" value="true" />
<add key="owin:AppStartup" value="EpiserverTest.Startup, EpiserverTest" />
<add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}" />
<add key="ida:Tenant" value="xxxxxxxxxx.onmicrosoft.com" />
<add key="ida:ClientId" value="This is Application(client) ID of MixedAuth-Local App" />
<add key="ida:PostLogoutRedirectUri" value="http://local.alloy1.com/CMSLogin/Signout" />
<add key="ida:Authority" value="common" />
</appSettings>
<system.web>
<authentication mode="None">
</authentication>
<membership>
<providers>
<clear />
</providers>
</membership>
<roleManager enabled="false">
<providers>
<clear />
</providers>
</roleManager>
</system.web>
<system.webServer>
<rewrite>
<rules>
<rule name="Redirect episerver logout url to our custom logout implementation" stopProcessing="true">
<match url="util/logout.aspx" ignoreCase="true" />
<action type="Redirect" url="/CMSLogin/Signout" />
</rule>
</rules>
</rewrite>
</system.webServer>
<episerver.framework>
<virtualRoles addClaims="true">
<providers>
<add name="Administrators" type="EPiServer.Security.WindowsAdministratorsRole, EPiServer.Framework" />
<add name="Everyone" type="EPiServer.Security.EveryoneRole, EPiServer.Framework" />
<add name="Authenticated" type="EPiServer.Security.AuthenticatedRole, EPiServer.Framework" />
<add name="Anonymous" type="EPiServer.Security.AnonymousRole, EPiServer.Framework" />
<add name="CmsAdmins" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="Administrators, WebAdmins, AzureWebAdmins" mode="Any" />
<add name="CmsEditors" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="WebEditors" mode="Any" />
<add name="Creator" type="EPiServer.Security.CreatorRole, EPiServer" />
<add name="PackagingAdmins" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="Administrators, WebAdmins" mode="Any" />
<add name="SearchAdmins" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="Administrators, WebAdmins, AzureSearchAdmins" mode="Any" />
<add name="VisitorGroupAdmins" type="EPiServer.Security.MappedRole, EPiServer" roles="AzureVisitorGroup, AzureWebAdmins, WebAdmins" mode="Any" />
<add name="SiteimproveAdmins" type="EPiServer.Security.MappedRole, EPiServer" roles="AzureSiteImproveGroup, AzureWebAdmins, WebAdmins" mode="Any" />
</providers>
</virtualRoles>
</episerver.framework>
<location path="EPiServer">
<system.web>
<authorization>
<allow roles="WebEditors, WebAdmins, Administrators, AzureWebAdmins, AzureWebEditors" />
<deny users="*" />
</authorization>
</system.web>
</location>
<location path="EPiServer/CMS/admin">
<system.web>
<authorization>
<allow roles="WebAdmins, Administrators, AzureWebAdmins" />
<deny users="*" />
</authorization>
</system.web>
</location>
<location path="Views/Plugins">
<system.web>
<authorization>
<allow roles="WebAdmins, WebEditors, Administrators" />
<deny users="*" />
</authorization>
</system.web>
</location>
</configuration>
using EPiServer.Cms.UI.AspNetIdentity;Register the route in route table (Global.asax.cs or wherever you have your routes)
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EpiserverTest;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Configuration;
using System.Globalization;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Helpers;
[assembly: OwinStartup(typeof(Startup))]
namespace EpiserverTest
{
public class Startup
{
private static readonly string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static readonly string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static readonly string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
private const string UrlLogout = "/CMSLogin/Signout";
private const string UrlLogin = "/Util/login.aspx";
public void Configuration(IAppBuilder app)
{
// Add CMS ASP.NET Identity
app.AddCmsAspNetIdentity<ApplicationUser>();
// Enable cookie authentication, used to store the claims between requests
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Use cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromHours(2.0),
LoginPath = new PathString(UrlLogin),
LogoutPath = new PathString(UrlLogout),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager<ApplicationUser>, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => manager.GenerateUserIdentityAsync(user)),
OnApplyRedirect = (context => context.Response.Redirect(context.RedirectUri)),
OnResponseSignOut = (context => context.Response.Redirect(UrlLogin))
}
});
//Enable OpenId authentication
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
RoleClaimType = ClaimTypes.Role
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Write(context.Exception.Message);
return Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
// Here you can change the return uri based on multisite
HandleMultiSiteReturnUrl(context);
// To avoid a redirect loop to the federation server send 403
// when user is authenticated but does not have access
if (context.OwinContext.Response.StatusCode == 401 &&
context.OwinContext.Authentication.User.Identity.IsAuthenticated)
{
context.OwinContext.Response.StatusCode = 403;
context.HandleResponse();
}
return Task.FromResult(0);
},
SecurityTokenValidated = (ctx) =>
{
var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri,
UriKind.RelativeOrAbsolute);
if (redirectUri.IsAbsoluteUri)
{
ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
}
// Storing role as SSO in claims dictionary. Useful when logging out user.
ctx.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Role, "SSO"));
//Sync user and the roles to EPiServer in the background
ServiceLocator.Current.GetInstance<ISynchronizingUserService>()
.SynchronizeAsync(ctx.AuthenticationTicket.Identity);
return Task.FromResult(0);
}
}
});
app.UseStageMarker(PipelineStage.Authenticate);
// If the application throws an antiforgery token exception like “AntiForgeryToken: A Claim of Type NameIdentifier or IdentityProvider Was Not Present on Provided ClaimsIdentity”
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}
private void HandleMultiSiteReturnUrl(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
// here you change the context.ProtocolMessage.RedirectUri to corresponding siteurl
// this is a sample of how to change redirecturi in the multi-tenant environment
if (context.ProtocolMessage.RedirectUri == null)
{
var currentUrl = HttpContext.Current.Request.Url;
context.ProtocolMessage.RedirectUri = new UriBuilder(
currentUrl.Scheme,
currentUrl.Host,
currentUrl.Port).ToString() + "/EPiServer";
}
}
}
}
public class EPiServerApplication : EPiServer.GlobalCreate CMSLoginController.cs (in Controllers/Login/CMSLoginController.cs) and paste below code:
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
//Tip: Want to call the EPiServer API on startup? Add an initialization module instead (Add -> New Item.. -> EPiServer -> Initialization Module)
Routes(RouteTable.Routes);
}
public void Routes(RouteCollection routes)
{
routes.MapRoute("Mixed Authentication", "CMSLogin/{action}/", new { controller = "CMSLogin", action = "Signin" });
}
}
using System;No need to create a view for CMSLoginController since we will redirect the user to Episerver's own authentication page.
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Configuration;
using Microsoft.Owin.Security;
using EpiserverTest.ViewModels.Pages;
using EPiServer.Shell.Security;
using EPiServer.ServiceLocation;
using Microsoft.Owin.Security.OpenIdConnect;
using Microsoft.Owin.Security.Cookies;
using System.Web.Security;
using EPiServer.Security;
using System.Security.Claims;
using EPiServer.Web.Routing;
using EPiServer.Core;
namespace EpiserverTest.Controllers.Login
{
public class CMSLoginController : Controller
{
private readonly string loginUrl = "/Util/login.aspx";
private readonly UISignInManager _uISignInManager;
public CMSLoginController(UISignInManager uISignInManager)
{
_uISignInManager = uISignInManager;
}
// GET: Login
[HttpGet]
[Route("CMSLogin/Signin/{ReturnUrl}")]
public ActionResult Signin(string ReturnUrl)
{
return Redirect(loginUrl);
}
[HttpGet]
[Route("CMSLogin/Signout")]
public ActionResult Signout()
{
if (HttpContext.GetOwinContext().Authentication.User.HasClaim(x => x.Type.Equals(ClaimTypes.Role) && x.Value.Equals("SSO") )) // if SSO user
{
HttpContext.GetOwinContext().Authentication.SignOut(
OpenIdConnectAuthenticationDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType);
return Redirect(UrlResolver.Current.GetUrl(ContentReference.StartPage));
}
else // if Aspnet user
{
_uISignInManager.SignOut();
return Redirect(loginUrl);
}
}
}
}