locked
Authentication and authorization is too complicated, too abstract RRS feed

  • Question

  • User1208998405 posted

    Hello

    This is general feedback on the identity and auth parts of ASP.NET Core 2.x.

    The story is that I've been trying for 2 weeks, unsuccessfully so far, to retrofit Google login to an ASP.NET 4.6.1 MVC site I'm migrating to core 2.2. I have run into other issues along the way with ETags and Cosmos DB as I've moved to Core version of these libs, but overall I am struggling to comprehend how it all works. There's a lot of documentation but I cannot build a good overall mental model of what is happening. I am frequently in the code on GitHub.

    When I wrote my original 4.6.1 site I had similar issues so I ended up reading the RFC and writing my own OAuth2 solution from scratch. I began using controller actions, since this is very simple to understand and eventually wrote some middleware, though while I like middleware for filtering, blocking, enriching and preparing, I'm never completely happy with the idea of middleware having a full conversation with the user agent (lots of magic).

    I eventually went on to be hired for consulting work on my experience of identity and auth and eventually have a patent in authorization of APIs for my financial services client.

    The problem is that how OAuth and authentication of HTTP requests works is fairly straight-forward. However, I feel that the AspNetCore solution to make this "super easy" (not for me) is abstracting this and insulating people from the truth: its giving me a fish when I need to be taught how to fish.

    I've been coding on the MS stack since 1998 but I didn't touch website development until 2009 when MVC was released. Though I built WinForms apps and SOAP APIs, I found Microsoft's web ASP.NET stack unlearnable; I couldn't construct a mental model that reconciled with my knowledge of the simple way the web works.

    Simple requests and responses and a simple printing of a dozen or so HTML elements had been turned into an insurmountable abstraction called WebForms. It's as if the architects of WebForms forgot we're all smart programmers who enjoy learning the dirty truth.

    When I did need a web UI, I initially began coding my own loops to write HTML into the response stream and was happy when I discovered this was how Marcus Frind got Plenty of Fish so large on 2 IIS boxes.

    My point is that I think the middleware, signin managers, schemes, user managers etc. are hiding the truth. The problem with abstracting everything is that it then must take full responsibility for every programmer's situation. The abstraction then grows ever more larger and complex and extensible and all things to all men, even though the underlying concepts are really simple.

    Take SignInManager.SignInAsync for example. The name is hiding what it is actually doing. When you think about it, "Sign-in" is a concept in user's minds, its not actually a thing on the server at all.

    I think the focus should be on making some simple Lego pieces to help people help themselves, place the code in controller actions where it can be debugged and rationalised about easily and then concentrate on education. We're talking about some redirects, passing data on URLs, validating callbacks and setting and reading cookies.

    You should say, here's how OAuth works, here's how cookies work, here's how turning a header into a claims principal works, here's a diagram and here's how our pieces can be assembled any way you like.

    That's all, thanks for listening.

    Friday, June 21, 2019 8:36 AM

All replies

  • User753101303 posted

    Hi,

    You tried https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-2.2 already ?

    Auth middleware always worked quite well for me past the initial Learning curve and as you told you would end up anyway in writing your own so that it could be reused and possibly customized.

    I agree for web forms but keep in mind it was designed around 2000 where you had developers being much less familiar with the web (and not carrying that forward to ASP.NET Core is likely partly because it changed a lot since then).

    Friday, June 21, 2019 9:04 AM
  • User-726261922 posted
    I'm glad (not glad) I'm not the only one who feels this way in the new Auth world. I felt like a lost puppy a few months back. I've used Windows Auth in the past in MVC which was easy, and I've rolled my own using the old Membership stuff using both MSSQL and RavenDB (hashing and salting, using Rijndaels, etc.), but I've been trying to figure out how to implement a user account system for my .NET Core 2.1 Angular 7 SPA project for a while now with no success (this was 5 or 6 months back).

    Thankfully I was told the initial demand is an intranet installation now, but I'll need to dive back in at some point and figure it out. I think the Single Page thing is what confused me because I want a completely different html page for logging in. I've read ASP.NET Core 3 makes things easier so I might upgrade in the future.
    Friday, June 21, 2019 3:39 PM
  • User753101303 posted

    I've read ASP.NET Core 3 makes things easier

    According to https://www.talkingdotnet.com/add-authentication-to-angular-7-app-using-asp-net-core-3/ it seems they added a template that handles that rather than a real change about how it is handled behing the scene.

    Friday, June 21, 2019 3:52 PM
  • User1208998405 posted

    Thanks for being helpful.

    I've been reading docs and the actual code for days to try and work out how it works (and why it doesn't for me). Really, it's not help I'm after here, its more that designs that do everything for us stop us growing.

    Interestingly, you say the SPA auth solution for Core 3 is to supply a template. I think folk would be better off having a bit less done for them, though saying that, I'd prefer a template chock full of code that exhibits reality than a one-liner method that makes it all just work so long I'm in the majority case, because when you're not, you're really on your own.

    Another example is CreateDefaultBuilder() which obscures exactly what it does for me while being very likely that it isn't exactly what I need either, so I have to go and get the code from GitHub.

    https://github.com/aspnet/MetaPackages/blob/396f413b389f5983e413be465f917cbfdbda26c9/src/Microsoft.AspNetCore/WebHost.cs#L152

    Friday, June 21, 2019 5:40 PM
  • User-474980206 posted

    I feel your pain, but authentication is not as simple as it was.  How many of the oauths flows did you implement?

        https://medium.com/@darutk/diagrams-and-movies-of-all-the-oauth-2-0-flows-194f3c3ade85

    did you support 2 factor authentication? 

    originally microsoft used simple libraries and template code to support authentication to allow modification. but this lead to the issue that it was hard to update with new authentication support. with core 2.2 they are trying to do libraries with configuration, so its just a NuGet update for new functionality, or simple NuGet and config to add a new feature.

    I've found the its the configuration that's the most trouble, which endpoint, which token version, which secret goes into which config values (ex. scope). this is all about understanding the provider more than the library code (the rest requests are simple, getting the proper data values  may be hard). I found if you know the required flow, simple network traces will get you thru. Often I do the flows with postman, or check the trace flows with postman to determine which parameter is incorrect.

    note: most likely your old code used a different flow than what's current, and you do not have the correct configuration, you should be using openId connect.

        https://developers.google.com/identity/protocols/OpenIDConnect

    Friday, June 21, 2019 5:54 PM
  • User1208998405 posted

    Thanks, number 1.

    I don't think OAuth is especially hard, the hard part is validation and signing parameters, setting cookies in chunks and all those fiddly bits. These helpers could be made available to us and we just assemble them.

    In the last hour I have just made some progress though I'm stuck on something else now.

    I've managed to get to a point where its setting up a new user and login, logging me in and expiring the temporary external cookie and dropping a new `.AspNetCore.Identity.Application` cookie. The problem now is that from `HomeController.User` has no claims, though I can see the cookie is being sent up by Chrome.

    This is the thing, when it doesn't work, I'm unable to help myself. It would be nice if the middleware threw me a bone in the logs; "I saw the cookie but didn't make a principal from it because x".

    Friday, June 21, 2019 7:40 PM
  • User753101303 posted

    I tried the link I posted earlier (see also the Microsoft part which gives the general idea and then section for each provider for specific guidance) and it seems to work just fine.

    Friday, June 21, 2019 8:58 PM
  • User-474980206 posted

    claims / roles are separate from authentication (what do google claims mean for permission in your site?). You supply additional middleware to supply claims for the authenticated users. you can have the claims retrieved on each request, or stored in a separate claims cookie. asp.net core supplies several claim / role providers, and has examples of implementing a custom provider. the custom interface is simple, you are passed a user, and you return an array of claims (from a database, ldap server, etc)

      https://joonasw.net/view/adding-custom-claims-aspnet-core-2

    or a more formal:

      https://damienbod.com/2018/10/30/implementing-user-management-with-asp-net-core-identity-and-custom-claims/

    also the core 2.2 migration guide helps explain

       https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x?view=aspnetcore-2.2

    Friday, June 21, 2019 9:19 PM
  • User1208998405 posted

    Thanks Bruce. Again, its not that I don't understand the concepts, to the contrary, its that these are simple things to comprehend but are made complex by "frameworkification".

    Moreover, when it doesn't work, its very hard for people. That's my point here. My point is: WebForms, WCF, and WPF are huge abstractions over simple ideas, so people don't like them. Middleware == Hiddenware. I understand that Microsoft want to make (esp. enterprise) developers super productive but there's a balance. The current auth design hides too much reality, IMO.

    It took Ruby on Rails to show Microsoft that stripping things back and teaching people how to fish was the better idea.

    Saturday, June 22, 2019 11:48 AM
  • User753101303 posted

    It's the other way round ie you have something that easily works out of the box and then you could study this closer if needed (as most if not all is open source now.). The https://docs.microsoft.com/en-us/azure/active-directory/develop/ doc may have interesting things depending on what you are trying to do. AFAIK it does exactly what needs to be done while WebForms was to expose web programming in a way similar to desktop programming so the gap here was huge between the abstraction and the underlying reality.

    From a practical point of view, I'm not sure if you try to sign in with Google or if you are past this point and try to customize something. So far, using "individual user accounts", following the documentation direction to register the app with Google, and adding the AddGoogle call to configure the provider with app secrets worked fine for me (Google shown an url error on my first attempt and it worked once fixed).

    Saturday, June 22, 2019 12:24 PM
  • User1208998405 posted

    Hey Patrice,

    I've hit all kinds of problems along the way. 

    For example, I here's my latest problem.

    https://stackoverflow.com/questions/56743645/no-sign-out-authentication-handler-is-registered-for-the-scheme-identity-twofac

    I could actually use some help on this one :-) the `SignOutAsync()` takes no params and has no overloads. Why does it think I'm signing-out of the 2FA scheme?

    It really hasn't just worked for me and it can only be because I'm using code from an old MVC app/controllers. But this means the APIs have been broken and have different behaviour. Okay its arguable that going to .NET Core might break behaviour of an SDK/API. But shouldn't calling SignInManager.SignOutAsync() just work? There are no arguments for this method, so there's no room for programmer error, thus its not unreasonable to expect it to work. The error makes no sense. The API design is fishy.

    Thanks, Luke

    Monday, June 24, 2019 9:11 PM
  • User1208998405 posted

    Actually I've solved this already. See the SO answer if curious. As suspected, its now an API I do not trust.

    Monday, June 24, 2019 9:26 PM
  • User475983607 posted

    lukepuplett

    Actually I've solved this already. See the SO answer if curious. As suspected, its now an API I do not trust.

    The SO response recommended using DI which is the standard method for passing services to controller constructors. 

    Can you explain why using DI causes you to not trust an API?  Or is it the configuration?

    I'm using the out-of-the-box scaffolded Identity which work perfectly.

    namespace RazorIdentityScaffold.Areas.Identity.Pages.Account
    {
        [AllowAnonymous]
        public class LogoutModel : PageModel
        {
            private readonly SignInManager<ApplicationUser> _signInManager;
            private readonly ILogger<LogoutModel> _logger;
    
            public LogoutModel(SignInManager<ApplicationUser> signInManager, ILogger<LogoutModel> logger)
            {
                _signInManager = signInManager;
                _logger = logger;
            }
    
            public void OnGet()
            {
            }
    
            public async Task<IActionResult> OnPost(string returnUrl = null)
            {
                await _signInManager.SignOutAsync();
                _logger.LogInformation("User logged out.");
                if (returnUrl != null)
                {
                    return LocalRedirect(returnUrl);
                }
                else
                {
                    return Page();
                }
            }
        }
    }

    I assume you have some kind of configuration issue.  Anyway when you use HttpContext you are expiring the auth cookie.

    Monday, June 24, 2019 9:31 PM
  • User1208998405 posted

    I don't trust it because I cannot reason about how its behaving. It's completely bizarre. Why would calling SignOutAsync on the injected SignInManager give me an error regarding 2FA when I've not configured 2FA?!

    But when I call the SignOutAsync method on the HttpContext supplying IdentityConstants.ApplicationScheme it works.


    Anyway, I'm no longer concerned with that as I've realised recently that my Google login is only half working.

    In my AccountController.ExternalLoginCallback method I call this.SignInManager.GetExternalLoginInfoAsync() but that returns null. However, if I call this.HttpContext.AuthenticateAsync() it returns me an AuthenticateResult with my principal and Google claims, the properties and everything!

    So why is GetExternalLoginInfoAsync unable to return the external login info? I mean, I could build an instance myself as have all the pieces I need in the AuthenticateResult.

    It's all totally messed up.

    Monday, June 24, 2019 10:48 PM
  • User-474980206 posted
    GetExternalLoginInfoAsync Is a method implemented by the sign manager that returns the the login info. This Is configured by the settings. You probably have not configured the external settings

    services.AddIdentity(options=>{
    options.Cookies.ExternalCookie.AuthenticationScheme = "SomeName";
    });
    Tuesday, June 25, 2019 4:11 AM
  • User475983607 posted

    Share the code along with the steps to reproduces the issue.  I'm sure a forum member will give you a hand.

    Tuesday, June 25, 2019 10:27 AM
  • User-474980206 posted
    I suggest you create a new project with identity scaffolding. Just follow the instructions. This should pretty much work out the box. Make the changes you want, then port this code to your original project.

    Tuesday, June 25, 2019 12:49 PM
  • User1208998405 posted

    Yeah I was arriving at the same conclusion, Bruce :-(

    Someone on SO explained why I was getting such an odd error when I was calling SignOut, but exemplifies how arcane this whole API design is.

    https://stackoverflow.com/questions/56743645/no-sign-out-authentication-handler-is-registered-for-the-scheme-identity-twofac/56744102#56744102

    My configuration FWIW is this.

    serviceCollection
        .AddIdentityCore<MyUser>(identityOptions =>
        {
            identityOptions.SignIn.RequireConfirmedEmail = false;
        })
        .AddUserStore<MyUserStore>()
        .AddSignInManager<SignInManager<MyUser>>();
    
    serviceCollection.AddAuthentication(IdentityConstants.ApplicationScheme)
        .AddCookie(IdentityConstants.ApplicationScheme, options =>
        {
            options.SlidingExpiration = true;
        })
        .AddGoogle(googleOptions =>
        {
            this.Configuration.Bind("OAuth2:Providers:Google", googleOptions);
    
            googleOptions.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub", "string");
        })
        .AddExternalCookie();
    Tuesday, June 25, 2019 7:18 PM