【发布时间】: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