Having some trouble identifying why I'm getting 401 errors despite having authenticated and having the valid roles. For context, I'm running:
- .NET 4.6.2
- Episerver CMS11 and Commerce13
- OWIN and OpenIdConnect 3.0.1
My site is running two separate UseOpenIdConnectAuthentication instances - one to handle front-end authentication using Azure AD B2C and one for back-end using Azure AD. The front-end one works properly (possibly because we aren't using role-based authentication). The latter doesn't seem to be working.
The authentication part works properly - sort of. Navigating directly to /episerver throws the error Error message 401.2.: Unauthorized: Logon failed due to server configuration, and does not get caught by the RedirectToIdentityProvider block. Navigating to the path configured in ConfigurationManager.AppSettings["AAD.LoginPath"] properly gets caught by the app.Use block, and then directs me to the Azure AD page for authenticating, and properly redirects me to /episerver thereafter. However, this time rather than giving the aforementioned 401.2, it hits the RedirectToIdentityProvider block, because it is returning a 401. Uncommenting the app.Use block which tried to catch /episerver does not change anything in the first scenario, and though it does catch in the second scenario, it doesn't solve anything.
The root of the problem seems to me that it isn't recognizing the role, despite the authenticated user having valid claims for the role - namely, handled by this part: <add name="Administrators" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="Administrators" mode="Any" />
Please see below for the relevant parts of my web.config and Startup.cs files. If there's anything else I can provide that would help identify the problem, please let me know. Thanks!
<system.web>
<authentication mode="None" />
<membership>
<providers>
<clear />
</providers>
</membership>
<roleManager enabled="false">
<providers>
<clear />
</providers>
</roleManager>
<anonymousIdentification enabled="true" />
</system.web>
<episerver.framework createDatabaseSchema="true" updateDatabaseSchema="true">
<appData basePath="App_Data" />
<scanAssembly forceBinFolderScan="true" />
<securityEntity>
<providers>
<add name="SynchronizingProvider" type="EPiServer.Security.SynchronizingRolesSecurityEntityProvider, EPiServer" />
</providers>
</securityEntity>
<virtualRoles addClaims="true">
<providers>
<add name="Administrators" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="Administrators" mode="Any" />
...
</providers>
</virtualRoles>
<virtualPathProviders>
<clear />
<add name="ProtectedModules" virtualPath="~/EPiServer/" physicalPath="Modules\_Protected" type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider, EPiServer.Framework.AspNet" />
</virtualPathProviders>
</episerver.framework>
<location path="Modules/_Protected">
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<handlers>
<clear />
<add name="BlockDirectAccessToProtectedModules" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
</system.webServer>
</location>
<location path="episerver">
<system.web>
<httpRuntime maxRequestLength="1000000" requestValidationMode="2.0" />
<pages enableEventValidation="true" enableViewState="true" enableSessionState="true" enableViewStateMac="true">
<controls>
<add tagPrefix="EPiServerUI" namespace="EPiServer.UI.WebControls" assembly="EPiServer.UI" />
<add tagPrefix="EPiServerScript" namespace="EPiServer.ClientScript.WebControls" assembly="EPiServer.Cms.AspNet" />
<add tagPrefix="EPiServerScript" namespace="EPiServer.UI.ClientScript.WebControls" assembly="EPiServer.UI" />
</controls>
</pages>
<globalization requestEncoding="utf-8" responseEncoding="utf-8" />
<authorization>
<allow roles="WebEditors, WebAdmins, Administrators, UHCSiteEditors" />
<deny users="*" />
</authorization>
</system.web>
<system.webServer>
<handlers>
<clear />
<add name="AssemblyResourceLoader-Integrated-4.0" path="WebResource.axd" verb="GET,DEBUG" type="System.Web.Handlers.AssemblyResourceLoader" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="PageHandlerFactory-Integrated-4.0" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="SimpleHandlerFactory-Integrated-4.0" path="*.ashx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.SimpleHandlerFactory" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="WebServiceHandlerFactory-Integrated-4.0" path="*.asmx" verb="GET,HEAD,POST,DEBUG" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="svc-Integrated-4.0" path="*.svc" verb="*" type="System.ServiceModel.Activation.ServiceHttpHandlerFactory, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="wildcard" path="*" verb="*" type="EPiServer.Web.StaticFileHandler, EPiServer.Framework.AspNet" />
</handlers>
</system.webServer>
</location>
[assembly: OwinStartup(typeof(Startup))]
...
// necessary to get HttpContext to work in SecurityTokenValidated
app.Use((context, next) =>
{
var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
httpContext.SetSessionStateBehavior(SessionStateBehavior.Required);
return next();
});
app.UseStageMarker(PipelineStage.MapHandler);
// must come AFTER the above
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
ConfigureOrgUserAuthentication(app);
ConfigureOptiBackendAuthentication(app);
app.Map(AzureADB2CSettings.StorefrontLoginPath, map =>
{
map.Run(context =>
{
var authenticationProperties = new AuthenticationProperties();
var redirectUrl = context.Request.Query.GetValues("returnUrl");
if (redirectUrl != null)
authenticationProperties.RedirectUri = redirectUrl[0];
context.Authentication.Challenge(authenticationProperties, AuthenticationType.Storefront);
return Task.CompletedTask;
});
});
app.Map(AzureADB2CSettings.StorefrontPasswordResetPath, map =>
{
map.Run(context =>
{
context.Set("Policy", AzureADB2CSettings.ResetPasswordPolicyId);
var authenticationProperties = new AuthenticationProperties();
var redirectUrl = context.Request.Query.GetValues("returnUrl");
if (redirectUrl != null)
authenticationProperties.RedirectUri = redirectUrl[0];
else
authenticationProperties.RedirectUri = AzureADB2CSettings.StorefrontAccountInformationPath;
context.Authentication.Challenge(authenticationProperties, AuthenticationType.Storefront);
return Task.CompletedTask;
});
});
app.Map(AzureADB2CSettings.StorefrontLogoutPath, map =>
{
map.Run(context =>
{
context.Authentication?.SignOut(CookieAuthenticationDefaults.AuthenticationType, AuthenticationType.Storefront);
_userService.SignOut();
return Task.CompletedTask;
});
});
//app.Map("/episerver", map =>
//{
// map.Run(context =>
// {
// context.Authentication.Challenge(new AuthenticationProperties(), AuthenticationType.Cms);
// return Task.CompletedTask;
// });
//});
app.Map(ConfigurationManager.AppSettings["AAD.LoginPath"], map =>
{
map.Run(context =>
{
var authenticationProperties = new AuthenticationProperties {
RedirectUri = "/episerver"
};
context.Authentication.Challenge(authenticationProperties, AuthenticationType.Cms);
return Task.CompletedTask;
});
});
app.Map(ConfigurationManager.AppSettings["AAD.LogoutPath"], map =>
{
map.Run(context =>
{
context.Authentication?.SignOut(CookieAuthenticationDefaults.AuthenticationType, AuthenticationType.Cms);
return Task.CompletedTask;
});
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
private void ConfigureOrgUserAuthentication(IAppBuilder app)
{
var clientId = AzureADB2CSettings.ClientId;
var authority = $"https://{AzureADB2CSettings.TenantName}.b2clogin.com/{AzureADB2CSettings.Tenant}/{AzureADB2CSettings.SignUpSignInPolicyId}/v2.0/";
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(authority))
return;
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
{
AuthenticationType = AuthenticationType.Storefront,
ClientId = clientId,
Authority = authority,
SignInAsAuthenticationType = AuthenticationType.Storefront,
Scope = OpenIdConnectScopes.OpenId,
ResponseType = OpenIdConnectResponseTypes.CodeIdToken,
RedirectUri = AzureADB2CSettings.RedirectUri,
PostLogoutRedirectUri = AzureADB2CSettings.PostLogoutRedirectUri,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
NameClaimType = "name"
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = async (context) => await _openIdConnectAuthenticationNotificationsService.HandleAuthenticationFailed(context),
AuthorizationCodeReceived = async (context) => await _openIdConnectAuthenticationNotificationsService.HandleAuthorizationCodeReceived(context),
MessageReceived = async (context) => await _openIdConnectAuthenticationNotificationsService.HandleMessageReceived(context),
RedirectToIdentityProvider = async (context) => await _openIdConnectAuthenticationNotificationsService.HandleRedirectToIdentityProvider(context),
SecurityTokenReceived = async (context) => await _openIdConnectAuthenticationNotificationsService.HandleSecurityTokenReceived(context),
SecurityTokenValidated = async (context) => await OrgUserSecurityTokenValidated(context),
}
});
}
private void ConfigureOptiBackendAuthentication(IAppBuilder app)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
{
AuthenticationType = AuthenticationType.Cms,
ClientId = ConfigurationManager.AppSettings["AAD.ClientId"],
Authority = ConfigurationManager.AppSettings["AAD.AADAuthority"],
RedirectUri = ConfigurationManager.AppSettings["AAD.RedirectUri"],
PostLogoutRedirectUri = ConfigurationManager.AppSettings["AAD.PostLogoutRedirectUri"],
SignInAsAuthenticationType = AuthenticationType.Cms,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "preferred_username",
RoleClaimType = ClaimTypes.Role
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Write(context.Exception.Message);
return Task.CompletedTask;
},
RedirectToIdentityProvider = context =>
{
HandleMultiSiteReturnUrl(context);
if (context.OwinContext.Response.StatusCode == 401 &&
context.OwinContext.Authentication.User.Identity.IsAuthenticated)
{
context.OwinContext.Response.StatusCode = 403;
context.HandleResponse();
}
if (context.OwinContext.Response.StatusCode == 401 &&
IsXhrRequest(context.OwinContext.Request))
context.HandleResponse();
return Task.CompletedTask;
},
SecurityTokenValidated = OnSecurityTokenValidated
}
});
}
Alright - figured out the answer.
The first piece of the puzzle was in the
<pages>section of<location path="episerver">- I had to setvalidateRequest="false"in order to prevent the site from using its native validation mechanisms, and then remove the<authorization>section to prevent it from using its native authorization mechanisms.Once that was done, I was able to capture the requests against
/episerverusing a traditionalIAppBuilder.Map. From there, I modified the approach to instead useMapWhen, and captured requests against/episerverthat were either unauthenticated, or were authenticated without the correct claims.At that point I used
IOwinContext.Challengeto direct the user to Azure AD for authentication, and then when they returned, they were able to access the back-end properly.EDIT
Here's the complete(ish) code