Click here to Skip to main content
15,867,686 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
Hello together,

I hang unsuccessfully for days on this problem and not a single answer to different posts at different websites helped me so solve it.

I`m working on a Windows 10 System and implementing with VisualStudio 2017.
With AspNetCore I`ve implemented the following projects:

1.) Web.AuthServer: IdentityServer4 for authentication.
2.) Web.ApiServer: The first SignalR-Server.
3.) Web.ApiSwitch: The second SignalR-Server.
It has a HostedService with 2 SignalR-Clients as
a "bridge" between the two SignalR-Servers.>


The Web.ApiSwitch starts his HostedService which connects to itself and the Web.ApiServer including authentication at Web.AuthServer. This worked well as long as they ran with some "localhost:PORT" URL.

Now I`ve tried to run all the projects with "MyIP:PORT". The Web.AuthServer is using HTTPS together with a self signed certificate (generated with OpenSSL).
The certificate itself has beend build with the following command lines:

Generating private key:
openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout IdentityServer4Auth.key -out IdentityServer4Auth.crt -subj "/CN=example.com" -days 3650


Generating the certificate:
openssl pkcs12 -export -out IdentityServer4Auth.pfx -inkey IdentityServer4Auth.key -in IdentityServer4Auth.crt -certfile IdentityServer4Auth.crt


The file has been added to mmc:
1.) File -> Add or Remove Snap-ins -> Certificates -> Add -> Computer Account -> OK
2.) Import the certificate (.cer) to personal -> Trusted Root Certification Authorities)
3.) Import the pfx, with exportable private key support, to personal -> certificates.

Code of Web.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();


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


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


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();


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


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


Token request for SignalR clients:

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


The TokenRetriever of ConfigureServices from Web.ApiServer is just for getting the authentication of SignalR clients running, in due to the reason that they`re passing tokens via query string. It does the job.

Now the problem:

The clients of the HostedService of Web.ApiServer are trying to get the authentication token (jwt bearer) from Web.AuthServer but every time i get
the following exception:

System.Security.Authentication.AuthenticationException: 'The remote certificate is invalid according to the validation procedure.'


If I open the browser and type in the adress of Web.AuthServer "MyIP:5000" everything is working fine, after I accept the self signed certificate.
But the clients of the HostedService of Web.ApiServer can`t do this.
How do I ged rid of this exception and get some valid certificate? Am I missing something at client implementation? Hopefully someone can help me - getting stucked at this since more than 4 days.

What I have tried:

- Building different certificates with EasyRSA, OpenSSL, the tools from Windows Kits 10.

- Serveral code changes.. to much to write down here.
Posted
Updated 10-Jun-20 22:21pm

If you want to use a self-signed certificate, then you'll need to bypass the certificate validation. There are several suggestions in this StackOverflow thread[^]. For example:
C#
using (var httpClientHandler = new HttpClientHandler())
{
    // NB: You should make this more robust by actually checking the certificate:
    httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
    
    using (var client = new HttpClient(httpClientHandler))
    {
        // Make your request...
    }
}

HttpClientHandler.ServerCertificateCustomValidationCallback Property (System.Net.Http) | Microsoft Docs[^]
 
Share this answer
 
Hi Richard, this does only eliminate some exceptions, but I still get the exception that the remote certificate isn`t valid.

If I start the Web.AuthServer and afterwards only the Web.ApiServer everything is fine. But the Web.ApiServer tries to get a SignalR-Connection to Web.ApiSwitch and if this one has been started the exception raises after returning the response token, which has success code 200.

APISwitchConnection = new HubConnectionBuilder()
    .WithUrl(apiSwitchUrl, options =>
    {
        options.AccessTokenProvider = async () =>
        {
            logger.LogInformation(string.Format("Hosted hub connection service requesting access token for {0}..", EventHub.PlantName));
            var response = await TokenService.RequestTokenAsync(_settings.ISAuthenticationSettings.Authority,
                                                                EventHub.PlantName,
                                                                                                    _settings.ISAuthenticationSettings.ApiName);
            return response.AccessToken;
        };
    })
    .Build();



public static async Task<TokenResponse> RequestTokenAsync(string authority, string clientID, string scope)
{
    using (var httpClientHandler = new HttpClientHandler())
    {
        httpClientHandler.ServerCertificateCustomValidationCallback += (message, cert, chain, erros) => true;

        using (var client = new HttpClient(httpClientHandler))
        {
            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 = clientID + "-SecretValue",
                Scope = scope
            });

            if (response.IsError)
            {
                throw new Exception(response.Error);
            }

            return response;
        }
    }
}
 
Share this answer
 
Hi,

Did you get any solution for this ? I am also facing similar issue on linux using docker.

Regards,
Rajesh
 
Share this answer
 
Comments
CHill60 11-Jun-20 7:50am    
If you have a question or a comment about a post then use the "Have a Question or Comment?" link next to it. The member will receive a notification about your message.
I suggest you remove this post as it is not a solution

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900