【问题标题】:In asp.net core, why is await context.ChallengeAsync() not working as expected?在 asp.net 核心中,为什么 await context.ChallengeAsync() 没有按预期工作?
【发布时间】:2020-04-10 06:13:15
【问题描述】:

我有两个问题,都参考下面的代码:

  1. 为什么在我调用 authenticateResult = await context.AuthenticateAsync(); 后 authenticateResult.Succeeded 为 false?

  2. 为什么我需要从我的自定义中间件 InvokeAsync 方法中调用“return”才能使其正常工作?

我有一个使用 OpenIdConnect 的 asp.net 核心应用程序。该应用程序有两个控制器动作;它们都具有 [Authorize] 属性,因此当应用程序启动时,用户会自动进入 OpenIdConnect 进程。这很好用。

这是我如何配置我的 OpenIdConnect 中间件,我碰巧在使用 PingOne:

            services.AddAuthentication(authenticationOptions =>
            {
                authenticationOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                authenticationOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie()
            .AddOpenIdConnect(openIdConnectOptions =>
            {
                openIdConnectOptions.Authority = Configuration["PingOne:Authority"];
                openIdConnectOptions.CallbackPath = Configuration["PingOne:CallbackPath"];
                openIdConnectOptions.ClientId = Configuration["PingOne:ClientId"];
                openIdConnectOptions.ClientSecret = Configuration["PingOne:ClientSecret"];

                openIdConnectOptions.ResponseType = Configuration["PingOne:ResponseType"];
                openIdConnectOptions.Scope.Clear();
                foreach (var scope in scopes.GetChildren())
                {
                    openIdConnectOptions.Scope.Add(scope.Value);
                }
            });

在用户进行身份验证后,我立即将用户重定向到另一个网站(使用相同的 OpenIdConnect 身份验证)。在“OtherWebsite”上,用户选择各种选项,然后被重定向回“OriginalWebsite”到一个名为“ReturningFromOtherWebsite”的特殊路径。返回 OriginalWebSite 后,我读取了查询字符串,根据查询字符串将一些声明加载到用户的主体身份中,并设置一个 Session 变量,以便我知道我曾经访问过 OtherWebSite。

我在 OriginalWebSite 中实际上没有名为“ReturningFromOtherWebsite”的控制器方法,因此我需要在我的中间件中查找该路径并拦截它的处理。

我决定将此功能包装在我称为“AfterAuthenticationMiddleware”的自定义中间件中,如下所示。我的问题由以“//QUESTION:...”开头的 cmets 标记

public class AfterAuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration Configuration;
    private IMembershipRepository MembershipRepository;

    public AfterAuthenticationMiddleware(RequestDelegate next, 
        IConfiguration configuration)
    {
        _next = next;
        Configuration = configuration;
    }

    private void SignInWithSelectedIdentity(Guid userId, 
        ClaimsIdentity claimsIdentity,
        AuthenticateResult authenticateResult,
        HttpContext context)
    {
        string applicationName = Configuration["ApplicationName"];

        List<string> roles = MembershipRepository.GetRoleNamesForUser(userId, applicationName);

        foreach (var role in roles)
        {
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
        }

        //add the claim to the authentication cookie
        context.SignInAsync(authenticateResult.Principal, authenticateResult.Properties);
    }


    public async Task InvokeAsync(HttpContext context, 
        IMembershipRepository membershipRepository)
    {
        MembershipRepository = membershipRepository;

        bool isIdentitySelected = context.Session.GetBoolean("IsIdentitySelected").GetValueOrDefault();

        if (isIdentitySelected)
        {
            //I know from existence of Session variable that there is no work to do here.
            await _next(context);
            return;
        }

        var authenticateResult = await context.AuthenticateAsync();
        ClaimsIdentity claimsIdentity = null;

        //the Controller action ReturningFromOtherWebSite does not actually exist.
        if (context.Request.Path.ToString().Contains("ReturningFromOtherWebSite"))
        {
            if (!authenticateResult.Succeeded)
            {
                //this next line triggers the OpenIdConnect process
                await context.ChallengeAsync();

                //QUESTION: If I re-fetch the authenticateResult here, why is IsSucceeded false, for example:
                //var authenticateResult = await context.AuthenticateAsync();

                //QUESTION: why is the next line needed for this to work
                return;


            }

            claimsIdentity = (ClaimsIdentity)authenticateResult.Principal.Identity;

            //set the Session variable so that on future requests we can bail out of this method quickly.
            context.Session.SetBoolean(Constants.IsIdentitySelected, true);
            var request = context.Request;

            //load some claims based on what the user selected in "OtherWebSite"
            string selectedIdentity = request.Query["selectedIdentity"];

            if (!Guid.TryParse(selectedIdentity, out Guid userId))
            {
                throw new ApplicationException(
                    $"Unable to parse Guid from 'selectedIdentity':{selectedIdentity} ");
            }

            SignInWithSelectedIdentity(userId, claimsIdentity, authenticateResult, context);

            //redirect user to the page that the user originally requested
            string returnUrl = request.Query["returnUrl"];
            if (string.IsNullOrEmpty(returnUrl))
                throw new ApplicationException(
                    $"Request is ReturnFromIdentityManagement but missing required parameter 'returnUrl' in querystring:{context.Request.QueryString} ");

            string path = $"{request.Scheme}://{request.Host}{returnUrl}";

            Log.Logger.Verbose($"AfterAuthentication InvokeAsync Redirect to {path}");
            context.Response.Redirect(path);
            //I understand why I call "return" here; I just want to send the user on to the page he/she originally requested without any more middleware being invoked
            return;
        }

        if (!authenticateResult.Succeeded)
        {
            //if the user has not gone through OIDC there is nothing to do here
            await _next(context);
            return;
        }

        //if get here it means user is authenticated but has not yet selected an identity on OtherWebSite
        claimsIdentity = (ClaimsIdentity)authenticateResult.Principal.Identity;

        Log.Logger.Verbose($"AfterAuthentication InvokeAsync check if redirect needed.");
        var emailClaim = claimsIdentity.Claims.FirstOrDefault(o => o.Type == ClaimTypes.Email);
        if(emailClaim == null)
            throw new ApplicationException($"User {authenticateResult.Principal.Identity.Name} lacks an Email claim");

        string emailAddress = emailClaim.Value;
        if(string.IsNullOrWhiteSpace(emailAddress))
            throw new ApplicationException("Email claim value is null or whitespace.");

        string applicationName = Configuration["ApplicationName"];
        if(string.IsNullOrEmpty(applicationName))
            throw new ApplicationException("ApplicationName missing from appsettings.json.");

        //if there is just one userid associated with the email address, load the claims.  if there is
        //more than one the user must redirect to OtherWebSite and select it
        List<Guid?> userIds =
            MembershipRepository.IsOtherWebsiteRedirectNeeded(emailAddress, applicationName);

        if (userIds == null
            || userIds[0] == null
            || userIds.Count > 1)
        {
            //include the path the user was originally seeking, we will redirect to this path on return
            //cannot store in session (we lose session on the redirect to other web site)
            string queryString =
                $"emailAddress={emailAddress}&applicationName={applicationName}&returnUrl={context.Request.Path}";

            context.Response.Redirect($"https://localhost:44301/Home/AuthenticatedUser?{queryString}");
        }
        else
        {
            SignInWithSelectedIdentity(userIds[0].Value, claimsIdentity, authenticateResult, context);
        }

        await _next(context);
    }
}

然后我按照通常的方式在 Configure 方法中添加中间件:

app.UseAuthentication();
app.UseAfterAuthentication();
app.UseAuthorization();

我绝望地添加了“return”调用,并震惊地发现它解决了问题,但在我知道为什么它解决了问题之前我不会感到舒服。

【问题讨论】:

    标签: c# asp.net-core openid-connect asp.net-core-middleware


    【解决方案1】:

    我将冒险猜测发生了什么。

    我在 Configure() 方法的末尾连接了一个侦听器到 OpenIdConnect 库,如下所示:

    IdentityModelEventSource.Logger.LogLevel = EventLevel.Verbose;
    IdentityModelEventSource.ShowPII = true;
    var listener = new MyEventListener();
    listener.EnableEvents(IdentityModelEventSource.Logger, EventLevel.Verbose);
    listener.EventWritten += Listener_EventWritten;
    

    然后在 Listener_EventWritten 事件中,我将记录到数据库。

    private void Listener_EventWritten(object sender, EventWrittenEventArgs e)
        {
            foreach (object payload in e.Payload)
            {
                Log.Logger.Information($"[{e.EventName}] {e.Message} | {payload}");
            }
        }
    

    我还在整个应用程序中添加了详细日志记录,以了解正在发生的事情。不幸的是,似乎没有任何方法可以将侦听器附加到 Authentication 或 Authorization 中间件。

    这就是我认为正在发生的事情。每个 asp.net 核心中间件按顺序触发——在请求期间按前向顺序触发,然后在响应期间按后向顺序触发。当我在自定义中间件中遇到让我感到困惑的代码时:

    if (context.Request.Path.ToString().Contains("ReturningFromOtherWebSite"))
        {
            if (!authenticateResult.Succeeded)
            {
                //this next line triggers the OpenIdConnect process
                await context.ChallengeAsync();
    
                //QUESTION: If I re-fetch the authenticateResult here, why is IsSucceeded false, for example:
                //var authenticateResult = await context.AuthenticateAsync();
    
                //QUESTION: why is the next line needed for this to work
                return;
            } 
    

    调用“await context.ChallengeAsync();”触发 Authentication 中间件;我可以从我的日志记录中看到,此时 Oidc 和 Cookie 身份验证都会触发。此调用后需要“返回”,因为我不希望执行线程在我的自定义中间件中继续;相反,我想让调用“等待 context.ChallengeAsync();”完成它的工作并再次调用我的自定义中间件。

    我可以从我的日志中看到我的自定义中间件确实被再次调用了,这次 authenticateResult.Succeeded 为真。

    调用 var "authenticateResult = await context.AuthenticateAsync();"产生“成功”的错误,因为我的自定义中间件此时不“知道”用户已通过身份验证。我的自定义中间件“知道”这一点的唯一方法是身份验证中间件使用“await(next)”调用它。这意味着我需要返回并等待该调用。

    再次,这是我的猜测,如果有人确切知道,我会很感激更好的解释。我试过查看 Oidc 源代码,但我承认我觉得它令人困惑,因为我是 Core 新手,还没有完全掌握整个异步业务。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-12-04
      • 2021-03-11
      • 2014-08-04
      • 2021-01-03
      • 2018-11-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多