【问题标题】:IdentityServer4 how to access parameters in /connect/token requestIdentityServer4如何访问/connect/token请求中的参数
【发布时间】:2020-04-07 18:32:02
【问题描述】:

我正在尝试创建一个 IdentityServer4 解决方案(ASP.NET Core 3.1,Entity Framework Core),它是多租户和每租户数据库模型。

我首先传递 acr_values 租户:租户名称。然后我从请求中提取它并动态获取正确的连接字符串。

但是,我最终得到了这个 OpenIdConnectProtocolException: Message contains error: 'invalid_grant', error_description: 'error_description is null', error_uri: 'error_uri is null'。

它发生在它尝试读/写 PersistedGrants 时。当我到达发生这种情况的 /connect/token 端点时,我可以知道,我丢失了请求中租户名称的所有跟踪。它不再在查询字符串、正文中,什么都没有……但我此时也没有经过身份验证的用户来查看声明。

什么是访问此信息以正确连接到最终请求的数据库的好方法?

我只附加了我的 EntityFrameworkCore db 上下文配置,因为这就是所有魔法发生的地方。

            services.AddDbContext<MyAppDbContext>((serviceProvider, options) =>
            {
                // Get the standard default connection string
                string connectionString = Configuration.GetConnectionString("DefaultConnection");

                // Inspect the HTTP Context
                var httpContext = serviceProvider.GetService<IHttpContextAccessor>().HttpContext;

                // function to parse the tenant name from the acr (i.e. tenant:testtenant)
                string GetTenantNameFromRequest(HttpRequest request)
                {
                    string ParseTenantName(string acrValues)
                    {
                        return Regex.Match(acrValues, @"tenant:(?<TenantName>[^\s]*)").Groups["TenantName"]?.Value;
                    }

                    if (request == null || request.Query?.Count == 0)
                        return null;

                    // Get the possible queries for the tenant name to show up in
                    var acr = request.Query["acr_values"];
                    if (!string.IsNullOrEmpty(acr))
                        return ParseTenantName(acr);

                    var returnUrl = request.Query["returnUrl"]; // Web MVC
                    if (!string.IsNullOrEmpty(returnUrl))
                    {
                        NameValueCollection returnUrlQuery = HttpUtility.ParseQueryString(returnUrl);
                        return ParseTenantName(returnUrlQuery["acr_values"]);
                    }

                    var redirectUri = request.Query["redirect_uri"]; // OIDC Client (WinForms)
                    if (!string.IsNullOrEmpty(redirectUri))
                    {
                        NameValueCollection returnUrlQuery = HttpUtility.ParseQueryString(returnUrl);
                        return ParseTenantName(returnUrlQuery["acr_values"]);
                    }

                    //  connect/token does not include any information about the authentication request

                    return null;
                }

                string tenantName = GetTenantNameFromRequest(httpContext?.Request);
                if (!string.IsNullOrEmpty(tenantName) && string.Compare(tenantName, "localhost", true) != 0)
                {
                    // call catalog to get the tenant connection information
                    var tenantLookupService = serviceProvider.GetService<TenantLookupService>();
                    connectionString = tenantLookupService.GetTenantConnectionStringAsync(tenantName).GetAwaiter().GetResult();
                }

                // connect with the proper connection string.
                options.UseSqlServer(connectionString, o =>
                {
                    o.EnableRetryOnFailure();
                });
            });

这是 IdentityServer4 设置代码,但我认为它与此问题无关。

            services.AddDefaultIdentity<User>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<MyAppDbContext>()
                .AddDefaultTokenProviders();

            services.AddTransient<IProfileService, ProfileService>();

            var builder = services.AddIdentityServer(options =>
            {
            })
            .AddInMemoryApiResources(Config.Apis)
            .AddInMemoryClients(Config.Clients)
            .AddInMemoryIdentityResources(Config.GetIdentityResources())
            .AddAspNetIdentity<User>()
            .AddOperationalStore<MyAppDbContext>(options =>
            { 
                options.EnableTokenCleanup = true;
            })
            .AddProfileService<ProfileService>()
            .AddDeveloperSigningCredential();

更新...尝试通过 cookie 访问的新方法

            services.AddDbContext<MyAppDbContext>((serviceProvider, options) =>
            {
                string connectionString = Configuration.GetConnectionString("DefaultConnection");

                var httpContext = serviceProvider.GetService<IHttpContextAccessor>().HttpContext;

                string GetTenantNameFromRequest(HttpContext context)
                {
                    string ParseTenantName(string acrValues)
                    {
                        return Regex.Match(acrValues, @"tenant:(?<TenantName>[^\s]*)").Groups["TenantName"]?.Value;
                    }

                    var request = context?.Request;

                    if (request == null) //|| request.Query?.Count == 0)
                        return null;

                    // Get the possible queries for the tenant name to show up in
                    var acr = request.Query["acr_values"];
                    if (!string.IsNullOrEmpty(acr))
                    { 
                        string tenantName = ParseTenantName(acr);
                        if (!string.IsNullOrEmpty(tenantName))
                        {
                            CookieOptions cookieOptions = new CookieOptions();
                            cookieOptions.IsEssential = true;
                            cookieOptions.SameSite = SameSiteMode.Strict;
                            cookieOptions.Secure = true;
                            cookieOptions.Expires = DateTime.Now.AddMinutes(10);
                            context.Response.Cookies.Append("signin-tenant", tenantName, cookieOptions);
                        }

                        return tenantName;
                    }
                    else
                    {
                        string tenantName = context.Request.Cookies["signin-tenant"];
                        return tenantName;
                    }
                }

                string tenantName = GetTenantNameFromRequest(httpContext);
                if (!string.IsNullOrEmpty(tenantName) && string.Compare(tenantName, "localhost", true) != 0)
                {
                    var tenantLookupService = serviceProvider.GetService<TenantLookupService>();
                    connectionString = tenantLookupService.GetTenantConnectionStringAsync(tenantName).GetAwaiter().GetResult();
                }

                options.UseSqlServer(connectionString, o =>
                {
                    o.EnableRetryOnFailure();
                });
            });

            services.AddDefaultIdentity<User>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<MyAppDbContext>()
                .AddDefaultTokenProviders();

【问题讨论】:

  • 这样不行。您需要在 DbContext 中处理您的租赁。在那里您选择连接字符串。不在启动内。
  • 嗨 Dennis,它实际上在 ASP.NET Core 3 中以这种方式工作得很好。每次建立连接时都会调用它,即使在启动时也是如此。甚至可能从 2.x 开始?不确定。无论如何,它有效,并且运作良好。只需要一种方法在 /connect/token 调用中获取请求的租户。
  • 只是为了明确说明正在采取哪些步骤,您使用的是哪种授权类型?我想象 authentication_code ,因为您提到在 core/connect/token 端点中使用 IPersistedGrantStore 实现。另外,您怎么知道问题是使用 PersistedGrantStore 进行读/写?
  • 是的,我正在使用授权码。我想我不是 100% 确定这是问题所在,不过我是基于几件事。我收到的消息... OpenIdConnectProtocolException:消息包含错误:'invalid_grant',error_description:'error_description is null',error_uri:'error_uri is null'。当我查找这个时,它似乎来自使用 InMemory 操作存储的人。因此,获取该信息,并知道在 localhost 上这工作正常,我指向一个在 db 中确实具有相同用户的 localdb,它可以工作。所以我确实在那里做了一个假设:)
  • 我想我对现在发生的事情了解得更多。当您使用“AddOperationalStore”将 MyAppDbContext 注册为 IPersistedGrantStore 实现时,IdentityServer 现在将在需要 IPersistedGrantStore 时运行“AddDbContext”中指定的配置。由于令牌请求没有 acr_values,因此 DbContext 在验证授权代码时实际上无法确定要连接到哪个数据库,并且代码兑换将失败。也许您可以在到达登录页面时在受保护的 cookie 中设置所需的 acr_values。

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


【解决方案1】:

这是无法实现的,并且已通过最小特权确认。如果您需要通过整个流程访问租户名称,acr_values 不是要走的路。

【讨论】:

    【解决方案2】:


    嗨,我通过添加以下代码实现了这一目标

     string acr_values = context?.ValidatedRequest?.Raw.Get("acr_values");
    

    另外如果你想在你的客户端注入acr_value,你可以通过它注入

      context.TokenEndpointRequest.Parameters.Add("acr_value", "tenantCode:xyz");
    

    【讨论】:

    • 我尝试了这个和任何其他方法来达到那个目的。事实是,当您到达 /connect/token 时,找不到这些 acr 值。我最终实现了一个端点路由器,并将租户名称直接注入到任何 IS4 端点的路径中。
    • 嗨@Nate,您能否提供您提供的方式,tenantId。我总是得到tenantId的值。
    猜你喜欢
    • 2017-07-26
    • 1970-01-01
    • 2019-10-18
    • 2012-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多