【问题标题】:OpenIdConnect Redirect on Form POST表单 POST 上的 OpenIdConnect 重定向
【发布时间】:2020-02-17 22:51:36
【问题描述】:

为什么在使用Microsoft.AspNetCore.Authentication.OpenIdConnect 中间件时,带有过期 access_token 的表单 POST 会导致 GET?发生这种情况时,输入到表单中的任何数据都会丢失,因为它没有到达 HttpPost 端点。相反,在 signin-oidc 重定向之后,请求将通过 GET 重定向到相同的 URI。这是一个限制,还是我的某些配置不正确?

我在缩短 AccessTokenLifetime 后注意到了这个问题,目的是强制更频繁地更新用户的声明(即,如果用户被禁用或他们的声明被撤销)。我只在 OpenIdConnect 中间件的 OpenIdConnectionOptions 设置为 true options.UseTokenLifetime = true; 时复制了此内容(将其设置为 false 会导致经过身份验证的用户的声明未按预期更新)。

我能够使用 IdentityServer4 示例快速入门 5_HybridFlowAuthenticationWithApiAccess 重新创建并演示此行为,并进行了以下更改。基本上,有一个具有 HttpGet 和 HttpPost 方法的授权表单。如果您等待的时间超过了 AccessTokenLifetime(在本例中配置为仅 30 秒)提交表单,则会调用 HttpGet 方法而不是 HttpPost 方法。

MvcClient/Startup.cs的修改

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

修改IdentityServer/Config.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,
}

添加到MvcClient/Controllers/HomeController

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

【问题讨论】:

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


    【解决方案1】:

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

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

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

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

    • 确定这是否是过期访问令牌的表单帖子
    • 修改 RedirectContext 以在 AuthenticationProperties 项目字典中包含自定义请求 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; }
        }
    
    

    【讨论】:

      猜你喜欢
      • 2015-11-04
      • 1970-01-01
      • 2013-08-17
      • 1970-01-01
      • 2015-12-02
      • 2013-12-31
      • 2011-12-06
      • 1970-01-01
      • 2017-03-30
      相关资源
      最近更新 更多