#asp.net-core #certificate #signalr #openid #identityserver4
#asp.net-ядро #сертификат #signalr #OpenID #identityserver4
Вопрос:
Я несколько дней безуспешно зависаю над этой проблемой, и ни один ответ на разные сообщения на разных веб-сайтах не помог мне решить ее.
Я работаю в системе Windows 10 и внедряю с помощью VisualStudio 2017. С AspNetCore я реализовал следующие проекты:
1.) Веб.AuthServer: IdentityServer4 для аутентификации.
2.) Web.ApiServer: первый SignalR-сервер.
3.) Web.ApiSwitch: второй сервер SignalR. У него есть HostedService с 2 SignalR-клиентами в качестве «моста» между двумя SignalR-серверами.>
Web.ApiSwitch запускает свой HostedService, который подключается к самому себе и Web.ApiServer, включая аутентификацию в Web.AuthServer. Это работало хорошо, пока они запускались с некоторым URL-адресом «localhost: PORT».
Теперь я попытался запустить все проекты с помощью «MyIP: PORT». Веб.AuthServer использует HTTPS вместе с самозаверяющим сертификатом (сгенерированным с помощью OpenSSL). Сам сертификат был создан с помощью следующих командных строк:
Генерация закрытого ключа:
openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout IdentityServer4Auth.key -out IdentityServer4Auth.crt -subj "/CN=example.com" -days 3650
Генерация сертификата:
openssl pkcs12 -export -out IdentityServer4Auth.pfx -inkey IdentityServer4Auth.key -in IdentityServer4Auth.crt -certfile IdentityServer4Auth.crt
Файл был добавлен в mmc:
1.) Файл -> Добавить или удалить оснастки -> Сертификаты -> Добавить -> Учетная запись компьютера -> ОК 2.) Импортируйте сертификат (.cer) в личные -> Доверенные корневые центры сертификации) 3.) Импортируйте pfx с поддержкой экспортируемого закрытого ключа в personal -> certificates.
Веб-код.AuthServer:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseKestrel(options =>
{
options.Listen(IPAddress.Any, 5000, listenOptions =>
{
listenOptions.UseHttps();
});
})
.UseStartup<Startup>()
.ConfigureLogging(builder =>
{
builder.ClearProviders();
builder.AddSerilog();
})
.Build();
Веб.AuthSever — ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
// Gets connection strings from "appsettings.json".
string csApplicationContext = Configuration.GetConnectionString("ApplicationContext");
string csConfigurationStore = Configuration.GetConnectionString("ConfigurationStore");
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
var settings = JsonFileManager<ServerSettings>.Load(AppDomain.CurrentDomain.BaseDirectory "Config\svConf.json");
// Add cross origin resource sharing.
services.AddCors(options =>
{
options.AddPolicy("default", policy =>
{
policy.WithOrigins(settings.CorsOrigins)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
// Add bearer token authentication.
services.AddAuthentication()
.AddJwtBearer(jwt =>
{
jwt.Authority = settings.JWTBearerSettings.Authority;
jwt.Audience = settings.JWTBearerSettings.Audience;
jwt.RequireHttpsMetadata = settings.JWTBearerSettings.RequireHttpsMetadata;
jwt.Validate();
});
services.AddPolicyServerClient(Configuration.GetSection("Policy"))
.AddAuthorizationPermissionPolicies();
// DB und User registieren für DI
services.AddDbContext<ApplicationDbContext>(builder =>
builder.UseSqlite(csApplicationContext, sqlOptions =>
sqlOptions.MigrationsAssembly(migrationsAssembly)));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddTransient<IClientStore, ClientService>();
// Add IS4 as authentication server.
var is4Builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseInformationEvents = true;
})
// Add config data (clients, resources, CORS).
.AddConfigurationStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlite(csConfigurationStore, sqlOptions =>
sqlOptions.MigrationsAssembly(migrationsAssembly)))
.AddClientStore<ClientService>()
.AddAspNetIdentity<ApplicationUser>();
SigninCredentialExtension.AddSigninCredentialFromConfig(is4Builder, Configuration.GetSection("SigninKeyCredentials"), Logger);
services.AddMvc(options =>
{
// this sets up a default authorization policy for the application
// in this case, authenticated users are required (besides controllers/actions that have [AllowAnonymous]
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
options.SslPort = 5000;
options.Filters.Add(new RequireHttpsAttribute());
});
}
Web.AuthSever — Configure:
public async void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
else
app.UseExceptionHandler("/Home/Error");
// Use specific cross origin resource sharing configuration.
app.UseCors("default");
app.UseDefaultFiles();
app.UsePolicyServerClaims();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseIdentityServer();
// Adding test data to database.
await InitializeDbTestData.GenerateTestData(app);
app.UseMvcWithDefaultRoute();
}
Web.AuthSever — SigninCredentialExtension:
public static class SigninCredentialExtension
{
private const string KeyType = "KeyType";
private const string KeyTypeKeyFile = "KeyFile";
private const string KeyTypeKeyStore = "KeyStore";
private const string KeyTypeTemporary = "Temporary";
private const string KeyFilePath = "KeyFilePath";
private const string KeyFilePassword = "KeyFilePassword";
private const string KeyStoreIssuer = "KeyStoreIssuer";
public static IIdentityServerBuilder AddSigninCredentialFromConfig(
this IIdentityServerBuilder builder, IConfigurationSection options, ILogger logger)
{
string keyType = options.GetValue<string>(KeyType);
logger.LogDebug($"SigninCredentialExtension keyType is {keyType}");
switch (keyType)
{
case KeyTypeTemporary:
logger.LogDebug($"SigninCredentialExtension adding Developer Signing Credential");
builder.AddDeveloperSigningCredential();
break;
case KeyTypeKeyFile:
AddCertificateFromFile(builder, options, logger);
break;
case KeyTypeKeyStore:
AddCertificateFromStore(builder, options, logger);
break;
}
return builder;
}
public static X509Certificate2 GetCertificateByThumbprint(string thumbprint)
{
using (X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
certStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
if (certCollection.Count > 0) return certCollection[0];
}
return null;
}
private static void AddCertificateFromStore(IIdentityServerBuilder builder,
IConfigurationSection options, ILogger logger)
{
var keyIssuer = options.GetValue<string>(KeyStoreIssuer);
logger.LogDebug($"SigninCredentialExtension adding key from store by {keyIssuer}");
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindByIssuerName, keyIssuer, true);
if (certificates.Count > 0)
{
builder.AddSigningCredential(certificates[0]);
builder.AddValidationKey(certificates[0]);
}
else
logger.LogError("A matching key couldn't be found in the store");
}
private static void AddCertificateFromFile(IIdentityServerBuilder builder,
IConfigurationSection options, ILogger logger)
{
var keyFilePath = options.GetValue<string>(KeyFilePath);
var keyFilePassword = options.GetValue<string>(KeyFilePassword);
if (File.Exists(keyFilePath))
{
logger.LogDebug($"SigninCredentialExtension adding key from file {keyFilePath}");
builder.AddSigningCredential(new X509Certificate2(keyFilePath, keyFilePassword));
}
else
{
logger.LogError($"SigninCredentialExtension cannot find key file {keyFilePath}");
}
}
}
Code of Web.ApiServer:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseKestrel(options =>
{
options.Listen(IPAddress.Any, 5004, listenOptions =>
{
listenOptions.UseHttps();
});
})
.UseStartup<Startup>()
.ConfigureLogging(builder =>
{
builder.ClearProviders();
builder.AddSerilog();
})
.Build();
Web.ApiServer — ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
// Add cross origin resource sharing.
services.AddCors(options =>
{
options.AddPolicy("default", policy =>
{
policy.WithOrigins(_settings.CorsOrigins)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
// Add bearer token authentication and our IS4 as authentication server.
services.AddAuthentication(_settings.ISAuthenticationSettings.DefaultScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = _settings.ISAuthenticationSettings.Authority;
options.RequireHttpsMetadata = _settings.ISAuthenticationSettings.RequireHttpsMetadata;
options.ApiName = _settings.ISAuthenticationSettings.ApiName;
// Handling the token from query string in due to the reason
// that signalR clients are handling them over it.
options.TokenRetriever = new Func<HttpRequest, string>(req =>
{
var fromHeader = TokenRetrieval.FromAuthorizationHeader();
var fromQuery = TokenRetrieval.FromQueryString();
return fromHeader(req) ?? fromQuery(req);
});
options.Validate();
});
// Add singalR as event bus.
services.AddSignalR(options => options.EnableDetailedErrors = true);
services.AddMvcCore(options =>
{
options.SslPort = 5003;
options.Filters.Add(new RequireHttpsAttribute());
})
.AddAuthorization()
.AddJsonFormatters();
// Register ConnectionHost as hosted service with its wrapper class.
services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService, ConnectionHost>();
}
Web.ApiServer — Настроить:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
app.UseHttpsRedirection();
// Has to be called before UseSignalR and UseMvc!
app.UseAuthentication();
// Use specific cross origin resource sharing configuration.
app.UseCors("default");
app.UseSignalR(routes => routes.MapHub<EventHub>("/live"));
app.UseMvc();
}
Запрос токена или клиенты SignalR:
public static async Task<TokenResponse> RequestTokenAsync(string authority, string clientID, string scope)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(authority);
if (disco.IsError) throw new Exception(disco.Error);
var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = clientID,
ClientSecret = "SomeTestSecret",
Scope = scope
});
if (response.IsError)
{
throw new Exception(response.Error);
}
return response;
}
TokenRetriever для ConfigureServices от Web.ApiServer предназначен только для проверки подлинности запущенных клиентов SignalR, по той причине, что они передают токены через строку запроса. Он выполняет свою работу.
Теперь проблема:
Клиенты HostedService Web.ApiServer пытаются получить токен аутентификации (jwt bearer) из Web.AuthServer но каждый раз, когда я получаю следующее исключение:
System.Security.Authentication.AuthenticationException: 'The remote certificate is invalid according to the validation procedure.'
Если я открою браузер и введу веб-адрес.AuthServer «MyIP: 5000» все работает нормально, после того, как я принимаю самозаверяющий сертификат.
Но клиенты HostedService Web.ApiServer не могут этого сделать.
Как мне ged избавиться от этого исключения и получить какой-либо действительный сертификат? Я что-то упускаю при реализации клиента? Надеюсь, кто-нибудь сможет мне помочь — застрял в этом более чем на 4 дня.
Ответ №1:
Чтобы клиенты доверяли серверу, они проверяют ряд свойств сертификата, который сервер предоставляет для TLS, таких как «является ли сертификат для ожидаемого домена», «истек ли срок действия сертификата». Одна из вещей, которую клиент проверит, — это цепочка сертификатов, которая является цепочкой доверия.
https://knowledge.digicert.com/solution/SO16297.html
Когда вы покупаете сертификат у центра сертификации, это отчасти то, что вы покупаете — например, давайте посмотрим на сертификат, используемый Facebook.
У них есть подстановочный сертификат, который будет работать для всех поддоменов facebook, а доверенным корневым центром сертификации является DigiCert (https://www.digicert.com/welcome/compatibility.htm ). Используя центр сертификации Digicert, которому широко доверяют, клиент знает, что сертификат Facebook был выдан Digicert, и поэтому можно доверять Facebook certifcate.
Это та часть, которую вы упускаете. Вы используете самозаверяющий сертификат, ваши клиенты не знают о корневом центре сертификации и не могут установить цепочку доверия. Принимая сертификат вручную, вы устраняете основную причину, но, очевидно, это не работает для клиентов, которых вы не полностью контролируете.
https://letsencrypt.org предоставьте бесплатную службу CA, которая сейчас работает на большом количестве клиентов — для многих вещей это достойное решение, поддерживающее автоматическое обновление сертификата. Итак, вместо использования самозаверяющего сертификата сгенерируйте сертификат для своего сервера с помощью letsencrypt (есть множество статей о том, как это сделать)
Использование сертификата, выданного центром сертификации, которому доверяют клиенты, является правильным решением этой проблемы.