在asp.net核心中,为什么等待context.ChallengeAsync()不能按预期工作? [英] In asp.net core, why is await context.ChallengeAsync() not working as expected?

查看:146
本文介绍了在asp.net核心中,为什么等待context.ChallengeAsync()不能按预期工作?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have two questions, both of which refer to the code below:

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

  1. Why is authenticateResult.Succeeded false after I call authenticateResult = await context.AuthenticateAsync();?

为什么我需要从我的自定义中间件InvokeAsync方法中调用返回"才能使其正常工作?

Why do I need to call "return" from my custom middleware InvokeAsync method for this to work properly?

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

I have an asp.net core application using OpenIdConnect. The application has two controller actions; both of them have the [Authorize] attribute, so when the application starts the user is automatically put through the OpenIdConnect process. This works fine.

这是我配置OpenIdConnect中间件的方式,我碰巧正在使用PingOne:

Here is how I configure my OpenIdConnect middleware, I happen to be using 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.

Immediately after a user authenticates I redirect the user to another website (which uses the same OpenIdConnect authentication). On "OtherWebsite" the user selects various options and then gets redirected back to the "OriginalWebsite" to a special path called "ReturningFromOtherWebsite". On return to OriginalWebSite I read the querystring, load some claims into the user's principal identity based on the querystring, and set a Session variable so that I know I've visited OtherWebSite once.

我在OriginalWebSite中实际上没有一个名为"ReturningFromOtherWebsite"的Controller方法,因此我需要在中间件中查找该路径并拦截该路径.

I do not actually have a Controller method called "ReturningFromOtherWebsite" in OriginalWebSite, so I need to look for that path in my middleware and intercept handling of it.

我决定将此功能包装在称为"AfterAuthenticationMiddleware"的自定义中间件中,它看起来像这样.我的问题以"//QUESTION:..."开头的注释为标记.

I decided to wrap this functionality in custom middleware I call "AfterAuthenticationMiddleware", which looks like this. My questions are marked by the comments that start with "//QUESTION:..."

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"方法中添加中间件:

And then I add the middlewares in the Configure method in the usual way:

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

我绝望地添加了"return"呼叫,很震惊地发现它可以解决问题,但是直到我知道为什么可以解决问题后,我才会感到自在.

I added the "return" call out of desperation and was shocked to discover that it fixed the problem, but I won't feel comfortable until I know why it fixed the problem.

推荐答案

我要冒险猜测正在发生的事情.

I'm going to hazard a guess as to what is happening.

我已经在Configure()方法末尾将侦听器连接到OpenIdConnect库,如下所示:

I've hooked up a listener to the OpenIdConnect library at the end of the Configure() method, like so:

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

,然后在Listener_EventWritten事件中,我正在登录数据库.

and then inside the Listener_EventWritten event I'm logging to a database.

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

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

I've also added verbose logging throughout the application, to get a sense of what is happening. Unfortunately there does not seem to be any way to attach listeners to the Authentication or Authorization middlewares.

这是我认为正在发生的事情.每个asp.net核心中间件都按顺序触发-在Request期间按向前顺序,然后在Response期间按向后顺序.当我在自定义中间件中碰到一些代码时,让我感到困惑:

Here is what I believe is happening. Each asp.net core middleware fires sequentially--in forward order during the Request, then in backwards order during the Response. When I hit the bit of code in my custom middleware that confused me:

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();"的调用触发身份验证中间件;从我的日志记录中可以看出,此时Oidc和Cookie身份验证均已启动.调用之后需要返回",因为我不希望执行线程在我的自定义中间件中继续;相反,我想让调用"await context.ChallengeAsync();"完成其工作,然后再次调用我的自定义中间件.

the call to "await context.ChallengeAsync();" fires the Authentication middleware; I can see from my logging that both the Oidc and Cookie authentication fire at this point. A "return" is needed after this call because I don't want the thread of execution to continue in my custom middleware; instead I want to let the call to "await context.ChallengeAsync();" complete its work and invoke my custom middleware again.

从日志中可以看到,确实再次调用了自定义中间件,这一次authenticateResult.Succeeded为true.

I can see from my logging that my custom middleware is indeed invoked again, and this time the authenticateResult.Succeeded is true.

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

The call to var "authenticateResult = await context.AuthenticateAsync();" yields a "Succeeded" of false because my custom middleware does not "know" at this point that the user has authenticated. The only way my custom middleware will "know" this is when the Authentication middleware calls it with "await(next)". That means I need to return and simply wait for that invocation.

再说一次,这是我的猜测,如果有人可以肯定,我会更好地解释一下.我尝试查看Oidc源代码,但是我承认我感到困惑,因为我是Core的新手,还没有完全掌握整个异步业务.

Again, this is my guess, if anyone knows for certain I'd appreciate a better explanation. I've tried looking at the Oidc source code but I admit I find it bewildering, as I'm new to Core and have not yet fully grasped the whole async business yet.

这篇关于在asp.net核心中,为什么等待context.ChallengeAsync()不能按预期工作?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆