Azure AD B2C - 身份验证质询不触发身份验证

Azure AD B2C - Authentication Challenge not triggering authentication

我正在使用 Azure AD B2C,我有一个奇怪的行为。我按照这个示例创建了一个新应用程序:AzureADQuickStarts/B2C-WebApp-OpenIdConnect-DotNet ,它非常有用。

然后我将代码移植到现有的应用程序中,但遇到了问题。在控制器中,我有以下方法:

[PolicyAuthorize(Policy = "b2c_1_signin01")]
public ActionResult Index()
{
    var vm = new IndexModel
    {
        FundsDocumentsModel = new FundsDocumentsModel { DocumentTypes = this.DocumentTypes_ReadDictionary() }
    };

    if (this.FundId != Guid.Empty)
    {
        var data = new FinanceDataProvider();
        var fund = data.GetFundById(this.FundId);

        if (fund != null)
        {
            this.ViewBag.LocalSubTitle = "for " + fund.Name;
        }
    }

    return this.View("~/Areas/DataRoom/Views/Index.cshtml", vm);
}

PolicyAuthorize 属性的代码与上述示例相同:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class PolicyAuthorize : AuthorizeAttribute
{
    public string Policy { get; set; }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        filterContext.HttpContext.GetOwinContext().Authentication.Challenge(
                new AuthenticationProperties(
                    new Dictionary<string, string>
                    {
                        { Constants.POLICY_KEY, this.Policy }
                    })
                {
                    RedirectUri = "/",
                }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
    }
}

当我在调试中访问我的 Web 应用程序时,我会自动进入控制器的 Index 方法。然后我进入属性的HandleUnauthorizedRequest,调用Challenge方法。

但我没有被重定向到 B2C 登录页面。相反,调试器返回到 Index 方法,就像我已经过身份验证一样,这是我不想要的。

现在,如果我转到 /Account/SignIn(与示例应用程序中的实现相同),我将被重定向到 B2C 登录页面。

问题是在示例应用程序中,每当我使用 PolicyAuthorize 属性时,我都会被重定向到 B2C 登录页面。

所以我不明白这种差异是从哪里来的。你们有什么想法吗?

更新:

这里有更多代码,显示所有内容都已移植。

Startup.Auth.cs:

public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions());

        var options = new OpenIdConnectAuthenticationOptions
        {
            // These are standard OpenID Connect parameters, with values pulled from web.config
            ClientId = ConfigurationHelper.Authentication.CLIENT_ID,
            RedirectUri = ConfigurationHelper.Authentication.REDIRECT_URI,
            PostLogoutRedirectUri = ConfigurationHelper.Authentication.REDIRECT_URI,
            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                AuthenticationFailed = this.AuthenticationFailed,
                RedirectToIdentityProvider = this.OnRedirectToIdentityProvider
            },
            Scope = "openid",
            ResponseType = "id_token",

            // The PolicyConfigurationManager takes care of getting the correct Azure AD authentication
            // endpoints from the OpenID Connect metadata endpoint.  It is included in the PolicyAuthHelpers folder.
            ConfigurationManager = new PolicyConfigurationManager(
                string.Format(CultureInfo.InvariantCulture, ConfigurationHelper.Authentication.AAD_INSTANCE, ConfigurationHelper.Authentication.TENANT, "/v2.0", Constants.OIDC_METADATA_SUFFIX),
                new[] { ConfigurationHelper.Authentication.SIGNUP_POLICY_ID, ConfigurationHelper.Authentication.SIGNIN_POLICY_ID, ConfigurationHelper.Authentication.PROFILE_POLICY_ID }),

            // This piece is optional - it is used for displaying the user's name in the navigation bar.
            TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "name",
            },
        };

        app.UseOpenIdConnectAuthentication(options);

    }

    /// <summary>
    /// This notification can be used to manipulate the OIDC request before it is sent. Here we use it to send the correct policy.
    /// </summary>
    /// <param name="notification">The notification.</param>
    private async Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
    {
        PolicyConfigurationManager mgr = notification.Options.ConfigurationManager as PolicyConfigurationManager;
        if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
        {
            OpenIdConnectConfiguration config = await mgr.GetConfigurationByPolicyAsync(CancellationToken.None, notification.OwinContext.Authentication.AuthenticationResponseRevoke.Properties.Dictionary[Constants.POLICY_KEY]);
            notification.ProtocolMessage.IssuerAddress = config.EndSessionEndpoint;
        }
        else
        {
            OpenIdConnectConfiguration config = await mgr.GetConfigurationByPolicyAsync(CancellationToken.None, notification.OwinContext.Authentication.AuthenticationResponseChallenge.Properties.Dictionary[Constants.POLICY_KEY]);
            notification.ProtocolMessage.IssuerAddress = config.AuthorizationEndpoint;
        }
    }

    /// <summary>
    /// Used for avoiding yellow-screen-of-death
    /// </summary>
    /// <param name="notification">The notification.</param>
    private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
    {
        notification.HandleResponse();
        notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
        return Task.FromResult(0);
    }
}

HttpDocumentRetriever.cs:

public class HttpDocumentRetriever : IDocumentRetriever
{
    private readonly HttpClient _httpClient;

    public HttpDocumentRetriever()
        : this(new HttpClient())
    { }

    public HttpDocumentRetriever(HttpClient httpClient)
    {
        Guard.AgainstNullArgument(nameof(httpClient), httpClient);

        this._httpClient = httpClient;
    }

    public async Task<string> GetDocumentAsync(string address, CancellationToken cancel)
    {
        Guard.AgainstNullArgument(nameof(address), address);

        try
        {
            HttpResponseMessage response = await this._httpClient.GetAsync(address, cancel).ConfigureAwait(false);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            throw new IOException("Unable to get document from: " + address, ex);
        }
    }
}

PolicyConfigurationManager.cs:

// This class is a temporary workaround for AAD B2C,
// while our current libraries are unable to support B2C
// out of the box.  For the original source code (with comments)
// visit https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/master/src/Microsoft.IdentityModel.Protocol.Extensions/Configuration/ConfigurationManager.cs
public class PolicyConfigurationManager : IConfigurationManager<OpenIdConnectConfiguration>
{
    public static readonly TimeSpan DefaultAutomaticRefreshInterval = new TimeSpan(5, 0, 0, 0);

    public static readonly TimeSpan DefaultRefreshInterval = new TimeSpan(0, 0, 0, 30);

    public static readonly TimeSpan MinimumAutomaticRefreshInterval = new TimeSpan(0, 0, 5, 0);

    public static readonly TimeSpan MinimumRefreshInterval = new TimeSpan(0, 0, 0, 1);

    private const string policyParameter = "p";

    private TimeSpan _automaticRefreshInterval = DefaultAutomaticRefreshInterval;
    private TimeSpan _refreshInterval = DefaultRefreshInterval;
    private Dictionary<string, DateTimeOffset> _syncAfter;
    private Dictionary<string, DateTimeOffset> _lastRefresh;

    private readonly SemaphoreSlim _refreshLock;
    private readonly string _metadataAddress;
    private readonly IDocumentRetriever _docRetriever;
    private readonly OpenIdConnectConfigurationRetriever _configRetriever;
    private Dictionary<string, OpenIdConnectConfiguration> _currentConfiguration;

    public PolicyConfigurationManager(string metadataAddress, string[] policies)
        : this(metadataAddress, policies, new HttpDocumentRetriever())
    {
    }

    public PolicyConfigurationManager(string metadataAddress, string[] policies, IDocumentRetriever docRetriever)
    {
        if (string.IsNullOrWhiteSpace(metadataAddress))
        {
            throw new ArgumentNullException("metadataAddress");
        }

        if (docRetriever == null)
        {
            throw new ArgumentNullException("retriever");
        }

        _metadataAddress = metadataAddress;
        _docRetriever = docRetriever;
        _configRetriever = new OpenIdConnectConfigurationRetriever();
        _refreshLock = new SemaphoreSlim(1);
        _syncAfter = new Dictionary<string, DateTimeOffset>();
        _lastRefresh = new Dictionary<string, DateTimeOffset>();
        _currentConfiguration = new Dictionary<string, OpenIdConnectConfiguration>();

        foreach (string policy in policies)
        {
            _currentConfiguration.Add(policy, null);
        }
    }

    public TimeSpan AutomaticRefreshInterval
    {
        get { return _automaticRefreshInterval; }
        set
        {
            if (value < MinimumAutomaticRefreshInterval)
            {
                throw new ArgumentOutOfRangeException("value", value, string.Format(CultureInfo.InvariantCulture, ErrorMessages.IDX10107, MinimumAutomaticRefreshInterval, value));
            }
            _automaticRefreshInterval = value;
        }
    }

    public TimeSpan RefreshInterval
    {
        get { return _refreshInterval; }
        set
        {
            if (value < MinimumRefreshInterval)
            {
                throw new ArgumentOutOfRangeException("value", value, string.Format(CultureInfo.InvariantCulture, ErrorMessages.IDX10106, MinimumRefreshInterval, value));
            }
            _refreshInterval = value;
        }
    }

    // Takes the ohter and copies it to source, preserving the source's multi-valued attributes as a running sum.
    private OpenIdConnectConfiguration MergeConfig(OpenIdConnectConfiguration source, OpenIdConnectConfiguration other)
    {
        ICollection<SecurityToken> existingSigningTokens = source.SigningTokens;
        ICollection<string> existingAlgs = source.IdTokenSigningAlgValuesSupported;
        ICollection<SecurityKey> existingSigningKeys = source.SigningKeys;

        foreach (SecurityToken token in existingSigningTokens)
        {
            other.SigningTokens.Add(token);
        }

        foreach (string alg in existingAlgs)
        {
            other.IdTokenSigningAlgValuesSupported.Add(alg);
        }

        foreach (SecurityKey key in existingSigningKeys)
        {
            other.SigningKeys.Add(key);
        }

        return other;
    }

    // This non-policy specific method effectively gets the metadata for all policies specified in the constructor,
    // and merges their signing key metadata.  It selects the other metadata from one of the policies at random.
    // This is done so that the middleware can take an incoming id_token and validate it against all signing keys
    // for the app, selecting the appropriate signing key based on the key identifiers.
    public async Task<OpenIdConnectConfiguration> GetConfigurationAsync(CancellationToken cancel)
    {
        OpenIdConnectConfiguration configUnion = new OpenIdConnectConfiguration();
        Dictionary<string, OpenIdConnectConfiguration> clone = new Dictionary<string, OpenIdConnectConfiguration>(_currentConfiguration);
        foreach (KeyValuePair<string, OpenIdConnectConfiguration> entry in clone)
        {
            OpenIdConnectConfiguration config = await GetConfigurationByPolicyAsync(cancel, entry.Key);
            configUnion = MergeConfig(configUnion, config);
        }

        return configUnion;
    }

    public async Task<OpenIdConnectConfiguration> GetConfigurationByPolicyAsync(CancellationToken cancel, string policyId)
    {
        DateTimeOffset now = DateTimeOffset.UtcNow;

        DateTimeOffset sync;
        if (!_syncAfter.TryGetValue(policyId, out sync))
        {
            sync = DateTimeOffset.MinValue;
        }

        OpenIdConnectConfiguration config;
        if (!_currentConfiguration.TryGetValue(policyId, out config))
        {
            config = null;
        }

        if (config != null && sync > now)
        {
            return config;
        }

        await _refreshLock.WaitAsync(cancel);
        try
        {
            Exception retrieveEx = null;
            if (sync <= now)
            {
                try
                {
                    // We're assuming the metadata address provided in the constructor does not contain qp's
                    config = await OpenIdConnectConfigurationRetriever.GetAsync(String.Format(_metadataAddress + "?{0}={1}", policyParameter, policyId), _docRetriever, cancel);
                    _currentConfiguration[policyId] = config;
                    Contract.Assert(_currentConfiguration[policyId] != null);
                    _lastRefresh[policyId] = now;
                    _syncAfter[policyId] = now.UtcDateTime.Add(_automaticRefreshInterval);
                }
                catch (Exception ex)
                {
                    retrieveEx = ex;
                    _syncAfter[policyId] = now.UtcDateTime.Add(_automaticRefreshInterval < _refreshInterval ? _automaticRefreshInterval : _refreshInterval);
                }
            }

            if (config == null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, ErrorMessages.IDX10803, _metadataAddress ?? "null"), retrieveEx);
            }

            return config;
        }
        finally
        {
            _refreshLock.Release();
        }
    }

    public void RequestRefresh(string policyId)
    {
        DateTimeOffset now = DateTimeOffset.UtcNow;
        DateTimeOffset refresh;
        if (!_lastRefresh.TryGetValue(policyId, out refresh) || now >= _lastRefresh[policyId].UtcDateTime.Add(RefreshInterval))
        {
            _syncAfter[policyId] = now;
        }
    }

    public void RequestRefresh()
    {
        foreach (KeyValuePair<string, OpenIdConnectConfiguration> entry in _currentConfiguration)
        {
            RequestRefresh(entry.Key);
        }
    }
}

Global.asax.cs:

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

Web.config:

<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=301880
  -->
<configuration>
  <appSettings>

    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />

    <!-- Azure AD B2C -->
    <add key="ida:Tenant" value="xxx" />
    <add key="ida:ClientId" value="xxx" />
    <add key="ida:ClientSecret" value="xxx"/>
    <add key="ida:AadInstance" value="https://login.microsoftonline.com/{0}{1}{2}" />
    <add key="ida:RedirectUri" value="https://localhost:44300/" />
    <add key="ida:PostLogoutRedirectUri" value="https://localhost:44300/" />
    <add key="ida:SignUpPolicyId" value="b2c_1_signup01" />
    <add key="ida:SignInPolicyId" value="b2c_1_signin01" />
    <add key="ida:UserProfilePolicyId" value="b2c_1_profile01" />
    <!-- /Azure AD B2C -->

    <add key="appinsights:instrumentationKey" value="xxx" />

  </appSettings>
  <system.web>
    <customErrors mode="Off" />
    <compilation debug="true" targetFramework="4.6.1" />
    <httpRuntime targetFramework="4.6.1" maxRequestLength="1048576" />
    <pages>
      <namespaces>
        <add namespace="Kendo.Mvc.UI" />
      </namespaces>
    </pages>
    <httpModules>
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" />
    </httpModules>
  </system.web>
  <system.webServer>
    <security>
      <requestFiltering>
        <requestLimits maxAllowedContentLength="1073741824" />
      </requestFiltering>
    </security>
    <staticContent>
      <remove fileExtension=".json" />
      <mimeMap fileExtension=".json" mimeType="application/json" />
    </staticContent>
    <rewrite>
      <rules>
        <!-- Enfore HTTPS -->
        <rule name="Force HTTPS" enabled="true">
          <match url="(.*)" ignoreCase="false" />
          <conditions>
            <add input="{HTTPS}" pattern="off" />
          </conditions>
          <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" appendQueryString="true" redirectType="Permanent" />
        </rule>
      </rules>
    </rewrite>
    <validation validateIntegratedModeConfiguration="false" />
    <modules>
      <remove name="ApplicationInsightsWebTracking" />
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" preCondition="managedHandler" />
    </modules>
  </system.webServer>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" culture="neutral" publicKeyToken="30ad4fe6b2a6aeed" />
        <bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Optimization" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="1.1.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-1.6.5135.21930" newVersion="1.6.5135.21930" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-5.1.0.0" newVersion="5.1.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Antlr3.Runtime" publicKeyToken="eb42632606e9261f" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-3.5.0.2" newVersion="3.5.0.2" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.ApplicationInsights" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-1.0.0.4220" newVersion="1.0.0.4220" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.IdentityModel.Tokens.Jwt" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-4.0.20622.1351" newVersion="4.0.20622.1351" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.IdentityModel.Protocol.Extensions" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-1.0.2.33" newVersion="1.0.2.33" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.AI.Agent.Intercept" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-1.2.1.0" newVersion="1.2.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Data.Services.Client" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Data.OData" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Data.Edm" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  <system.codedom>
    <compilers>
      <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:6 /nowarn:1659;1699;1701" />
      <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:14 /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
    </compilers>
  </system.codedom>
</configuration>

目前,我有 PolicyAuthorize 的 "dirty" 解决方法:

protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
    if (this.Policy.Equals(ConfigurationHelper.Authentication.SIGNIN_POLICY_ID, StringComparison.InvariantCultureIgnoreCase))
    {
        filterContext.HttpContext.Response.Redirect("/Account/SignIn", true);
    }
    else if (this.Policy.Equals(ConfigurationHelper.Authentication.SIGNUP_POLICY_ID, StringComparison.InvariantCultureIgnoreCase))
    {
        filterContext.HttpContext.Response.Redirect("/Account/SignUp", true);
    }
    else
    {
        throw new NotSupportedException($"Policy ID {this.Policy} is not supported.");
    }
}

但这并不完美,调试器仍然进入我的控制器方法,但随后我被重定向到登录页面。所以现在我正在使用它,即使它并不完美。

当您说 "Then I ported the code to an existing app" 时,您是否可能漏掉了什么?演示应用程序中有几个对身份验证过程有重要意义的 类 。我对它们的理解不够深入,无法为您指出影响您的问题的正确方法。但是像 Startup.Auth.cs、Startup.c 甚至 Global.asax.cs 这样的文件具有与身份验证机制的全部功能相关的代码。您是否在您的应用程序中检查了所有这些代码以确保它们与可运行的演示应用程序中的相应代码相似?

检查配置中的重定向 uri 是否正确。

您可以添加一个 AuthorizationCodeRecieved 事件处理程序,以查看挑战后返回的内容。 就像在样本中一样——即 ConfigureAuth():

B2C quickstart Web-api-dotnet - with AuthorizationCodeRecieved eventHandler