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

标签 c# asp.net-mvc authentication azure-active-directory azure-ad-b2c

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

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

[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 应用程序时,我会自动进入 Controller 的 Index 方法。然后我进入属性的 HandleUnauthorizedRequest 并调用 Challenge 方法。

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

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

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

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

更新:

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

启动.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);
    }
}

网络配置:

<?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 有一个“肮脏”的解决方法:

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.");
    }
}

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

最佳答案

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

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

B2C quickstart Web-api-dotnet - with AuthorizationCodeRecieved eventHandler

关于c# - Azure AD B2C - 身份验证质询不触发身份验证,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34907859/

相关文章:

c# - Owin 授权拒绝角色

c# - 我的单元测试有效, Controller 中的相同代码不起作用

c# - MVC3 : Overriding the Model values in HTTPPost action method

ios - 我怎样才能只要求使用 Alamofire 进行一次身份验证?

authentication - 使用用户名和密码的 Openvpn

java - JSP登录2页

c# - ScrollViewer 在没有固定高度的情况下不显示滚动距离

c# - 顶级工作方法中的异步模式

C# 未从 HttpWebResponse 获得正确的响应。编码?

c# - 在 IGrouping 中使用 Where( Expression<Func<T, bool>> )