表单POST上的OpenIdConnect重定向

时间:2019-10-21 19:52:48

标签: asp.net-core identityserver4 openid-connect

使用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,
}

已添加到IdentityServer/Config.cs

[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);
}

1 个答案:

答案 0 :(得分:1)

经过进一步的研究和调查,我得出的结论是,现成不支持完成重定向到OIDC提供程序的表单POST(至少对于Identity Server,但我怀疑其他身份也是如此)以及连接提供商)。这是我唯一能找到的提及:Sending Custom Parameters to Login Page

我能够针对此问题提出解决方法,下面对此进行了概述,希望对其他人有用。关键组件是以下OpenIdConnect和Cookie中间件事件:

  • OpenIdConnectEvents.OnRedirectToIdentityProvider-保存发布请求以供以后检索
  • CookieAuthenticationEvents.OnValidatePrincipal-检查已保存的Post请求并以已保存状态更新当前请求

OpenIdConnect中间件公开了OnRedirectToIdentityProvider事件,这使我们有机会:

  • 确定这是否是过期的访问令牌的表单发布
  • 修改RedirectContext以在AuthenticationProperties Items字典中包含自定义请求ID
  • 将当前的HttpRequest映射到可以持久保存到高速缓存存储区的HttpRequestLite对象,对于负载平衡的环境,我建议使用过期的分布式高速缓存。为了简单起见,我在这里使用静态字典
    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; }
    }