Require Authentication for all requests to an OWIN application - c#

I am working with a self-hosted OWIN application and am trying to figure out how to require authentication/authorization for all requests (or arbitrary requests).
Some of the individual components in the pipeline have their own Authorization facilities (ex. WebAPI, SignalR, Nancy) but that seems somewhat redundant when I want to restrict everything. Additionally, some middle-ware does not have authorization support (ex. Microsoft.Owin.StaticFiles).
If my OWIN Startup looks something like this:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.RequireSsl();
app.UseCookieAuthentication(new CookieAuthenticationOptions());
//...
app.UseGoogleAuthentication();
// ** Need to add something that restricts access **
app.UseDirectoryBrowser();
}
}
How do I require the user have authenticated (redirecting if necessary) before serving the directory browser? (The directory browser could arbitrarily be other OWIN components.)

Put this between your auth middleware and the components you want to protect. It will check to ensure that each request is authenticated.
app.Use(async (context, next) =>
{
var user = context.Authentication.User;
if (user == null || user.Identity == null || !user.Identity.IsAuthenticated)
{
context.Authentication.Challenge();
return;
}
await next();
});

Related

How to serve SPA on root using Azure AD's AuthorizeforScopes in a BFF

I am pulling my hair out on this issue and I wonder if anyone here could help me out. If not, does anyone know who can?
My situation is that I have a BFF (Backend-for-frontend) that serves my Angular SPA. I use Azure AD Auth.
I use .NET Core 5 and the up to date Azure AD nuget libs
The BFF is useful because it prevents my Auth tokens from being saved in the browser; the BFF saves it in a http-only secure cookie so the SPA has no knowledge of the user; it has to ask the API about the user.
The BFF also ensures that you can only access the SPA if you are logged in
The BFF also reverse proxies calls to /api to the actual API; which is hosted inside our k8s cluster so you can't reach it from the internet
The diagram basically looks like this. See image.
My problem is that I need to use AuthorizeForScopes for Azure AD Auth but this requires stupid workarounds. As far as I can find online I must put this on a Controller/Action like so:
[HttpGet]
[Authorize]
[AuthorizeForScopes(ScopeKeySection = "Scopes")]
public IActionResult Index()
{
return PhysicalFile($"{_webHostEnvironment.ContentRootPath}/ClientApp/dist/index.html", "text/html");
}
This means that if you go to /, you will hit this endpoint and get served the index.html. This isn't a super neat setup because I would rather just let the UseSpa() in Startup.cs handle this. But I believe this is necessary because I can't just use AuthorizeForScopes in my middleware.
Another problem is that this doesn't work during development because there the UseProxyToSpaDevelopmentServer() in Startup.cs handles all this. If you run this during development you get an error cuz it can't find the index.html
The current solution I have on prod is that the code I posted above instead redirects to /home which the SPA handles, so that way if you go to / you get redirected to /home and the BFF pipeline then redirects you to the SPA and boom, it all works. But this means I can't run my SPA on /
I currently have the following Startup.cs setup for my pipeline. I removed unnecessary code.
app.UseStaticFiles();
app.UseStatusCodePagesWithReExecute("/errorstatus/{0}");
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// Proxy calls to the API through the BFF so the API can only be reached from within the cluster. This is more secure
app.Map("/api", true, config => RunApiProxy(/* Stuff here */));
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapHealthChecks("/health");
});
if (!Environment.IsDevelopment())
{
// During development we serve files from the dev-server, not from the location of the spa static files
app.UseSpaStaticFiles();
}
// Redirect the user to authenticate if the user isnt at this moment
// All static frontend related files are served using the UseSpaStaticFiles middleware
// What's left is the index.html
app.Use(async (context, next) =>
{
if (context.User?.Identity?.IsAuthenticated != true)
{
await context.ChallengeAsync(WellKnownAuthenticationSchemes.OpenIdConnect);
}
else
{
await next();
}
});
if (Environment.IsDevelopment())
{
// Forward devserver socket calls during development
app.MapWhen(p => p.Request.Path.StartsWithSegments("/sockjs-node"), config =>
{
config.UseSpa(spa =>
{
spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
});
});
}
// Serve the angular app
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (Environment.IsDevelopment())
{
spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
}
});
How do I implement AuthorizeForScopes without that Action method posted above so I can serve my SPA on / only if the user is [Authorized] and [AuthorizeForScopes]?
How do I implement AuthorizeForScopes without that Action method posted above so I can serve my SPA on / only if the user is [Authorized] and [AuthorizeForScopes]
There are two ways of doing this in the startup.cs > ConfigureServices(IServiceCollection services) method.
Option 1:
services.AddControllers(o =>
{
o.Filters.Add(new RequiredScopeAttribute(new[] { "access_as_user" }));
});
This will automatically add the Scope attribute to all controllers, but this will not work if you have endpoints with [AllowAnonymous].
To get around this the other way is to add your own authorization policy in the same ConfigureServices method for scope:
services.AddAuthorization(o =>
{
var authPolicy = new AuthorizationPolicyBuilder(new[] { JwtBearerDefaults.AuthenticationScheme })
.RequireAuthenticatedUser()
.RequireAssertion(ctx =>
{
// Attempt with Scp claim
Claim scopeClaim = ctx.User.FindFirst(ClaimConstants.Scp);
// Fallback to Scope claim name
if (scopeClaim == null)
{
scopeClaim = ctx.User.FindFirst(ClaimConstants.Scope);
}
return scopeClaim != null && scopeClaim.Value.Split(' ').Intersect(new[] { "access_as_user" }).Any();
})
.Build();
o.DefaultPolicy = authPolicy;
});
The second option sets this auth policy as default for all routes via o.DefaultPolcy = authPolcy but unlike the first option this can be overridden by using [AllowAnonymous] on your method/controller.
You can also use o.AddPolicy("authMeUpScotty", authPolicy) instead of o.DefaultPolicy and use [Authorize(Policy = "authMeUpScotty")] if it was only for particular endpoints.
Hope this helps!

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

Authentication/Authorization with ASP .NET Core and React based on Identity

I've been trying to create a SPA web application using React with ASP .NET Core 3.1 as backend, and now I need to restrict users going to certain pages. I know that for API methods I can do the following:
[Authorize(Roles="admin")]
[HttpGet]
public async Task<Whatever> Get(){ ... }
But that would only block users from using the API methods, which is good, but I also want to not let them go into the pages themselves.
Since I do not want to use their Blazor pages (because it breaks the separation of the client app and the backend), I cannot scaffold their login page, so I created mine using React and then implemented login and logout methods, which work, since when I'm logged in, the AspNetCore.Identity.Application cookie appears.
In order to do this, I added the following lines to Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddIdentityCore<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
services.AddAuthentication (o =>
{
o.DefaultScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
}).AddIdentityCookies(o => {});
...
}
public void Configure(IApplicationBuilder app)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
}
Now I've read (source: https://stackoverflow.com/a/40055744/14806778) that, in React, you can define an onEnter method and check authentication there. So, to check if user is logged in or not, I implemented the simple method:
public bool IsUserLoggedIn()
{
return User.Identity.IsAuthenticated;
}
This also works, so I guess I could call this method in the onEnter method of React routing. I don't know if that's efficient or not, though.
Last, to check if user is in role, I could do something like this:
public async Task<bool> IsUserInRole(string requiredRole)
{
if(User.Identity.IsAuthenticated)
{
var user = await _userManager.GetUserAsync(User);
return await _userManager.IsInRoleAsync(user, requiredRole);
}
return false;
}
And call this onEnter instead.
My question is, is this approach valid? Is it safe? Does it have a huge performance impact? What are the alternatives? I've looked around a bit but I haven't seen a lot. I've read about JWT but I don't know how is it so different to this.
Also, I don't need Google/Apple/Facebook login, this is for an app which will be most likely running on localhost inside a VPN. I am using .NET Core 3.1, React 16.14 and React router dom 5.2.0.
Thank you.

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 do I disable/enable authentication at runtime in Asp.Net Core 2.2?

A website is per default anonymous access only.
The admin has a button to switch the site into maintenance mode, which should enable authorization using the built-in CookieAuthentication (flip a bit in a database, not relevant for this post).
In order to make that work, I first configured cookie authentication (in startup.cs):
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/auth/login");
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
}
Then on relevant controllers, I put an [Authorize] attribute.
[Authorize]
public class HomeController : Controller
{
//removed
}
This works perfectly - cookie auth kicks in when authorize-attribute is present. So far so good.
Now I want to disable authorization at runtime when maintenance mode is off.
Attempted Solution
This is what I ended up with after a lot of trial and error and research.
public void OnAuthorization(AuthorizationFilterContext context)
{
IMaintenanceModeDataService ds = context.HttpContext.RequestServices.GetService<IMaintenanceModeDataService>();
if (!ds.IsMaintenanceModeEnabled)
{
//Maintenance mode is off, no need for authorization
return;
}
else
{
ClaimsPrincipal user = context.HttpContext.User;
if (user.Identity.IsAuthenticated)
{
//when the user is authenticated, we don't need to do anything else.
return;
}
else
{
//we're in maintenance mode AND the user is not
//It is outside the scope of this to redirect to a login
//We just want to display maintenancemode.html
context.Result = new RedirectResult("/maintenancemode.html");
return;
}
}
}
[MaintenanceModeAwareAuthorize]
public class HomeController : Controller
{
//removed
}
This works great when the site is in maintenance mode.
When the site is NOT in maintenance mode, the cookie authentication still kicks in and requires auth. I could remove that and try to implement my own auth, but that would be stupid, when we already have perfectly well-crafted solutions built-in.
How do I disable authorization when the site is NOT in maintenance mode (at runtime)?
Notes:
Q: Why not handle this by doing x (which requires serverside access to config, environment vars, server or similar)?
A: Because this needs to be immediately accessible to non-technical admin-users by clicking a button in the backend.
Yes you can!
The authorization system in ASP.NET Core is extensible and you can implement your scenario easily with poliy-based authorization.
Two main things to know to get going:
an authorization policy is made of one or more requirements
all of the requirements must be satisfied for a policy to succeed
Our goal is then to create a requirement which is satisfied if any of the following statements is true:
the maintenance mode is not enabled, or
the user is authenticated
Let's see the code!
The first step is to create our requirement:
public class MaintenanceModeDisabledOrAuthenticatedUserRequirement : IAuthorizationRequirement
{
}
We then have to implement the handler for this requirement, which will determine if it's satisfied or not. The good news is handlers support dependency injection:
public class MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler : AuthorizationHandler<MaintenanceModeDisabledOrAuthenticatedUserRequirement>
{
private readonly IMaintenanceModeDataService _maintenanceModeService;
public MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler(IMaintenanceModeDataService maintenanceModeService)
{
_maintenanceModeService = maintenanceModeService;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MaintenanceModeDisabledOrAuthenticatedUserRequirement requirement)
{
if (!_maintenanceModeService.IsMaintenanceModeEnabled || context.User.Identities.Any(x => x.IsAuthenticated))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Next, we need to create an authorization policy that uses this requirement, and you have 2 choices here:
you can redefine the default policy, used when "empty" [Authorize] attributes are used, or
create an explicit policy that you'll have to reference in your attributes, like [Authorize(Policy = "<your-policy-name>")]
There's no right or wrong answer; I'd pick the first option is my application had only one authorization policy, and the second one if it had several of them. We'll see how to do both:
services
.AddAuthorization(options =>
{
// 1. This is how you redefine the default policy
// By default, it requires the user to be authenticated
//
// See https://github.com/dotnet/aspnetcore/blob/30eec7d2ae99ad86cfd9fca8759bac0214de7b12/src/Security/Authorization/Core/src/AuthorizationOptions.cs#L22-L28
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddRequirements(new MaintenanceModeDisabledOrAuthenticatedUserRequirement())
.Build();
// 2. Define a specific, named policy that you can reference from your [Authorize] attributes
options.AddPolicy("MaintenanceModeDisabledOrAuthenticatedUser", builder => builder
.AddRequirements(new MaintenanceModeDisabledOrAuthenticatedUserRequirement()));
});
Next, you need to register the requirement handler as an IAuthorizationHandler, as indicated in the official docs
// The lifetime you pick is up to you
// You just need to remember that it's got a dependency on IMaintenanceModeDataService, so if you
// registered the implementation of IMaintenanceModeDataService as a scoped service, you shouldn't
// register the handler as a singleton
// See this captive dependency article from Mark Seeman: https://blog.ploeh.dk/2014/06/02/captive-dependency/
services.AddScoped<IAuthorizationHandler, MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler>();
The final step is to apply the [Authorize] attributes on your controllers/actions as needed.
// 1. If you redefined the default policy
[Authorize]
public IActionResult Index()
{
return View();
}
// 2. If you defined an explicit policy
[Authorize(Policy = "MaintenanceModeDisabledOrAuthenticatedUser")]
public IActionResult Index()
{
return View();
}
I am afraid that could not be done .The accept of authorization is different from authentication, when context.HttpContext.User.Identity.IsAuthenticated is false, it will always redirect to login page.
It's better to have actions that must or may require authorization in a controller together, and unauthorized actions in a separate controller with [AllowAnonymous].
if (!user.IsMaintenanceModeEnabled)
{
context.Result = new RedirectResult("Another controller with [AllowAnonymous]");
return;
}
Since current pages need work perfectly with anonymous mode, then authentication should NOT be in Controller level.
I think your requests are:
If a Maintancer login system,
run extra code to show maintance elements(switch button or others) on page, so Maintancer can switch page with different mode, and do maintancer actions
If user visit site anonymously, anonymous-mode elements will render to browser
If user login but not an Maintancer, normal-user-mode elements will render to browser
To resolve those, The key is to block unauthorized user to visit Maintancer ACTIONS, instead of controller.
my suggestions are:
in _Layout.cshtml page, check if Maintancer Login, then enject switch button
in the actions or pages that could visit anornymously, check if "Maintancer Login" && IsMaintenanceMode, then show Maintancer-authorized elements, like Delete Post, Edit Content, ...
in Controller.Actions that works only for Maintancer(like Delete Post), add [Authorize(Roles="Maintancer")] or [Authorize(Policy="Maintancer")] or you customized authorize.

Categories

Resources