我有两个问题,这两个问题都涉及以下代码:
为什么在我调用context.AuthenticateAsync();?
我有一个使用asp.net的OpenIdConnect核心应用程序。应用程序有两个控制器操作;它们都具有授权属性,因此当应用程序启动时,用户将自动通过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时,我读取查询字符串,根据查询字符串将一些声明加载到用户的主体标识中,并设置一个会话变量,以便我知道我曾经访问过OtherWebSite。
实际上,我在OriginalWebSite中没有一个名为“OriginalWebSite”的Controller方法,因此我需要在中间件中查找该路径并拦截它的处理。
我决定将此功能封装在我称为"AfterAuthenticationMiddleware“的自定义中间件中,该中间件如下所示。我的问题以“//问题:.”开头的评论为标志。
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);
}
}然后以通常的方式在配置方法中添加中间件:
app.UseAuthentication();
app.UseAfterAuthentication();
app.UseAuthorization();出于绝望,我加入了“返回”的号召,并震惊地发现它解决了问题,但在我知道为什么修复了问题之前,我不会感到舒服。
发布于 2019-12-18 14:02:18
我要冒险猜一下发生了什么事。
我已经在配置()方法的末尾将一个侦听器连接到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}");
}
}我还在整个应用程序中添加了详细的日志记录,以了解正在发生的事情。不幸的是,似乎没有任何方法将侦听器附加到身份验证或授权中间件。
以下是我认为正在发生的事情。每个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;
} 对“等待context.ChallengeAsync()”的调用触发身份验证中间件;从日志记录中可以看出Oidc和Cookie身份验证都在此时触发。在这个调用之后需要一个“返回”,因为我不希望执行线程在我的定制中间件中继续;相反,我想让调用“等待context.ChallengeAsync()”完成它的工作并再次调用我的定制中间件。
从日志记录可以看出,我的自定义中间件确实再次被调用,这一次authenticateResult.Succeeded是真的。
对var "authenticateResult =等待context.AuthenticateAsync()“的调用产生了一个”成功“的false,因为我的自定义中间件此时并不”知道“用户已经通过了身份验证。我的自定义中间件将“知道”的唯一方法是当身份验证中间件使用“等待(下一个)”调用它时。这意味着我需要返回,只需等待调用。
再说一次,这是我的猜测,如果有人知道的话,我会感激一个更好的解释。我试过查看Oidc源代码,但我承认我觉得它令人费解,因为我刚接触过Core,还没有完全掌握整个异步业务。
https://stackoverflow.com/questions/59379370
复制相似问题