MVC authorization inside MapWhen() is applied to all controllers
Asked Answered
P

5

13

In ASP.Net Core 3.0 Preview 7, I tried to write some code as follows:

public void Configure(IApplicationBuilder app) {
    app.MapWhen(context =>
        context.Request.Path.StartsWithSegments(
              new PathString("/UnsecureLog")),
        a => {
            a.UseRouting();
            a.UseEndpoints(endpoints => {
                endpoints.MapControllers();
            });
        }
    );
    
    app.UseAuthentication();
    app.UseAuthorization();

    app.MapWhen(context =>
        context.Request.Path.StartsWithSegments(
           new PathString("/SecureLog")),
        a => {
            a.UseRouting();
            a.UseEndpoints(endpoints => {
                endpoints.MapControllers()
                    .RequireAuthorization("MustBeReader");
            });
        }
    );
}

My goal was to allow certain controllers to be handled in the middleware without authentication, and my thinking was that MapWhen() was the way to pull that off.

Instead I see this error when hitting the /UnsecureLog endpoint:

System.InvalidOperationException: Endpoint ... contains authorization metadata,
but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization()
inside the call to Configure(..) in the application startup code.

Translation: "How's about you go and implement security features for that endpoint you didn't want to secure".

My takeaway is that any call to RequireAuthorization("MustBeReader") in any MapWhen() block handling controller logic will in fact be be applied to all MVC controller routes.

My current workaround is to remove the .RequireAuthorization("MustBeReader") call in the 2nd MapWhen() code block and re-apply it as an attribute ([RequireAuthorization("MustBeReader")]) to those endpoints I wish to secure. This works with no errors and produces the desired behavior.

But that sorta wasn't the goal, was it?

I'd prefer to manage whole groups of controllers with similar policies, while sparing others from security at all, and handle all of this from within Configure(). Instead I must apply the desired authorization requirements to each and every controller, piecemeal.

I am hoping there is a better way to implement routing that avoids the issue noted here. Maybe I'm doing something improperly.

Thoughts, anyone?

Provincialism answered 7/8, 2019 at 14:46 Comment(1)
I've also since discovered the opposite is also true... If you call MapWhen() twice and do this in ONE of them .WithMetadata(new AllowAnonymousAttribute()) -- guess what? No security applies to either. Even if the other one had policies galore applied across the board.Provincialism
P
3

I'm giving myself credit for the full answer, having spent some time today on this and posting the code. Thanks to mwilson for giving me the nudge to try again.

Prior to my new approach, every time the code ran, this warning was shown...

Startup.cs(189,13): warning ASP0001: The call to UseAuthorization
should appear between app.UseRouting() and app.UseEndpoints(..)
for authorization to be correctly evaluated.

So the solution is not merely recognizing that warning and heeding it, but also to find a way to appease the compiler gods. (And you'll see below that I still haven't figured out how to do that, yet.)

Here's what I did figure out today. I placed the UseAuthentication and UseAuthorization calls in two different places. This works. Somewhat.

In this API project I now am able to run anonymous unsecured endpoints, secured endpoints, and, for bonus points, secured GraphQL endpoints as well.

FWIW, code below:

        public void Configure(...)
        {
            ...
            ...
            app.UseStaticFiles();
            app.UseRouting();

            app.MapWhen(
                context => (context.Request.Path
                    .StartsWithSegments(new PathString("/api"))
                ),
                a =>
                {
                    a.UseRouting();
                    a.UseAuthentication();
                    a.UseAuthorization();
                    a.UseEndpoints(endpoints =>
                    {
                        endpoints
                            .MapControllers()
                            .RequireAuthorization(...);
                    });
                }
            );

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseWebSockets();
            app.UseGraphQLWebSockets<...>(...);
            app.UseGraphQL<...>(...);
        }

That works, but I still get the compiler warning. What's more, if I get too smart by half and try to use the following controller...


    [Route("vapi/[controller]")]
    //[AllowAnonymous]
    public class VersionController : Controller
    { ...

along with this additional Startup.cs code...

            app.MapWhen(
                context => (context.Request.Path
                    .StartsWithSegments(new PathString("/vapi"))
                ),
                a =>
                {
                    a.UseRouting();
                    // a.UseAuthentication(); <-- look, Ma, no authentication!
                    // a.UseAuthorization(); <-- look, Ma, no authorization!
                    a.UseEndpoints(endpoints =>
                    {
                        endpoints
                            .MapControllers()
                            .RequireAuthorization(...);
                    });
                }
            );

I also still get the error noted in my OP. (It's all coming back to me, like an old nightmare...)

Endpoint ....Controllers.VersionController.Version (...) contains authorization
metadata, but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization() ...

So where I am leaving the matter is this: I still cannot do what I wanted to do, which was to secure only certain controller paths. What I can do (without adding that "vapi"-path-supporting code to Startup) is this:

    [Route("api/[controller]")]
    [AllowAnonymous]
    public class VersionController : Controller
    { ...

So... I'll call this the answer, until someone posts a usable set of code that improves on it.

Provincialism answered 11/2, 2020 at 18:49 Comment(0)
C
18

Move the below code higher up in your Startup class (above app.MapWhen).

 app.UseAuthentication();
 app.UseAuthorization();
Compressibility answered 8/10, 2019 at 6:25 Comment(5)
If I did that, all routes would be protected. Achieving the opposite of the stated goal.Provincialism
Thank you. This was my problem learn.microsoft.com/en-us/aspnet/core/migration/…Armourer
I find it mildly amusing that an answer which doesn't (to my knowledge) address my question should continue to pile up points. Or have I missed something, and the joke's on me? By my read, the posted solution makes the error message go away... and in the same stroke also removes the desired functionality I was seeking to achieve. Color me confused.Provincialism
@Provincialism I imagine that it solved a number of users' problems, even though not your own.Frontpage
Thanks! That fixed my issue.Tafia
B
6

It's important to note that app.UseAuthorization must appear between app.UseRouting() and app.UseEndpoints(...);

So, it should look something like this:

        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller}/{action=Index}/{id?}");
        });
Brachy answered 11/2, 2020 at 2:20 Comment(1)
Can UseAuthorization and UseAuthorization be placed inside individual MapWhen() calls? Am wondering if I tried that already, but will give it a whirl when I get a chance. If it works, yours is the solution I was looking for.Provincialism
P
3

I'm giving myself credit for the full answer, having spent some time today on this and posting the code. Thanks to mwilson for giving me the nudge to try again.

Prior to my new approach, every time the code ran, this warning was shown...

Startup.cs(189,13): warning ASP0001: The call to UseAuthorization
should appear between app.UseRouting() and app.UseEndpoints(..)
for authorization to be correctly evaluated.

So the solution is not merely recognizing that warning and heeding it, but also to find a way to appease the compiler gods. (And you'll see below that I still haven't figured out how to do that, yet.)

Here's what I did figure out today. I placed the UseAuthentication and UseAuthorization calls in two different places. This works. Somewhat.

In this API project I now am able to run anonymous unsecured endpoints, secured endpoints, and, for bonus points, secured GraphQL endpoints as well.

FWIW, code below:

        public void Configure(...)
        {
            ...
            ...
            app.UseStaticFiles();
            app.UseRouting();

            app.MapWhen(
                context => (context.Request.Path
                    .StartsWithSegments(new PathString("/api"))
                ),
                a =>
                {
                    a.UseRouting();
                    a.UseAuthentication();
                    a.UseAuthorization();
                    a.UseEndpoints(endpoints =>
                    {
                        endpoints
                            .MapControllers()
                            .RequireAuthorization(...);
                    });
                }
            );

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseWebSockets();
            app.UseGraphQLWebSockets<...>(...);
            app.UseGraphQL<...>(...);
        }

That works, but I still get the compiler warning. What's more, if I get too smart by half and try to use the following controller...


    [Route("vapi/[controller]")]
    //[AllowAnonymous]
    public class VersionController : Controller
    { ...

along with this additional Startup.cs code...

            app.MapWhen(
                context => (context.Request.Path
                    .StartsWithSegments(new PathString("/vapi"))
                ),
                a =>
                {
                    a.UseRouting();
                    // a.UseAuthentication(); <-- look, Ma, no authentication!
                    // a.UseAuthorization(); <-- look, Ma, no authorization!
                    a.UseEndpoints(endpoints =>
                    {
                        endpoints
                            .MapControllers()
                            .RequireAuthorization(...);
                    });
                }
            );

I also still get the error noted in my OP. (It's all coming back to me, like an old nightmare...)

Endpoint ....Controllers.VersionController.Version (...) contains authorization
metadata, but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization() ...

So where I am leaving the matter is this: I still cannot do what I wanted to do, which was to secure only certain controller paths. What I can do (without adding that "vapi"-path-supporting code to Startup) is this:

    [Route("api/[controller]")]
    [AllowAnonymous]
    public class VersionController : Controller
    { ...

So... I'll call this the answer, until someone posts a usable set of code that improves on it.

Provincialism answered 11/2, 2020 at 18:49 Comment(0)
P
0

I use the Authorize-attribute on classes/controllers to enforce the system to require an authenticated user. If you specify a parameter on this attribute it required the user to have this claim. For any controller or method that should be accessible without authentication we set the AllowAnonymous-attribute on either the controller or specific method. This should overrule any authorize requirements.

Plenitude answered 26/8, 2019 at 13:2 Comment(2)
I have confirmed that [AllowAnonymous] does nothing to prevent this error. But even if it did, my goal was to be able to, in the middleware pipeline, handle whole groups of controllers differently, rather than adding attributes to controllers piecemeal. Imagine, for example, a group of controllers in a route prefixed with "/secured" and another group prefixed with "/unsecured".Provincialism
Giving up. Either I'm hopelessly confused or I'm asking too much of the current pipleline feature set. I've found interesting alleys to walk down, including this and this however I think I'll whistle in the dark past this distraction and get on with life without what I was hoping for here.Provincialism
P
0

I was beating my head against the wall with this for along time, giving up and then trying again after some months.

But discussion here somehow pushed me to try another approach which seemed to work:

  1. start with app.UseRouting();
  2. describe all app.UseWhen(...) that do not need auth
  3. only then put app.UseAuthentication(); app.UseAuthorization();
  4. and only then add your app.UseWhen(...) that does require auth

Looking at it now - feels obvious, but somehow took a very long time to get right.

Prowl answered 24/11, 2022 at 9:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.