Aspnetcore: Identity philosophy

Created on 17 Jun 2017  路  36Comments  路  Source: dotnet/aspnetcore

I am talking about current stable version of ASP.NET Core Mvc and Identity (1.1.3 and 1.1.2 respectively)

Is there any place where Asp.Net Core Identity design philosophy would be explained in depth? Whenever I try to implement some non-standard behavior, I find the complete absence of in-depth documentation.

For example, there is an "admin" section on my web site implemented by controllers and actions marked with Authorize attribute, I don't want unauthenticated users to be redirected to the sign in page, instead I just want a 403 error page displayed on an attempt to access those actions (average Joe User is not supposed to be able to log in, so I don't want to confuse them if they somehow obtain a link to a closed section of the web site). And I can't figure out how to do that. I ended up checking the source code of the AuthorizeFilter and it seems there are only two possible outcomes: either user is allowed in, or the ChallangeResult is issued, so it seems I can't do that by using the default implementation, Is that correct? Do I need to implement my own version of AuthorizeFilter? How do I replace the default implementation?

More general question is where do I start looking for the in-depth information, so I am able to figure out myself how to proceed in such situations?

Most helpful comment

No you're still crazy :) In all honesty, aside from David wanting this for a demo this is the first time I've encountered this request.

Authorization and authentication are designed to be separate. Authorization returns a Yes this can continue, or No it can't. That's it. The authentication middlewares watch for this and decide what to do, because for each middleware the behaviour can be very different - forms does a redirect, bearer returns status codes, basic authentication would set a header and a status code.

So you're correct that no you can't do this by using the default implementation, or any authorize implementation that follows our pattern, because of this separation of concerns. Adding a 3rd behaviour would be a huge breaking change, and now we'd have to abandon the designed separation and give authorization some understanding of what happens after it's ran, something I'm very leery of doing.

As David says you could do it imperatively, as opposed to declaratively by calling Authorization within your method and then, instead of returning ChallengeResult, do what you want, in this case, redirecting to another page which returns a forbidden status code, and hopefully a nice error message.

All 36 comments

authorization is kind of a distinct thing from identity, though related.
Authorize attribute can be configured for roles or policy name, policy is most flexible, then you control the policy by defining it in Startup.cs, ie what roles or claims or other rules are required to meet the policy. Using named policies means you can change the policies in a central location as needed without revisiting the controllers
See docs here
https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies

For example if a user is authenticated but tries to access a controller action protected by a policy and he does not meet the requirement he will get the 403. Authorize attribute without any policy or roles just means the user must be authenticated, but that is just the most simple use of AuthorizeAttribute. It takes optional parameters such as policy name.

[Authorize(Policy="SomeNamedPolicy")]

If you want some real world code to study where lots of the concepts are tied together you can take a look at my cloudscribe Core project

@agr I had this exact discussion with @DamianEdwards and @blowdart last week (so no you're not crazy). There's no way to do this using the authorize attribute and there's no way to determine the behavior (challenge for forbid) based on whether a user is logged in or not.

The only way to do this would be to manually write the code to call Forbid if the policy check failed (so you won't be able to use the attribute).

No you're still crazy :) In all honesty, aside from David wanting this for a demo this is the first time I've encountered this request.

Authorization and authentication are designed to be separate. Authorization returns a Yes this can continue, or No it can't. That's it. The authentication middlewares watch for this and decide what to do, because for each middleware the behaviour can be very different - forms does a redirect, bearer returns status codes, basic authentication would set a header and a status code.

So you're correct that no you can't do this by using the default implementation, or any authorize implementation that follows our pattern, because of this separation of concerns. Adding a 3rd behaviour would be a huge breaking change, and now we'd have to abandon the designed separation and give authorization some understanding of what happens after it's ran, something I'm very leery of doing.

As David says you could do it imperatively, as opposed to declaratively by calling Authorization within your method and then, instead of returning ChallengeResult, do what you want, in this case, redirecting to another page which returns a forbidden status code, and hopefully a nice error message.

Hmmm I like this discussion, thank you guys

OK, authorization and authentication are supposed to be separate. I understand that, what I don't understand is why "Authorization failed" result manifests itself in a ChallengeResponse object in the AuthorizeFilter? I don't understand why I as a developer don't have a choice of response here. Checking for authorization in the method is OK if you have one such method, but if you have many, you need something else. A custom authorization filter? Then I'd have to dispose of all AuthorizeAttribute usage and make sure my custom filter is used everywhere. But then there is this quote in the docs: "You should only write a custom authorization filter if you are writing your own authorization framework". Well, no, I don't want to write my own authorization framework, I just want existing one not to show log in form for any unauthorized action.

Speaking of checking authorization in the method, there is another thing that confuses me: the ForbidResponse. Naming suggests that it has something to do with the HTTP 403 status which does not assume that logging in would change the response, but in reality, issuing the ForbidResponse redirects you to the log in page.

The scenario I have in mind is a portfolio web site: users can access it, see my work, and maybe contact me. No user scenario assumes logging in, there is no UI for them to suggest they might log in. Yet I, as an administrator, have an access to a site's control panel where I can change stuff.

I suppose the easiest workaround is to set LoginPage to point to 403 error page, but that sounds like a terrible hack.

Anyway, before this turns into rant (if not already), @blowdart, you mention that whatever I want does not "follow your pattern", is this pattern formalized somewhere, so I could go and read what drives the development of authorization and authentication mechanisms?

Simply put - no-one has ever requested the ability to do what you want before, so it's not a scenario we've thought of.

The driver for the rewrite was that writing custom authorize attributes resulted in lots of duplicated code customers shouldn't have had to write, and often custom attributes were hard to test, and upon occasion had bypasses. Hiding a login page smells of security by obscurity too :)

And note that it's an authorization middleware's decision on how to react to ChallengeResponse, or ForbidResponse. It may not end up in a redirect, just because cookie middleware does that it doesn't mean all middleware will do it. Bearer auth, for example, does not.

I suppose the easiest workaround is to set LoginPage to point to 403 error page, but that sounds like a terrible hack.

It is, don't do that 馃槃 .

Hiding a login page smells of security by obscurity too :)

I don't see why. The site doesn't allow arbitrary people to login.

And note that it's an authorization middleware's decision on how to react to ChallengeResponse, or ForbidResponse. It may not end up in a redirect, just because cookie middleware does that it doesn't mean all middleware will do it. Bearer auth, for example, does not.

That isn't the problem, the problem is that you can't change this behavior without rewriting pretty much everything and using the IAuthorizationService directly. Ideally, you'd be able to override on a per policy level if you want to challenge or forbid as a result of authorization passing/failing. Right now, the logic says, if you're not authenticated, challenge, otherwise if you are authenticated and authorization fails, then forbid. So a random unauthenticated user is always challenged (which means bounced to the login page for cookie authentication).

Isn't that the normal path that blogs follows?

All pages read only and you either have an "Admin" link on your page somewhere or you just go to /admin and you are prompted for login?

That's @blowdart 's point but in this case, @agr would like to return an access denied (forbid) instead of going to the login page.

You can always go SPA. index.html is 200. All the APIs would return 403 unless authenticated.

Ideally, you'd be able to override on a per policy level if you want to challenge or forbid as a result of authorization passing/failing.

But by doing it in policy you know have to have authorization aware of authentication behaviour and that's something we've deliberately designed not to happen, and it would take a lot of demand in order to break that specific design decision. It's just like timeouts in Kestrel, you want some in MVC because they're MVC specific and Kestrel doesn't know or care what's running on top of it.

So, if we change the cookie auth settings to be as follows,

c# app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationScheme = "AdministrationCookie", LoginPath = new PathString("/Account/Login/"), AccessDeniedPath = new PathString("/Account/Forbidden/"), AutomaticAuthenticate = true, AutomaticChallenge = false });

And change AutomaticChallenge to be false then you'll get the raw status code back, a 401 or a 403. Then you could use the status code error page middleware with app.UseStatusCodePages(); to intercept that, and return a suitable page for both status codes. By using a specific scheme you could then support the normal login/redirect behaviour and have different behaviour for admin pages, by specifying the applicable scheme name in the policy.

But by doing it in policy you know have to have authorization aware of authentication behaviour and that's something we've deliberately designed not to happen, and it would take a lot of demand in order to break that specific design decision.

Except, we introduced this new IPolicyEvaluator in 2.0 https://github.com/aspnet/Security/tree/879f0b7f4053a34b44e34bd22c788c6129115175/src/Microsoft.AspNetCore.Authorization.Policy which basically hard codes this behavior (BTW this is what the AuthorizeFilter uses now). Maybe we could look at something there since that's where the logic is today (https://github.com/aspnet/Security/blob/879f0b7f4053a34b44e34bd22c788c6129115175/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs#L86).

An interface hard codes nothing :)

The default implementation still doesn't change that you can turn automatic challenge off, and end up with the raw status codes, and act as you see fit after that.

Maybe we should add a PolicyEvaluatorOptions that lets you specify the behavior.

No - it's up to the authn service to decide how to react, it's NOT an authz concern. All that is happening there is "If you're not logged in, it's a 401, if you are it's a 403".

In 2.0 you can replace that entire evaluator with whatever you want, but what you want is unexpected and surprising, and that's not a good idea for security pieces.

Except the policy evaluator is actually the one deciding forbidden vs challenge. The caller then uses that results to call Forbid() or Challenge() which results in the "appropriate behavior" depending on the effective authentication scheme.

Yes, but that's just a return value that matches what people expect, it's not doing anything. That's the authn pieces. Thus keeping things seperate and not surprising.

Yes, but that's just a return value that matches what people expect, it's not doing anything. That's the authn pieces. Thus keeping things seperate and not surprising.

@blowdart All @agr wants to do is change that behavior, without changing all of the infrastructure. Changing the result of that method affects the MVC AuthorizeFilter as thats what it uses to determine if to challenge or forbid https://github.com/aspnet/Mvc/blob/f824704741dcd4bfdb0d6ce6351dea41e56205df/src/Microsoft.AspNetCore.Mvc.Core/Authorization/AuthorizeFilter.cs#L136-L145.

@agr Unfortunately, in 1.x there's no service for this, so you'd need to write a custom AuthorizeFilter (since that's where the logic is). Otherwise go back to the imperative approach or just forget it and deal with the login screen showing up.

@davidfowl Or in 1.x @agr turns off teh automatic challenge as I suggested and uses error pages to direct the user.

@agr does that approach work for you?

Yes we introduced the PolicyEvaluator to be responsible for determining Challenge/Forbid behavior, so its certainly reasonable to make it easier to configure via a new PolicyEvaluatorOptions

But for 2.0, its likely going to be plug in your own to change the default behavior, we could make it easier to configure for 2.1

I'll try the AutomaticChallenge thing (I might not have time to do it today though), but I think I tried that and it caused exception to be thrown when methods marked as Authorize were called by unauthenticated users (something about "No middleware for authentication type 'Automatic'").

Do you have any authn middleware wired up yet?

Hm... I tested it on a default application fresh from VS template and setting AutomaticChallenge to false there makes it return HTTP status 401 (which is wrong by the way in this situation because the response is missing the WWW-Authenticate header).

But in my application when I do the same I get:

InvalidOperationException: No authentication handler is configured to handle the scheme: Automatic
Microsoft.AspNetCore.Http.Authentication.Internal.DefaultAuthenticationManager+<ChallengeAsync>d__13.MoveNext()
... long stack trace omitted

I guess I messed something up with the setup. My difference from the template code is that I additionally use the Google authentication (which is also set up with AutomaticChallenge = false), otherwise I'm not sure what's happening. I.e. I use UseIdentity + UseGoogleAuthentication.

Did you set AutomaticChallenge to false in the identity cookie?

services.AddIdentity(o => o.Cookies.ApplicationCookie.AutomaticChallenge = false);

Yes:

            services.AddIdentity<User, IdentityRole>(options =>
            {
                options.Cookies.ApplicationCookie.AutomaticChallenge = false;
                options.Cookies.ApplicationCookie.AutomaticAuthenticate = true;
                options.Cookies.ExternalCookie.AutomaticChallenge = false;
                options.Cookies.TwoFactorRememberMeCookie.AutomaticChallenge = false;
                options.Cookies.TwoFactorUserIdCookie.AutomaticChallenge = false;
            })

and for the Google setup:

            app.UseGoogleAuthentication(new GoogleOptions
            {
                ClientId = config["Auth:Google:ClientId"],
                ClientSecret = config["Auth:Google:Secret"],
                AutomaticChallenge = false
            });

The lack of a scheme in the header is correct, because cookie based auth isn't considered an auth for HTTP, because it doesn't use the auth header. So it is what it is, and that's how it will stay. Because with cookies we expect you to do the redirect, which is the very thing you don't want to do :)

Guys please do put an example with OAuth 2 Refresh tokens. Much needed.

Any particular provider? They all do refresh tokens differently.

The one that works best with Asp.net Core 2.0

@shyamal890 Google's the first one I got working:
https://github.com/aspnet/Security/pull/1314

Note this is for the latest dev branch, not preview2.
Something similar would work for 1.x except that there's no way to get the options later, you'd have to duplicate the settings.

The one that works best with Asp.net Core 2.0

@Tratcher perhaps he means with OIDC MW and IdentityServer. Especially re-storing RTs int he properties if you're using the cookie storage -- it's not pleasant code.

@brockallen updated with OIDC: https://github.com/aspnet/Security/pull/1314. It's not much different. Note I tested it with AAD, but everything looked pretty standard.

So, just to put my comment in context. I actually wanted to know how to implement OAuth 2 refresh token scenario with new Identity provider and not with 3rd party.

Microsoft.AspNetCore.Identity.Service? It should be the same as my OIDC sample.

Closed as old & discussion

Was this page helpful?
0 / 5 - 0 ratings