ASP.NET Core REST API with React SPA - c#

I have an ASP.NET Core web application that hosts a REST API. I'm attempting to add a React-based Single-Page Application (SPA) to the server but the application is not using MVC.
When I enable a Router my app no longer loads. If I disable the Router and only host the static app, it loads correctly, but of course it can't communicate with the web API.
I've also sucessfully hosted my SPA using Python's built-in web server and using a simple file server written in Go, but, again, then it can't "talk" to my API without running separate services and or proxying/redirecting URLs. My goal is a single, C# application that can serve both the API and the static user interface.
Here are relevant portions of my MyStartup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddLogging();
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
app.UseDefaultFiles();
app.UseStaticFiles(new StaticFileOptions {
ServeUnknownFileTypes = true
});
app.UseRouter(buildRoutes(app));
}
private IRouter buildRoutes(IApplicationBuilder app)
{
// TODO: Figure out how to add a default (and versioned) API base URL to the Router...
string url(string path) {
return $"api/{path}";
}
RouteBuilder routeBuilder = new RouteBuilder(app, new RouteHandler(null));
/// Notify API
routeBuilder.MapGet(url("status"), GetControllerStatus);
routeBuilder.MapPost(url("heartbeat"), GotHeartbeat);
routeBuilder.MapGet(url("items/{id}"), httpCtx => {
string id = httpCtx.GetRouteValue("id").ToString();
return RetrieveItem(httpCtx, id);
});
// ... More routes ...
return routeBuilder.Build();
}
and my Main class contains this excerpt:
IWebHost host =
new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseUrls("http://*:11111")
.UseStartup<MyStartup>()
.Build();
host.Run();
I've investigated the following tools and tutorials, but they all seem to require different approaches using MVC:
https://github.com/aspnet/JavaScriptServices
https://learn.microsoft.com/en-us/aspnet/core/client-side/spa-services?view=aspnetcore-2.0#routing-helpers
https://github.com/bradymholt/aspnet-core-react-template/tree/master/api
https://github.com/aspnet/templating/blob/dev/src/Microsoft.DotNet.Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs
https://github.com/aspnet/JavaScriptServices/issues/973#issuecomment-303766597 (very relevant!)
https://github.com/aspnet/JavaScriptServices/issues/1150 (very relevant!)
Is it possible to host an SPA from an ASP.NET Core application that isn't using MVC?

Related

How do you restrict requests so they only work if it is accessed by a specific domain

I have this ASP.NET Web API and I want to restrict access so it only works when called by specific host. I cannot, for what I know until now, secure it by token, because the WEB API url will be a postback url for a system that will call it automatically when a certain action is made. I have checked out CORS, but what CORS seems to do is to allow a specific domain to access the API. So, does this mean that my WEB API is already restricted for other domains? Then why I can access it by Postman locally, even if it is hosted in Azure?
I just want my service to allow calls from localhost and another specific domain only.
How do I achieve this?
Thanks!
One possibility is to use a custom authorization filter that creates an HTTP response with a failure status code like 400 bad request or 404 not found if the requests has a host that is not allowed. We could define an authorization filter named RestrictDomain that looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public class RestrictDomainAttribute : Attribute, IAuthorizationFilter
{
public IEnumerable<string> AllowedHosts { get; }
public RestrictDomainAttribute(params string[] allowedHosts) => AllowedHosts = allowedHosts;
public void OnAuthorization(AuthorizationFilterContext context)
{
// Get host from the request and check if it's in the enumeration of allowed hosts
string host = context.HttpContext.Request.Host.Host;
if (!AllowedHosts.Contains(host, StringComparer.OrdinalIgnoreCase))
{
// Request came from an authorized host, return bad request
context.Result = new BadRequestObjectResult("Host is not allowed");
}
}
}
If you want to apply the RestrictDomain filter globally, then you can add the filter in the Startup.cs file like this:
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
// Add the restrict domain filter globally
// You could read the allowed hosts from a config file, here we hard code them
options.Filters.Add(new RestrictDomainAttribute("localhost", "example.com"));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
With this setting, if I remove "localhost" from the constructor and only allow "example.com", I get a 400 request when I use Postman since the host will be localhost.
Another option is to use the filter in a controller or controller action directly since we configure it to work as an attribute. However, the allowed hosts will have to be constant values instead of values that can be calculated at runtime. Here's an example:
using Microsoft.AspNetCore.Mvc;
[Route("")]
[ApiController]
public class HomeController : ControllerBase
{
[HttpGet]
public ContentResult Index() => Content("Home");
[HttpGet("greeting")]
[RestrictDomain("localhost", "example.com")] // values must be constants
public ContentResult Greeting() => Content("Hello, World!");
}
If you don't want constant values and don't want to apply the filter globally, then you could inject the IConfiguration into the filter to read the allowed hosts that can access the resource.
CORS on its own only tells the browser / client apps to stop sending requests, in many ASP.Net Web API implementation it doesn't actually block the request pipeline. This is why Postman works, Postman doesn't execute the same pre-flight OPTIONS check first, it just send the request.
You should absolutely look into adding authentication to your API, Bearer token based authentication works well enough with and between APIs. Have a read over Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2, but that is out of scope for this question.
At a conceptual level, you just need to intercept the incoming request in the OWIN pipeline before the API middleware and request the request if it doesn't match your rules. This should be using in conjunction with CORS so that browsers respond in a standard way.
For background, have a read over Block or limit unwanted traffic to Asp.Net Web Application. Usually we manage domain or IP level security filtering in the hosting architecture or routing. IIS or Azure hosts have many built in policies to help manage this.
You could implement this at a global level by adding the following OWIN request processor:
public void ConfigureOAuth(IAppBuilder app)
{
app.Use((context, next) =>
{
string[] AllowedDomains = new string[] { "::1", "localhost", "mybusinessdomain.com" };
if (!AllowedDomains.Contains(context.Request.Host.Value.ToLower()))
{
context.Response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
return System.Threading.Tasks.Task.FromResult<object>(null);
}
return next();
});
// TODO: add in your other OWIN configuration AFTER the request filter.
}
If you are NOT using the OWIN hosting pipeline and you want to manage this globally then you could put a check into the global.asax.cs toi handle the Application_BeginRequest:
private static readonly string[] AllowedDomains = new string[] { "::1", "localhost", "mybusinessdomain.com" };
protected void Application_BeginRequest(Object sender, EventArgs e)
{
if >(!AllowedDomains.Contains(HttpContext.Current.Request.UserHostName.ToLower()))
{
HttpContext.Current.Response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
HttpContext.Current.Response.End();
// or transfer to a specific page
// Server.Transfer("~/banned.aspx");
}
}

Implement two authentication options (Token and Certificate) in ASP Net Core

[Target netcoreapp3.1]
Hi there! So I have this Web Api that is protected by a middleware of this form in my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
//other services configuration
services.AddProtectedWebApi(options => { /* config */};
//other services configuration
}
This verifies Jwt Tokens issued by Azure and grants access to the API; it works fine.
At present, I have a front-end angular client website where a user signs in via Azure AD. Angular sends the token to my web API and everything works.
I would now like to use the same webapp to handle query requests from a user without credentials, but with a client certificate that would have been provided in advance. So basically, I'd like to authenticate on my Angular WebSite via Azure OR via a client cert. Angular would then follow up the information to my webapp, which would in turn authenticate the user with the appropriate method.
To be clear, I still want someone to be able to log in without a certificate by using his Azure account.
Is there a simple way to have two authentication options in this case without having to create a separate webapp? I read a bit there : https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.1#optional-client-certificates
But it seems it'd only work on the preview of ASP.NET Core 5, which I can't use in my situation.
Hope what follows will help someone!
I eventually found this link : https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-3.1
It explains how to implement multiple authorization policies that both have a chance to succeed. Below is the solution I found using IIS after a bit more research:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//other services configuration
services.Configure<IISOptions>(options =>
{
options.ForwardClientCertificate = true;
});
services.Configure<CertificateForwardingOptions>(options =>
{
options.CertificateHeader = {/*your header present in client request*/};
});
//other services configuration
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes =/*Whatever you need*/;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
if ({/*CertValidationClass*/}.ValidateCertificate(context.ClientCertificate))
{
context.Success();
}
else
{
context.Fail("invalid cert");
}
return Task.CompletedTask;
}
};
});
services.AddProtectedWebApi(options => { /* config */};
//other services configuration
}
{CertValidationClass} being a service or helper class custom made to verify all I have to verify to approve the certificate. Obviously you can add a lot more verifying and actions on your own to this template.
I already had app.UseAuthentication(); app.UseAuthorization(); in my middleware pipeline, no need to change that, but you do have to add app.UseCertificateForwarding(); before these two.
Now I just had to specify above the controller I wanted to protect that I wanted to use both Authorization methods, and just like that, if one fails, it falls back on the other and it works perfectly, I tested by making requests via Insomnia with/without tokens and with/without certficates.
MyApiController.cs
[Authorize(AuthenticationSchemes = AuthSchemes)]
public class MyApiController
{
//Just add the schemes you want used here
private const string AuthSchemes =
CertificateAuthenticationDefaults.AuthenticationScheme; + "," +
JwtBearerDefaults.AuthenticationScheme;

How to enable the.net core web api to receive request form any subdomain?

I want to develop a reverse proxy in .net core api and I want to use subdomains to route requests to different services. Subdomains are not specified already rather it will be processed dynamically
I have tried to use the following CORS policy
but my middlewares does not capture any request coming through subdomains rather only handle which are coming from http://www.localproxy.com
namespace OrchestratorReverseProxy
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddCors
(
options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.SetIsOriginAllowedToAllowWildcardSubdomains()
.WithOrigins("http://*.localproxy.com")
.AllowAnyMethod()
.AllowCredentials()
.AllowAnyHeader()
.Build()
);
}
);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseCors("CorsPolicy");
app.UseMiddleware<DarkLaunchMiddleware>();
app.UseMiddleware<ReverseProxyMiddleware>();
}
}
}
The browser shows response like
This site can’t be reached www.the.localproxy.com’s server IP address could not be found.
Try running Windows Network Diagnostics.
DNS_PROBE_FINISHED_NXDOMAIN

SignalR in ASP.Net Core 401 Unauthorized

I have a small problem with my UWP app. First, the UWP app has the following capabilities:
Enterprise Authentication
Shared user certificates
Private Networks
User Account Information
Now I want to connect to a SignalR-Hub in an ASP.NET Core 2.1 Web API. The hub looks like this:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Test.Namespace
{
[Authorize]
public class SyncHub : Hub
{
public void SendUpdate()
{
Clients.All.SendAsync("Update");
}
}
}
And this is my Startup.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Test.Namespace
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseSignalR(routes =>
{
routes.MapHub<SyncHub>("/syncHub");
});
app.UseMvc();
}
}
}
The whole API runs on an IIS with Windows Authentication configured. The Active Directory runs on the same machine.
And this is how my UWP app calls the Service:
HubConnection connection = new HubConnectionBuilder().WithUrl("http://Server:81/syncHub", options => {
options.UseDefaultCredentials = true;
}).Build();
await connection.StartAsync();
This call always throws a 401.
What am I doing wrong? I work on this Problem for more than a week now and I can't figure out why it is not working.
Thanks to all who will help me :)
Edit: So I tried a few thinks today and found out, that this is not a problem of SignalR itself.I created a ASP.NET Core console app with the exact same call and everything works fine. It also works when I hardcode the credentials in the UWP app. It only doesn't work when I use "UseDefaultCredentials" in UWP. I am completly clueless. I have rechecked the capabilities but this doesn't help either.
It seems app.UseHttpsRedirection(); fail to redirect the client credentials.
Try to make a test with https url.
var hubConnectionBuilder = new HubConnectionBuilder();
var hubConnection = hubConnectionBuilder.WithUrl("https://localhost:44381/timeHub",options => {
options.UseDefaultCredentials = true;
}).Build();
await hubConnection.StartAsync();
Finally!
The answer was realy hard to find, but now it is working!
According to this site: https://support.microsoft.com/en-us/help/303650/intranet-site-is-identified-as-an-internet-site-when-you-use-an-fqdn-o
the app identifies the call as a call to the internet and therefore does not allow sending the default credentials. I must add this page manually to the local Intranet sites with the Internet Explorer and than it worked like a charm.
Thank to all who helped me with this :)

Failed to load http://localhost:5000/.well-known/openid-configuration: No 'Access-Control-Allow-Origin' header is present on the requested resource

I am a newbie to identityserver4, recently I have seen the Quickstart8 sample provided by the identityserver team, in that 3 project are included 1.Identityserver
2. Api 3.Client all are working fine in the browser when I deployed to iis they are not working properly it is showing error like...
I am using javascript client ...
Please help me with this issue.
This is my code...
Api (startup.cs)
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Api
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
services.AddCors(options =>
{
// this defines a CORS policy called "default"
options.AddPolicy("default", policy =>
{
policy.WithOrigins("http://localhost:5003")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
}
public void Configure(IApplicationBuilder app)
{
app.UseCors("default");
app.UseAuthentication();
app.UseMvc();
}
}
}
Api (Identity Controller)
[Route("[controller]")]
[Authorize]
public class IdentityController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
QuickstartIdentityServer (startup.cs)
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
string connectionString = #"Data Source=DOTNET-Foo;Initial Catalog=IdentityServer4;Integrated Security=True";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddTestUsers(Config.GetUsers())
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
});
// this adds the operational data from DB (codes, tokens, consents)
//.AddOperationalStore(options =>
//{
// options.ConfigureDbContext = builder =>
// builder.UseSqlServer(connectionString,
// sql => sql.MigrationsAssembly(migrationsAssembly));
// // this enables automatic token cleanup. this is optional.
// options.EnableTokenCleanup = true;
// options.TokenCleanupInterval = 30;
//});
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "434483408261-55tc8n0cs4ff1fe21ea8df2o443v2iuc.apps.googleusercontent.com";
options.ClientSecret = "3gcoTrEDPPJ0ukn_aYYT6PWo";
})
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "implicit";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
// IdentityServerDatabaseInitialization.InitializeDatabase(app);
}
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
I'm not able to access http://localhost:5000/.well-known/openid-configuration
I think the sample doesn't work anymore when you run the projects from IIS because the addresses, or more precisely the ports, are not the same.
Ports used when run in IIS Express
When you run the projects through Visual Studio or use dotnet run, the URL over which the project is hosted is driven by a file called launchSettings.json in the Properties folder of your project.
IdentityServer is hosted over http://localhost:5000 - Link to launchSettings.json
JavaScriptClient is hosted over port http://localhost:5003 - Link to launchSettings.json
Api is accessible at http://localhost:5002 - Link to launchSettings.json
Associated configuration
Knowing this there's a few configuration settings that come into play; let's go together over them.
Client settings in IdentityServer
When you define a client (i.e. an application that will federate its authentication to IdentityServer), you get to specify a few things, like:
to which URL(s) is IdentityServer allowed to redirect the user after logging in or logging out;
if this is a JS client, from which URL(s) should the browser be allowed to initiate an authorisation request
This can be found in the Config class over here.
You'll notice that all the URLs specified in that config point to where the JavaScriptClient is hosted when using IIS Express; you'll need to update those to the URL of the JS client when deployed to IIS.
JS configuration
Since in this example, the JS client makes a request directly to IdentityServer, some settings are defined in the JS application itself; we can find them in the app.js file:
authority is the IdentityServer URL - localhost:5000 is correct when we use IIS Express
redirect_uri and post_logout_redirect_uri use localhost:5003 which is the JS client URL when we use IIS Express
Again, you'll need to update all those values to match the URLs where both the applications are hosted when you use IIS.
API configuration
This sample shows how the JS client can make a request to the API and have it send the token to IdentityServer to validate it.
There are a few settings involved here:
The JS client needs to know the URL of the API - this is defined again in app.js in the JS client
The API needs to know how to reach IdentityServer - we'll find this in Startup.cs of the API
The API needs to allow, through a CORS policy, the browser to make an AJAX request to its endpoints, and that again is done in the Startup class in the API project
Once more, you'll need to update all those URLs to match the ones used when you deploy your projects to IIS.
Hopefully I didn't miss anything ;-)
You don't need to do anything special here, ISD4 handles CORS properly out of the box. You need to specify http://localhost:5003 in the CORS origins for your client config. IDS4 will pick this up and allow the request to the discovery endpoint.
Finally i solved the problem by giving the sql login permission for Login failed for user 'IIS APPPOOL\IdServe
I fixed problem by open IdentityServer port in firewall.
Struggling to much time for fixing this
I was running into this issue also with an Angular 9 app and a .net core web api project deployed to Azure in separate app services/endpoints. I am using Azure DevOps for CI/CD and ultimately what I realized is that in the deployed web.config for the API I had:
<environmentVariable name="ASPNETCORE_HTTPS_PORT" value="44340"/>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development"/>
This was causing an issue because the WebHostBuilder was then using my Development configuration to initialize the Configuration for the Startup class, and that is why localhost was being used at all, since that is what is specified in my appsettings.Development.json file (my understanding - if I have it wrong I'm sure somebody will chime in :)).
I added a web.Staging.config file to my project with
<environmentVariable name="ASPNETCORE_HTTPS_PORT" value="443" xdt:Locator="Match(name)" xdt:Transform="Replace"/>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Staging" xdt:Locator="Match(name)" xdt:Transform="Replace"/>
and my CI build is now transforming the web.config and my deployed ecosystem is healthy.

Categories

Resources