EPiServer OWIN - 调试 401 问题(角色无法识别?)

问题描述 投票:0回答:1

尽管已通过身份验证并拥有有效的角色,但在确定为何收到 401 错误时遇到了一些困难。对于上下文,我正在运行:

  • .NET 4.6.2
  • Episerver CMS11 和 Commerce13
  • OWIN 和 OpenIdConnect 3.0.1

我的站点正在运行两个独立的

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
                }
            });
        }
openid-connect owin episerver .net-4.6.2
1个回答
0
投票

好吧 - 找到答案了。

第一块拼图位于

<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;
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.