尽管已通过身份验证并拥有有效的角色,但在确定为何收到 401 错误时遇到了一些困难。对于上下文,我正在运行:
我的站点正在运行两个独立的
UseOpenIdConnectAuthentication
实例 - 一个用于使用 Azure AD B2C 处理前端身份验证,另一个用于使用 Azure AD 处理后端。前端工作正常(可能是因为我们没有使用基于角色的身份验证)。后者似乎不起作用。
身份验证部分可以正常工作。直接导航到
/episerver
会引发错误 Error message 401.2.: Unauthorized: Logon failed due to server configuration
,并且不会被 RedirectToIdentityProvider
块捕获。导航到 ConfigurationManager.AppSettings["AAD.LoginPath"]
中配置的路径正确地被 app.Use
块捕获,然后将我定向到 Azure AD 页面进行身份验证,然后正确地将我重定向到 /episerver
。然而,这一次它并没有给出前面提到的 401.2
,而是击中了 RedirectToIdentityProvider
块,因为它返回了 401
。取消注释试图捕获 app.Use
的 /episerver
块不会改变第一个场景中的任何内容,尽管它确实在第二个场景中捕获,但它没有解决任何问题。
在我看来,问题的根源在于它没有识别该角色,尽管经过身份验证的用户对该角色具有有效的声明 - 即由这部分处理:
<add name="Administrators" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="Administrators" mode="Any" />
请参阅下面我的
web.config
和 Startup.cs
文件的相关部分。如果我还能提供任何其他信息来帮助确定问题,请告诉我。谢谢!
<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
}
});
}
好吧 - 找到答案了。
第一块拼图位于
<pages>
的 <location path="episerver">
部分 - 我必须设置 validateRequest="false"
以防止网站使用其本机验证机制,然后删除 <authorization>
部分以防止它使用其本机授权机制。
完成后,我就能够使用传统的
/episerver
捕获针对 IAppBuilder.Map
的请求。从那里,我修改了方法以改为使用 MapWhen
,并捕获针对 /episerver
的请求,这些请求要么未经身份验证,要么在没有正确声明的情况下经过身份验证。
当时我使用
IOwinContext.Challenge
将用户引导到 Azure AD 进行身份验证,然后当他们返回时,他们就能够正确访问后端。
这是完整的代码
public class Startup
{
public void Configuration(IAppBuilder app)
{
// 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.MapWhen(ctx => FrontendNeedsAuthentication(ctx, _aadb2cSettings.StorefrontLoginPath), map =>
{
map.Run(async 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);
});
});
app.MapWhen(ctx => FrontendNeedsAuthentication(ctx, _aadb2cSettings.StorefrontPasswordResetPath), map =>
{
map.Run(context =>
{
context.Set("Policy", _aadb2cSettings.ResetPasswordPolicyId);
var authenticationProperties = new AuthenticationProperties();
var redirectUrl = context.Request.Query.GetValues("returnUrl");
if (redirectUrl != null)
authenticationProperties.RedirectUri = redirectUrl[0];
else
authenticationProperties.RedirectUri = _aadb2cSettings.StorefrontAccountInformationPath;
context.Authentication.Challenge(authenticationProperties, AuthenticationType.Storefront);
return Task.CompletedTask;
});
});
app.Map(_aadb2cSettings.StorefrontLogoutPath, map =>
{
map.Run(context =>
{
_userService.SignOut();
return Task.CompletedTask;
});
});
app.MapWhen(ctx => BackendNeedsAuthentication(ctx), map =>
{
map.Run(context =>
{
context.Authentication.Challenge(new AuthenticationProperties(), AuthenticationType.Cms);
return Task.CompletedTask;
});
});
app.Map(_aadSettings.LoginPath, map =>
{
map.Run(context =>
{
var authenticationProperties = new AuthenticationProperties {
RedirectUri = "/episerver"
};
context.Authentication.Challenge(authenticationProperties, AuthenticationType.Cms);
return Task.CompletedTask;
});
});
app.Map(_aadSettings.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 domain = _aadb2cSettings.CustomDomain ?? $"{_aadb2cSettings.TenantName}.b2clogin.com";
var clientId = _aadb2cSettings.ClientId;
var authority = $"https://{domain}/{_aadb2cSettings.Tenant}/{_aadb2cSettings.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 = _aadb2cSettings.StorefrontRedirectUri,
PostLogoutRedirectUri = _aadb2cSettings.StorefrontPostLogoutRedirectUri,
ProtocolValidator = new OpenIdConnectProtocolValidator()
{
RequireNonce = false
},
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
NameClaimType = "name"
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = async (context) => await OrgUserAuthenticationFailed(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)
{
var AADRedirectUri = Settings.Instance.EnableScheduler ?
(_aadSettings.SchedulerRedirectUri ?? ConfigurationManager.AppSettings["AAD.AppScheduler.RedirectUri"]) :
_aadSettings.RedirectUri;
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
{
AuthenticationType = AuthenticationType.Cms,
ClientId = _aadSettings.ClientId,
Authority = _aadSettings.Authority,
RedirectUri = AADRedirectUri,
PostLogoutRedirectUri = _aadSettings.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 &&
!HasBackendClaim(context.OwinContext.Authentication.User.Identity as ClaimsIdentity))
{
context.OwinContext.Response.StatusCode = 403;
context.HandleResponse();
}
if (context.OwinContext.Response.StatusCode == 401 &&
IsXhrRequest(context.OwinContext.Request))
context.HandleResponse();
return Task.CompletedTask;
},
SecurityTokenValidated = OnSecurityTokenValidated
}
});
}
private async Task OrgUserAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> ctx)
{
var authenticationProperties = new AuthenticationProperties
{
RedirectUri = $"{EPiServer.Web.SiteDefinition.Current.SiteUrl}/login/handleloginfailed?returnUrl=/"
};
ctx.OwinContext.Authentication?.SignOut(authenticationProperties, CookieAuthenticationDefaults.AuthenticationType, AuthenticationType.Storefront);
}
private async Task OrgUserSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> ctx)
{
// skip this for reset password, because we do't need to renew the login
if (!string.Equals(ctx.AuthenticationTicket.Identity.GetTfpClaim(), _aadb2cSettings.ResetPasswordPolicyId, StringComparison.OrdinalIgnoreCase))
{
// stuff
}
}
private async Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> ctx)
{
var synchronizingUserService = ServiceLocator.Current.GetInstance<ISynchronizingUserService>();
await synchronizingUserService.SynchronizeAsync(ctx.AuthenticationTicket.Identity);
}
private static bool IsXhrRequest(IOwinRequest request)
{
const string xRequestedWith = "X-Requested-With";
var query = request.Query;
if ((query != null) && (query[xRequestedWith] == "XMLHttpRequest"))
{
return true;
}
var headers = request.Headers;
return (headers != null) && (headers[xRequestedWith] == "XMLHttpRequest");
}
private void HandleMultiSiteReturnUrl(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
if (context.ProtocolMessage.RedirectUri == null)
{
var currentUrl = EPiServer.Web.SiteDefinition.Current.SiteUrl;
context.ProtocolMessage.RedirectUri = new UriBuilder(currentUrl.Scheme,
currentUrl.Host,
currentUrl.Port,
HttpContext.Current.Request.Url.AbsolutePath).ToString();
}
}
private bool FrontendNeedsAuthentication(IOwinContext ctx, string storefrontPath)
{
bool isStorefrontPath = ctx.Request.Path.Value.ToLower().StartsWith(storefrontPath);
if (!isStorefrontPath)
return false;
else if (ctx.Request.ContentType == "application/csp-report")
return false;
else if (ctx.Request.Method.ToLower() == "options")
return false;
else
return true;
}
private bool BackendNeedsAuthentication(IOwinContext ctx)
{
bool isAuthenticated = ctx.Authentication.User.Identity.IsAuthenticated;
bool isEpiserverPath = ctx.Request.Path.Value.ToLower().StartsWith("/episerver");
if (!isEpiserverPath)
return false;
else if (!isAuthenticated || !(ctx.Authentication.User.Identity is ClaimsIdentity claimsIdentity))
return true;
else
return HasBackendClaim(claimsIdentity);
}
private bool HasBackendClaim(ClaimsIdentity claimsIdentity)
{
return !claimsIdentity?.Claims?.Any(claim => claim.Type == "aud" &&
claim.Value == _aadSettings.ClientId) ?? false;
}
}