Globally Require Authenticated Users By Default Using Fallback Policies in ASP.NET Core

tldr;

You can use Fallback Policies in ASP.NET Core 3.0+ to require an Authenticated User by default. Conceptually, you can think of this as adding an [Authorize] attribute by default to every single Controller and Razor Page ONLY WHEN no other attribute is specified on a Controller or Razor Page (like [AllowAnonymous] or [Authorize(PolicyName="PolicyName")]).  See lines 9-11 below.


public class Startup
{
// Other Startup code omitted
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
// Register other policies here
});
// Other service registrations omitted
}
}

view raw

Startup.cs

hosted with ❤ by GitHub


View this gist on GitHub

 

A Quick Lap Around the [Authorize] and [AllowAnonymous] Attributes

In ASP.NET Core (and even previously in ASP.NET), we’ve had the ability to add a [Authorize] attribute to a resource (such as a Controller or Razor Page) in order to tell ASP.NET Core not to let a user access that resource unless they are authenticated.


[Authorize]
public class IndexModel : PageModel
{
public void OnGet()
{
// do something
}
}

view raw

Index.cshtml.cs

hosted with ❤ by GitHub

The [Authorize] attribute can also take a PolicyName parameter that tells it what Authorization Policy to execute.  The Policy below says only Admins can access this page.


[Authorize(PolicyName="Admin")]
public class IndexModel : PageModel
{
public void OnGet()
{
// do something
}
}

view raw

Index.cshtml.cs

hosted with ❤ by GitHub

You can follow this link to learn more how to set up policies in ASP.NET Core and how to enforce your own custom rules (such as what defines an Admin).

By default, if you do not add an [Authorize] attribute, then the resource will not be secured and will be accessible to unauthenticated users.  A resource can also be accessible to unauthenticated users by explicitly adding a [AllowAnonymous] attribute.

Word of Caution: Adding the [AllowAnonymous]attribute bypasses all Authorization, and short-circuits out of the Authorization pipeline, even if Authorization is set further up the stack.

 

The Problem – Having to remember to add [Authorize] attributes everywhere

When you create a new Controller or Razor Page in ASP.NET Core, by default the resource will be accessible to anyone, because there is no [Authorize] attribute.  This is a problem if you’re creating a site where a majority of the site is protected by some sort of authentication.  It is really easy to forget to add an [Authorize] attribute which could open up your application to a security vulnerability, and leave you with a potential… let’s call it a “resume updating event.” 🙂

 

What are Fallback Policies?

ASP.NET Core 3.0 turned on Endpoint Routing by default, which was a way to get Routing information out of being tightly coupled to MVC and make Routing more global to the entire stack (such as Middleware).  The 3.0 release introduced the concept of Fallback Policies with Endpoint Routing.

A Fallback Policy means that if no other policy or attribute is specified on a Controller or Razor Page, the Authorization middleware will use the Fallback Policy.  Therefore, if you do not add any other attribute (such as [AllowAnonymous] or [Authorize(PolicyName="PolicyName")], then ASP.NET Core will use the Fallback Policy.

 

The Solution – Using a Fallback Policy to require authentication by default

So by leveraging a Fallback Policy, we can specify that a user must always be authenticated for every Controller or Razor Page in our application.  You can wire this up under ConfigureServices via the AuthorizationOptions in services.AddAuthorization.   See lines 9-11 below:


public class Startup
{
// Other Startup code omitted
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
// Register other policies here
});
// Other service registrations omitted
}
}

view raw

Startup.cs

hosted with ❤ by GitHub

Conceptually, you can think of this as adding an [Authorize] attribute by default to every single Controller and Razor Page ONLY WHEN no other attribute is specified on a Controller or Razor Page (like [AllowAnonymous] or [Authorize(PolicyName="PolicyName")]).

Of course, you could also take this one step further, by having your Fallback Policy be a policy that requires certain claims instead of just being authenticated.  The choice is up to you!

 

What do we gain by doing this?

  1. A more secure default.  A developer doesn’t have to remember to add an [Authorize] attribute to every Controller or Razor Page.
  2. Less boilerplate.  Every Controller and Razor Page requiring authentication has one less line of boilerplate code to worry about.
  3. You don’t give up any flexibility.
    1. If a Controller or Razor Page is supposed to be public to unauthenticated users (such as a Login page or Forgot Password page), then you can still add a [AllowAnonymous] attribute and the Fallback Policy is bypassed.
    2. If a Controller or Razor Page needs a specific policy, you can still add an Authorize attribute with a custom policy name.  That will take precedence over the Fallback Policy such as [Authorize(PolicyName="PolicyName")].

 

Default Policy vs. Fallback Policy

You might get confused when seeing that there’s also a “Default Policy” in addition to Fallback Policies (or at least I did).  In my head I thought “oh the Fallback Policy is kind of like the default policy that runs… but wait… what’s a Default Policy then?”

The Default Policy is the policy that gets evaluated when authorization is required, but no explicit policy is specified.  In other words, it’s the policy that evaluates when you add an [Authorize] attribute without any PolicyName.  Out of the box, the Default Policy is set to requiring Authenticated Users.

A Fallback Policy, on the other hand, is the policy that gets evaluated if no other policy is specified (such as when no [AllowAnonymous] or [Authorize] attribute exists on a Controller or Razor Page)

 

The Old Solution in ASP.NET Core 2.x

A common solution to this problem in ASP.NET Core 2.x was to add a Global Filter to MVC such as lines 10-13 below:


// OLD WAY FROM ASP.NET CORE PRIOR TO 3.0!!!!!!!!!
public class Startup
{
// Other Startup code omitted
public void ConfigureServices(IServiceCollection services)
{
// OLD WAY FROM ASP.NET CORE PRIOR TO 3.0!!!!!!!!!
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
// Other service registrations omitted
}
}

view raw

Startup.cs

hosted with ❤ by GitHub

 

Conclusion

In my opinion, the new way using a Fallback Policy makes a lot more sense. It keeps everything inside the Authorization configuration and doesn’t sprinkle Authorization logic into MVC Filters.  The only thing that’s a little goofy is the naming between a Default Policy and Fallback Policy, but once you learn that nuance, the naming makes sense.

 

In a future post, I’ll go over other tips and tricks for leveraging the ASP.NET Core authorization system.  Stay tuned.

8 thoughts on “Globally Require Authenticated Users By Default Using Fallback Policies in ASP.NET Core

  1. so once I implement this, my application won’t load past the root of the website (localhost:44359/). Where do I tell it to allow anonymous so that they can go through the authentication process (log in)?

    • Hey Andrew – you’ll still need to add an [AllowAnonymous] to your Login Controller Action (if MVC) or Login PageModel (if Razor Pages) in order to hit your login page. Hope that helps!

  2. Does this work for a Blazor solution? I added an @attribute [AllowAnonymous] in the login page but the app enters a loop of redirection. In the App.razor I have a redirector to the login page:

    • The code was cut. But basically I have an AuthorizeRouteView with the RouteData=”@routeData” DefaultLayout=”@typeof(MainLayout)” properties and a NotAuthorizedHandler component to redirect to the Login page.

Leave a Reply