NET5 JWT Bearer Authentication 不识别 SSL 证书
NET5 JWT Bearer Authentication not recognizing SSL certificate
我有一个使用 Grpc 的 .NET 5 WebApi 和一个位于 YARP 反向代理后面的 IdentityServer4 运行。反向代理使用有效的 Let's Encrypt 证书并将请求路由到其他两个正在侦听 localhost:port 并使用自签名证书的请求。它们是 Linux Mint 20.1 上的 运行,我使用 OpenSSL 创建了自签名证书并将其添加到 /usr/local/share/ca-certificates/extra
和 运行 update-ca-certificates
以更新证书存储。
一切正常,YARP 识别用于路由请求的 sefl 签名证书,但向需要授权的 WebApi 请求抛出此异常:
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id "0HMCHFQCVF5UE", Request id "0HMCHFQCVF5UE:0000000B": An unhandled exception was thrown by the application.
System.InvalidOperationException: IDX20803: Unable to obtain configuration from: 'System.String'.
---> System.IO.IOException: IDX20804: Unable to retrieve document from: 'System.String'.
---> System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: NotTimeValid
at System.Net.Security.SslStream.SendAuthResetSignal(ProtocolToken message, ExceptionDispatchInfo exception)
at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](TIOAdapter adapter, Boolean receiveFirst, Byte[] reAuthenticationData, Boolean isApm)
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Boolean async, Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Boolean async, Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.SendAsyncCore(HttpRequestMessage request, HttpCompletionOption completionOption, Boolean async, Boolean emitTelemetryStartStop, CancellationToken cancellationToken)
at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel)
--- End of inner exception stack trace ---
at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel)
at Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever.GetAsync(String address, IDocumentRetriever retriever, CancellationToken cancel)
at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)
--- End of inner exception stack trace ---
at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Grpc.AspNetCore.Web.Internal.GrpcWebMiddleware.HandleGrpcWebRequest(HttpContext httpContext, ServerGrpcWebMode mode)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.HandleException(HttpContext context, ExceptionDispatchInfo edi)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
我正在使用 Microsoft.AspNetCore.Authentication.JwtBear 5.0.11,这似乎就是它的来源。该错误似乎表明证书上的时间有问题,但使用 OpenSSL 检查不早于和不晚于日期都可以。
这是它在启动时的设置方式:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = appSettings.Authorization.Authority;
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
options.TokenValidationParameters.ValidateAudience = false;
});
我尝试同时使用 "https://localhost:port"
和 "https://real.hostname"
作为权威,结果是一样的。我也尝试使用 options.RequireHttpsMetadata = false;
但它没有效果,我也不想切换到普通的 http。
我也在 运行 我的 Windows 机器上使用相同的证书尝试了相同的设置,并且一切正常。是什么导致了不同的行为?我如何才能获得有关它实际尝试验证的证书以及它如何确定错误的更多信息?
我设法找到了原因并解决了它。
原因
Microsoft.AspNetCore.Authentication.JwtBearer
实际上对 IdentityServer4 进行了 2 次调用,而不是 1 次调用:一次调用 /.well-known/openid-configuration
以获取配置,然后调用上一个响应的 jwks_uri
中返回的端点。第一次调用是使用自签名证书调用 localhost:port
端点并正常工作,但第二次调用是使用 Let's Encrypt 证书调用 realdomain
端点并因 OP 中的错误而失败。
- Let's Encrypt 证书在链中有一个过期的证书:它使用的是 DST Root CA X3 而不是 ISRG Root X1(更多信息在这里:https://letsencrypt.org/docs/dst-root-ca-x3-expiration-september-2021/)
修复使用不同域的调用
选项 1 - 在 IdentityServer4 中修复它
这可以通过更改 Startup.cs
中的 IdentityServer4 源来实现:
app.Use(async (ctx, next) =>
{
ctx.SetIdentityServerOrigin("origin");
await next();
});
选项 2 - 在 API jwt 授权配置中修复它
这需要实现您自己的配置管理器:
public class CustomConfigurationManager : IConfigurationManager<OpenIdConnectConfiguration>
{
private readonly string authority;
private readonly string authorityReturnedOrigin;
public CustomConfigurationManager(string authority, string authorityReturnedOrigin)
{
this.authority = authority;
this.authorityReturnedOrigin = authorityReturnedOrigin;
}
public async Task<OpenIdConnectConfiguration> GetConfigurationAsync(CancellationToken cancel)
{
var httpClient = new HttpClient();
var request = new HttpRequestMessage
{
RequestUri = new Uri($"{authority}/.well-known/openid-configuration"),
Method = HttpMethod.Get
};
var configurationResult = await httpClient.SendAsync(request, cancel);
var resultContent = await configurationResult.Content.ReadAsStringAsync(cancel);
if (configurationResult.IsSuccessStatusCode)
{
var config = OpenIdConnectConfiguration.Create(resultContent);
var jwks = config.JwksUri.Replace(authorityReturnedOrigin, authority);
var keyRequest = new HttpRequestMessage
{
RequestUri = new Uri(jwks),
Method = HttpMethod.Get
};
var keysResposne = await httpClient.SendAsync(keyRequest, cancel);
var keysResultContent = await keysResposne.Content.ReadAsStringAsync(cancel);
if (keysResposne.IsSuccessStatusCode)
{
config.JsonWebKeySet = new JsonWebKeySet(keysResultContent);
var signingKeys = config.JsonWebKeySet.GetSigningKeys();
foreach (var key in signingKeys)
{
config.SigningKeys.Add(key);
}
}
else
{
throw new Exception($"Failed to get jwks: {keysResposne.StatusCode}: {keysResultContent}");
}
return config;
}
else
{
throw new Exception($"Failed to get configuration: {configurationResult.StatusCode}: {resultContent}");
}
}
public void RequestRefresh()
{
// if you are caching the configuration this is probably where you should invalidate it
}
}
然后替换Startup.cs
中的默认配置管理器:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = appSettings.Authorization.Authority;
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
options.TokenValidationParameters.ValidateAudience = false;
options.ConfigurationManager = new CustomConfigurationManager("authority", "authorityReturnedOrigin");
}
修复过期的证书链
这适用于 Linux Mint 20.1,其他发行版可能有不同的处理方式。还建议备份。
- 确保 openssl 版本高于 1.0.1 或 1.0.2,因为它们可能存在问题(运行
openssl version
进行检查)
- 运行
ls -l | grep ISRG
并删除您找到的内容 - 这一步可能不是必需的,但我就是这样做的
- 转到
/etc/ssl/certs
- 运行
ls -l | grep DST
并删除你找到的内容
- 下载这些文件:isrgrootx1 isrgrootx2 letsencrytptr3
- 将它们复制到
/etc/ssl/certs
请注意 运行ning 证书更新命令,如 update-ca-certificate
可能会恢复此状态。
可能有更好的解决方案,也许只需更新到更新的 OS 版本就可以解决这个问题。这个问题只出现在 API 从 Linux Mint 20.1 上的代码 运行 对 API 进行的 http 调用中,在 Windows 上一切正常,通过浏览器访问没有显示任何内容SSL 证书错误。
我有一个使用 Grpc 的 .NET 5 WebApi 和一个位于 YARP 反向代理后面的 IdentityServer4 运行。反向代理使用有效的 Let's Encrypt 证书并将请求路由到其他两个正在侦听 localhost:port 并使用自签名证书的请求。它们是 Linux Mint 20.1 上的 运行,我使用 OpenSSL 创建了自签名证书并将其添加到 /usr/local/share/ca-certificates/extra
和 运行 update-ca-certificates
以更新证书存储。
一切正常,YARP 识别用于路由请求的 sefl 签名证书,但向需要授权的 WebApi 请求抛出此异常:
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id "0HMCHFQCVF5UE", Request id "0HMCHFQCVF5UE:0000000B": An unhandled exception was thrown by the application.
System.InvalidOperationException: IDX20803: Unable to obtain configuration from: 'System.String'.
---> System.IO.IOException: IDX20804: Unable to retrieve document from: 'System.String'.
---> System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: NotTimeValid
at System.Net.Security.SslStream.SendAuthResetSignal(ProtocolToken message, ExceptionDispatchInfo exception)
at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](TIOAdapter adapter, Boolean receiveFirst, Byte[] reAuthenticationData, Boolean isApm)
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Boolean async, Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Boolean async, Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.SendAsyncCore(HttpRequestMessage request, HttpCompletionOption completionOption, Boolean async, Boolean emitTelemetryStartStop, CancellationToken cancellationToken)
at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel)
--- End of inner exception stack trace ---
at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel)
at Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever.GetAsync(String address, IDocumentRetriever retriever, CancellationToken cancel)
at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)
--- End of inner exception stack trace ---
at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Grpc.AspNetCore.Web.Internal.GrpcWebMiddleware.HandleGrpcWebRequest(HttpContext httpContext, ServerGrpcWebMode mode)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.HandleException(HttpContext context, ExceptionDispatchInfo edi)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
我正在使用 Microsoft.AspNetCore.Authentication.JwtBear 5.0.11,这似乎就是它的来源。该错误似乎表明证书上的时间有问题,但使用 OpenSSL 检查不早于和不晚于日期都可以。 这是它在启动时的设置方式:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = appSettings.Authorization.Authority;
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
options.TokenValidationParameters.ValidateAudience = false;
});
我尝试同时使用 "https://localhost:port"
和 "https://real.hostname"
作为权威,结果是一样的。我也尝试使用 options.RequireHttpsMetadata = false;
但它没有效果,我也不想切换到普通的 http。
我也在 运行 我的 Windows 机器上使用相同的证书尝试了相同的设置,并且一切正常。是什么导致了不同的行为?我如何才能获得有关它实际尝试验证的证书以及它如何确定错误的更多信息?
我设法找到了原因并解决了它。
原因
Microsoft.AspNetCore.Authentication.JwtBearer
实际上对 IdentityServer4 进行了 2 次调用,而不是 1 次调用:一次调用/.well-known/openid-configuration
以获取配置,然后调用上一个响应的jwks_uri
中返回的端点。第一次调用是使用自签名证书调用localhost:port
端点并正常工作,但第二次调用是使用 Let's Encrypt 证书调用realdomain
端点并因 OP 中的错误而失败。- Let's Encrypt 证书在链中有一个过期的证书:它使用的是 DST Root CA X3 而不是 ISRG Root X1(更多信息在这里:https://letsencrypt.org/docs/dst-root-ca-x3-expiration-september-2021/)
修复使用不同域的调用
选项 1 - 在 IdentityServer4 中修复它
这可以通过更改 Startup.cs
中的 IdentityServer4 源来实现:
app.Use(async (ctx, next) =>
{
ctx.SetIdentityServerOrigin("origin");
await next();
});
选项 2 - 在 API jwt 授权配置中修复它
这需要实现您自己的配置管理器:
public class CustomConfigurationManager : IConfigurationManager<OpenIdConnectConfiguration>
{
private readonly string authority;
private readonly string authorityReturnedOrigin;
public CustomConfigurationManager(string authority, string authorityReturnedOrigin)
{
this.authority = authority;
this.authorityReturnedOrigin = authorityReturnedOrigin;
}
public async Task<OpenIdConnectConfiguration> GetConfigurationAsync(CancellationToken cancel)
{
var httpClient = new HttpClient();
var request = new HttpRequestMessage
{
RequestUri = new Uri($"{authority}/.well-known/openid-configuration"),
Method = HttpMethod.Get
};
var configurationResult = await httpClient.SendAsync(request, cancel);
var resultContent = await configurationResult.Content.ReadAsStringAsync(cancel);
if (configurationResult.IsSuccessStatusCode)
{
var config = OpenIdConnectConfiguration.Create(resultContent);
var jwks = config.JwksUri.Replace(authorityReturnedOrigin, authority);
var keyRequest = new HttpRequestMessage
{
RequestUri = new Uri(jwks),
Method = HttpMethod.Get
};
var keysResposne = await httpClient.SendAsync(keyRequest, cancel);
var keysResultContent = await keysResposne.Content.ReadAsStringAsync(cancel);
if (keysResposne.IsSuccessStatusCode)
{
config.JsonWebKeySet = new JsonWebKeySet(keysResultContent);
var signingKeys = config.JsonWebKeySet.GetSigningKeys();
foreach (var key in signingKeys)
{
config.SigningKeys.Add(key);
}
}
else
{
throw new Exception($"Failed to get jwks: {keysResposne.StatusCode}: {keysResultContent}");
}
return config;
}
else
{
throw new Exception($"Failed to get configuration: {configurationResult.StatusCode}: {resultContent}");
}
}
public void RequestRefresh()
{
// if you are caching the configuration this is probably where you should invalidate it
}
}
然后替换Startup.cs
中的默认配置管理器:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = appSettings.Authorization.Authority;
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
options.TokenValidationParameters.ValidateAudience = false;
options.ConfigurationManager = new CustomConfigurationManager("authority", "authorityReturnedOrigin");
}
修复过期的证书链
这适用于 Linux Mint 20.1,其他发行版可能有不同的处理方式。还建议备份。
- 确保 openssl 版本高于 1.0.1 或 1.0.2,因为它们可能存在问题(运行
openssl version
进行检查) - 运行
ls -l | grep ISRG
并删除您找到的内容 - 这一步可能不是必需的,但我就是这样做的 - 转到
/etc/ssl/certs
- 运行
ls -l | grep DST
并删除你找到的内容 - 下载这些文件:isrgrootx1 isrgrootx2 letsencrytptr3
- 将它们复制到
/etc/ssl/certs
请注意 运行ning 证书更新命令,如update-ca-certificate
可能会恢复此状态。
可能有更好的解决方案,也许只需更新到更新的 OS 版本就可以解决这个问题。这个问题只出现在 API 从 Linux Mint 20.1 上的代码 运行 对 API 进行的 http 调用中,在 Windows 上一切正常,通过浏览器访问没有显示任何内容SSL 证书错误。