I am writing a test to get a token from identity server4 using Microsoft.AspNetCore.TestHost
var hostBuilder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers())
;
})
.Configure(app =>
{
app.UseIdentityServer();
});
var server = new TestServer(hostBuilder);
var client = server.CreateClient();
client.BaseAddress = new Uri("http://localhost:5000");
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
Then disco.Error comes up with the following error
Error connecting to
http://localhost:5001/.well-known/openid-configuration: An error
occurred while sending the request.
What am i missing?
The discovery client is obviously doing an external call to that actual address. You want it to call the test server that happens to "live" InMemory.
Take a look at these tests here for IdentityServer4 that tests the discovery document.
To answer your question though you need to use one of the overloaded methods for the DiscoveryClient that takes in a handler that would make the correct "call" to your InMemory test server. Below is an example of how this could be done.
var server = new TestServer(hostBuilder);
var handler = server.CreateHandler();
var discoveryClient = new DiscoveryClient("http://localhost:5000", handler);
var discoveryDocument = await discoveryClient.GetAsync();
Also I highly recommend going over the IdentityServer4 integration tests if youre going to be doing some of your own tests like this.
Related
I am creating a test server for integration tests like so, and this works well for all HttpClient requests...
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "IntegrationTesting");
var appBuilder = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped(sp => MockUserMessagingService.Object);
services.AddSingleton(sp => MockDealerStrategy.Object);
});
builder.UseSetting("https_port", "8080");
});
HttpClient = appBuilder.CreateClient();
I now also need to connect a ClientWebSocket to that test server. Can someone tell me how that is done?
I am trying to test my SignalR connections in my integration tests.
The client looks like this:
var connection = new HubConnectionBuilder()
.WithUrl(
$"{client.BaseAddress}meeting-notifications",
o =>
{
o.HttpMessageHandlerFactory = _ => Server?.CreateHandler();
})
.Build();
connection.On<BoardDto>("BoardStateChanged", board => { Do Something... });
await connection.StartAsync();
I am calling the method in my ASP-NET Core backend like so:
public async Task BroadcastBoardStateAsync(int boardId, BoardDto board)
{
await _notificationHub.Clients.All.BoardStateChanged(board);
}
The client is able to call a method on the server but not the other way around.
Does anyone know what I am missing here?
Edit: I debugged the server call and the _notificationHub contains the connection-id of the client.
Turns out SignalR v3.x does json serialization via System.Text.Json which had some problem with my POCO's.
To fix this, I had to explicitly tell SignalR to use NewtonsoftJson for serialization via this method call:
var connection = new HubConnectionBuilder()
.WithUrl(
$"{client.BaseAddress}meeting-notifications",
o =>
{
o.HttpMessageHandlerFactory = _ => Server?.CreateHandler();
})
----> .AddNewtonsoftJsonProtocol()
.Build();
The current version of the Microsoft.Azure.Functions.Extensions package exposes an additional property that allows you easy access to the IConfiguration provided to the function. Previously this required manually building a service provider, which was obviously problematic.
Using that package my FunctionsStartup.cs looks like this:
public override void Configure(IFunctionsHostBuilder builder)
{
base.Configure(builder);
var config = builder.GetContext().Configuration; // new in v1.1.0 of Microsoft.Azure.Functions.Extensions
var mySetting = config["MySetting"];
int.Parse(mySetting, out var mySetting);
// ... use mySetting...
}
In order to test my HTTP-triggered functions I've used this article as a base, which details how to manually build and start a host to execute my function as if it was running in Azure, similar to how TestServer works in ASP.NET Core:
var host = new HostBuilder()
.ConfigureWebJobs(new FunctionsStartup().Configure)
.Build();
var functionsInstance = ActivatorUtilities.CreateInstance<MyFunctions>(host.Services);
I can then execute the function methods defined on MyFunctions to test their responses:
var request = new DefaultHttpRequest(new DefaultHttpContext());
var response = (OkObjectResult)functionsInstance.HttpTriggerMethod(request);
... assert that response is valid
The problem is that when I run my tests, builder.GetContext().Configuration is returning null in FunctionsStartup.Configure, which of course causes those tests to fail. How can I work around this?
The article I linked to hasn't been updated to take into account the existence of builder.GetContext().Configuration, but you can make this work for testing purposes with a little tweaking. Instead of using:
var host = new HostBuilder()
.ConfigureWebJobs(new FunctionsStartup().Configure)
.Build();
you need to explicitly copy the host's settings into a new WebJobsBuilderContext that you then pass to your function's startup:
var host = new HostBuilder()
.ConfigureWebJobs((context, builder) => new FunctionsStartup().Configure(new WebJobsBuilderContext
{
ApplicationRootPath = context.HostingEnvironment.ContentRootPath,
Configuration = context.Configuration,
EnvironmentName = context.HostingEnvironment.EnvironmentName,
}, builder))
.Build();
I'm not sure if this is the completely correct way to achieve this, but it has worked well for me.
I need to implement Client Certificate authentication on some of the endpoints in my .NET 5 Web API. So I don't want to enable HTTPS across all endpoint as described here in the MS docs. I am using Kestrel on my local machine and not IIS express or IIS.
I have tried the following three methods with no luck on either of them:
var clientCertHeaders = context.HttpContext.Request.Headers;
This one returns the normal headers for the request but no certificate.
var clientCert = context.HttpContext.Connection.ClientCertificate;
var clientCertAsync = context.HttpContext.Connection.GetClientCertificateAsync().Result;
These two both return null.
I've tried applying the following to my services:
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-SSL-CERT";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if(!string.IsNullOrWhiteSpace(headerValue))
{
var bytes = Encoding.UTF8.GetBytes(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
Even with that enabled in my services I am not retrieving the client certificate.
I am using Postman to make the requests to the API requests.
You need to configure Kestrel to allow client certificates in the program.cs The default value is ClientCertificateMode.NoCertificate so in your ConfigureWebHostDefaults you need to change that to ClientCertificateMode.AllowCertificate.
Here's an edited chunk of code from the docs you sent where I did that:
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureKestrel(o =>
{
o.ConfigureHttpsDefaults(o =>
o.ClientCertificateMode =
ClientCertificateMode.AllowCertificate);
});
});
}
A bit of background, I have an IdenityServer 4 project that I use to protect access to an mvc project that I have (Using ASP.NET Identity).
Now what I also wanted was an api that is protected via client credentials that returns some information.
What I did was make a new core api project and this was working fine with the client protection, however, I wanted to move the api so it was within IdenityServer.
e.g. localhost:5000/api/info/getinfo
Now I have moved the code over I get a 500 error when I use the attribute [Authorize(AuthenticationSchemes = "Bearer")]
I can use the DiscoveryClient to get a successful token using the credentials but can't with any request unless they are not authorized.
So in ID I set up my start up like this:
services.AddMvc();
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
// Configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddInMemoryApiResources(Config.GetApiResources())
.AddAspNetIdentity<ApplicationUser>();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetSection("Authority").Value;
options.RequireHttpsMetadata = false;
options.ApiName = "IdentityInfoApi";
});
And then for my api call that is protected I tag it with: [Authorize(AuthenticationSchemes = "Bearer")]
but this returns me a 500 error, now when I use the tag: [Authorize] it works but that's because the user is logged into the mvc app and the response is an html page and not the json object i want.
At the moment I'm using a unit test to hit the api and the code looks like this:
var client = new HttpClient();
var disco = DiscoveryClient.GetAsync("https://localhost:5000").Result;
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = tokenClient.RequestClientCredentialsAsync("IdentityInfoApi").Result;
client.SetBearerToken(tokenResponse.AccessToken);
var response = client.GetAsync("https://localhost:5000/api/info/getinfo").Result;
if (!response.IsSuccessStatusCode)
{
var userResult = response.Content.ReadAsStringAsync().Result;
var result = JsonConvert.DeserializeObject<PagedUserList>(userResult);
Assert.NotNull(result);
}
Is there something wrong with my setup of ID, the client code or can you not use ID in this way?
Thank's for your help
After a lot of playing around I believe I found the fix.
You must define AddAuthentication() before AddIdentity() or in other words you must configure the api before Identity Server
It's fine to do it any way round if your api is external but not if it is within the Identity Server app it'self.
My new code looks like this:
//Configure api
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "IdentityInfoApi";
});
//end
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration.GetSection("SMTP"));
services.AddMvc();
// Configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddInMemoryApiResources(Config.GetApiResources())
.AddAspNetIdentity<ApplicationUser>();
Hope this helps anyone else