Click here to Skip to main content
14,427,846 members
Rate this:
Please Sign up or sign in to vote.
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 26-Mar-19 20:52pm
Rate this:
Please Sign up or sign in to vote.

Solution 1

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:
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[^]
   
Rate this:
Please Sign up or sign in to vote.

Solution 2

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

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




CodeProject, 503-250 Ferrand Drive Toronto Ontario, M3C 3G8 Canada +1 416-849-8900 x 100