使用Microsoft.AspNetCore.Authentication.OpenIdConnect
中间件时,为什么带有过期access_token的表单POST导致GET?发生这种情况时,输入到表单中的所有数据都会丢失,因为它不会到达HttpPost端点。相反,在signin-oidc重定向之后,使用GET将请求重定向到相同的URI。这是一个限制,还是我的配置不正确?
我在缩短AccessTokenLifetime之后发现了这个问题,目的是强迫用户更频繁地更新其声明(即,如果用户被禁用或他们的声明被撤消)。我只是在OpenIdConnect中间件的OpenIdConnectionOptions设置为true options.UseTokenLifetime = true;
(将其设置为false导致通过身份验证的用户的声明未按预期更新)时重现此内容。
我可以使用IdentityServer4示例快速入门ExternalName重新创建并演示此行为,并进行以下更改。基本上,有一个具有HttpGet和HttpPost方法的授权表单。如果您等待的时间比AccessTokenLifetime(在此示例中配置为仅30秒)的提交时间长,则将调用HttpGet方法而不是HttpPost方法。
对5_HybridFlowAuthenticationWithApiAccess
的修改public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
// the following was added
options.SlidingExpiration = false;
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("api1");
options.ClaimActions.MapJsonKey("website", "website");
// the following were changed
options.UseTokenLifetime = true;
options.Scope.Add("offline_access");
});
}
对MvcClient/Startup.cs中的“客户端”列表的修改
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Hybrid,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1",
IdentityServerConstants.StandardScopes.OfflineAccess,
},
AllowOfflineAccess = true,
// the following properties were configured:
AbsoluteRefreshTokenLifetime = 14*60*60,
AccessTokenLifetime = 30,
IdentityTokenLifetime = 15,
AuthorizationCodeLifetime = 15,
SlidingRefreshTokenLifetime = 60,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
UpdateAccessTokenClaimsOnRefresh = true,
RequireConsent = false,
}
[Authorize]
[HttpGet]
[Route("home/test", Name = "TestRouteGet")]
public async Task<IActionResult> Test()
{
TestViewModel viewModel = new TestViewModel
{
Message = "GET at " + DateTime.Now,
TestData = DateTime.Now.ToString(),
AccessToken = await this.HttpContext.GetTokenAsync("access_token"),
RefreshToken = await this.HttpContext.GetTokenAsync("refresh_token"),
};
return View("Test", viewModel);
}
[Authorize]
[HttpPost]
[Route("home/test", Name = "TestRoutePost")]
public async Task<IActionResult> Test(TestViewModel viewModel)
{
viewModel.Message = "POST at " + DateTime.Now;
viewModel.AccessToken = await this.HttpContext.GetTokenAsync("access_token");
viewModel.RefreshToken = await this.HttpContext.GetTokenAsync("refresh_token");
return View("Test", viewModel);
}
答案 0 :(得分:1)
经过进一步的研究和调查,我得出的结论是,现成不支持完成重定向到OIDC提供程序的表单POST(至少对于Identity Server,但我怀疑其他身份也是如此)以及连接提供商)。这是我唯一能找到的提及:Sending Custom Parameters to Login Page
我能够针对此问题提出解决方法,下面对此进行了概述,希望对其他人有用。关键组件是以下OpenIdConnect和Cookie中间件事件:
OpenIdConnect中间件公开了OnRedirectToIdentityProvider
事件,这使我们有机会:
RedirectContext
以在AuthenticationProperties
Items字典中包含自定义请求ID new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async (context) =>
{
if (context.HttpContext.Request.Method == HttpMethods.Post && context.Properties.ExpiresUtc == null)
{
string requestId = Guid.NewGuid().ToString();
context.Properties.Items["OidcPostRedirectRequestId"] = requestId;
HttpRequest requestToSave = context.HttpContext.Request;
// EXAMPLE - saving this to memory which would work on a non-loadbalanced or stateful environment. Recommend persisting to external store such as Redis.
postedRequests[requestId] = await HttpRequestLite.BuildHttpRequestLite(requestToSave);
}
return;
},
};
Cookie中间件公开了OnValidatePrincipal
事件,这使我们有机会:
CookieValidatePrincipalContext
中检查AuthenticationProperties
项是否有自定义词典项目。我们将其检查为已保存/缓存的请求的ID
ShouldRenew
设置为true可以保留后续请求的所有更改HttpRequestLite
对象,并覆盖CookieValidatePrincipalContext
对象中的Request对象
new CookieAuthenticationEvents
{
OnValidatePrincipal = (context) =>
{
if (context.Properties.Items.ContainsKey("OidcPostRedirectRequestId"))
{
string requestId = context.Properties.Items["OidcPostRedirectRequestId"];
context.Properties.Items.Remove("OidcPostRedirectRequestId");
context.ShouldRenew = true;
if (postedRequests.ContainsKey(requestId))
{
HttpRequestLite requestLite = postedRequests[requestId];
postedRequests.Remove(requestId);
if (requestLite.Body?.Any() == true)
{
context.Request.Body = new MemoryStream(requestLite.Body);
}
context.Request.ContentLength = requestLite.ContentLength;
context.Request.ContentLength = requestLite.ContentLength;
context.Request.ContentType = requestLite.ContentType;
context.Request.Method = requestLite.Method;
context.Request.Headers.Clear();
foreach (var header in requestLite.Headers)
{
context.Request.Headers.Add(header);
}
}
}
return Task.CompletedTask;
},
};
我们需要一个类来将HttpRequest映射到/从中映射出来,以进行序列化。这将读取HttpRequest及其主体,而无需修改内容,它使HttpRequest保持不变,不会出现其他中间件,这些中间件可能会在我们尝试之后读取它(这在尝试读取Body流时非常重要,默认情况下只能读取一次)。
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.Primitives;
public class HttpRequestLite
{
public static async Task<HttpRequestLite> BuildHttpRequestLite(HttpRequest request)
{
HttpRequestLite requestLite = new HttpRequestLite();
try
{
request.EnableRewind();
using (var reader = new StreamReader(request.Body))
{
string body = await reader.ReadToEndAsync();
request.Body.Seek(0, SeekOrigin.Begin);
requestLite.Body = Encoding.ASCII.GetBytes(body);
}
//requestLite.Form = request.Form;
}
catch
{
}
requestLite.Cookies = request.Cookies;
requestLite.ContentLength = request.ContentLength;
requestLite.ContentType = request.ContentType;
foreach (var header in request.Headers)
{
requestLite.Headers.Add(header);
}
requestLite.Host = request.Host;
requestLite.IsHttps = request.IsHttps;
requestLite.Method = request.Method;
requestLite.Path = request.Path;
requestLite.PathBase = request.PathBase;
requestLite.Query = request.Query;
requestLite.QueryString = request.QueryString;
requestLite.Scheme = request.Scheme;
return requestLite;
}
public QueryString QueryString { get; set; }
public byte[] Body { get; set; }
public string ContentType { get; set; }
public long? ContentLength { get; set; }
public IRequestCookieCollection Cookies { get; set; }
public IHeaderDictionary Headers { get; } = new HeaderDictionary();
public IQueryCollection Query { get; set; }
public IFormCollection Form { get; set; }
public PathString Path { get; set; }
public PathString PathBase { get; set; }
public HostString Host { get; set; }
public bool IsHttps { get; set; }
public string Scheme { get; set; }
public string Method { get; set; }
}