Aspnetcore: Blazor Authorization Should Redirect to Challenge When Default Challenge Scheme is Set

Created on 5 Sep 2019  Â·  29Comments  Â·  Source: dotnet/aspnetcore

If you believe you have an issue that affects the security of the platform please do NOT create an issue and instead email your issue details to [email protected]. Your report may be eligible for our bug bounty but ONLY if it is reported through email.

Describe the bug

I expected using @attribute [Authorize] to bounce me to my OpenID Connect login page, but instead it displays "Not Authorized" message.

To Reproduce

Steps to reproduce the behavior:

  1. Using this version of ASP.NET Core 3.0.0-preview9.19424.4
  2. Run this code:

Startup.cs - ConfigureServices

services.AddAuthentication(options =>
{
    options.DefaultScheme = "My.WebApp";
    options.DefaultChallengeScheme = "Accelist";
}).AddCookie("My.WebApp")
.AddOpenIdConnect("Accelist", "Accelist SSO", options =>
{
    options.ClientId = Configuration["OIDC:ClientId"];
    options.ClientSecret = Configuration["OIDC:ClientSecret"];
    options.Authority = Configuration["OIDC:AuthorityBaseUri"];

    // https://auth0.com/docs/api-auth/which-oauth-flow-to-use
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.RequireHttpsMetadata = Env.IsProduction();

    options.Scope.Add("email");
    options.Scope.Add("phone");

    // https://joonasw.net/view/adding-custom-claims-aspnet-core-2
    // https://leastprivilege.com/2017/11/15/missing-claims-in-the-asp-net-core-2-openid-connect-handler/
    // https://www.jerriepelser.com/blog/authenticate-oauth-aspnet-core-2/
    options.GetClaimsFromUserInfoEndpoint = true;
    options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
});

Startup.cs - Configure

app.UseCookiePolicy();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization(); // <-- Is this really needed? HELP!

app.UseRouting();

app.UseEndpoints(endpoints =>
{
    //endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    endpoints.MapBlazorHub();
    endpoints.MapFallbackToPage("/_Host");
});

Index.Razor

@page "/"
@attribute [Authorize]

<h1>Hello, world!</h1>

Welcome to your new app.

App.Razor

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <CascadingAuthenticationState>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </CascadingAuthenticationState>
    </NotFound>
</Router>
  1. With CTRL + F5 on Visual Studio 2019 Preview 16.3
  2. See screenshot below

Expected behavior

I should get redirected using my default challenge scheme

Screenshots

image

Additional context

Add any other context about the problem here.
Include the output of dotnet --info

.NET Core SDK (reflecting any global.json):
 Version:   3.0.100-preview9-014004
 Commit:    8e7ef240a5

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.18362
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\3.0.100-preview9-014004\

Host (useful for support):
  Version: 3.0.0-preview9-19423-09
  Commit:  2be172345a

.NET Core SDKs installed:
  2.1.801 [C:\Program Files\dotnet\sdk]
  2.2.401 [C:\Program Files\dotnet\sdk]
  3.0.100-preview9-014004 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0-preview9.19424.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0-preview9-19423-09 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.0.0-preview9-19423-09 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

To install additional .NET Core runtimes or SDKs:
  https://aka.ms/dotnet-download

Following tutorial from:

area-blazor question

Most helpful comment

This scenario can be accomplished by first defining a RedirectToLogin component like this:

@inject NavigationManager Navigation
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo("Identity/Account/Login", true);
    }
}

and then use AuthorizeRouteView like this:

<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
    <NotAuthorized>
        <RedirectToLogin />
    </NotAuthorized>
</AuthorizeRouteView>

All 29 comments

Thanks for contacting us, @ryanelian.
The behavior you're observing is by-design. The AuthorizeRouteView has a property which allows to set view for unauthorized case, and you haven't defined it, which results in the default message to show up.
@danroth27 do you have updated docs regarding this already?

The AuthorizeRouteView has a property which allows to set view for unauthorized case

Thank you for the confirmation.

Can we have the redirect just so the app behaves like any other ASP.NET MVC web app? Our customers love (cough demand cough) the ASP.NET MVC redirect URI behavior / feature whenever an unauthorized user access a random page. (e.g. /something --> /login?redirectUri=/something --> /something again)

It'll be a hard sell to our customers if Blazor cannot do that...

Please add a last-minute addition to Blazor for this specific feature? I know you guys are going GA at the end of September, but I would highly value this feature being available without waiting for version 3.1.0

This scenario can be accomplished by first defining a RedirectToLogin component like this:

@inject NavigationManager Navigation
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo("Identity/Account/Login", true);
    }
}

and then use AuthorizeRouteView like this:

<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
    <NotAuthorized>
        <RedirectToLogin />
    </NotAuthorized>
</AuthorizeRouteView>

Amazing. Thank you. This should probably do the trick...

// I'm not sure the redirectUri here is secure, but whatever, it works.

// razor page login page
    public class LoginModel : PageModel
    {
        public async Task OnGet(string redirectUri)
        {
            await HttpContext.ChallengeAsync(new AuthenticationProperties
            {
                RedirectUri = redirectUri
            });
        }
    }

// razor page logout page
    public class LogoutModel : PageModel
    {
        public async Task<IActionResult> OnGet()
        {
            await HttpContext.SignOutAsync();
            return Redirect("/");
        }
    }

// blazor component redirect to login WHEN NOT authenticated
@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor ctx

@code {
    protected override void OnInitialized()
    {
        if (ctx.HttpContext.User.Identity.IsAuthenticated == false)
        {
            var challengeUri = "/login?redirectUri=" + System.Net.WebUtility.UrlEncode(Navigation.Uri);
            Navigation.NavigateTo(challengeUri, true);
        }
    }
}

<p>
    You are not authorized to access this page.
</p>

// app razor

        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
            <NotAuthorized>
                <Challenge></Challenge>
            </NotAuthorized>
        </AuthorizeRouteView>

Although, I still insist that this feature is critical, and must be in the base framework without requiring developers to write that on our own.

This issue still seems to be present in the RTM release. I am authenticating with AzureAD so when I add the following to my Blazor page I am expecting it to automatically challenge, it is not.

@attribute [Authorize(Policy = Policies.AccessRole)]

Do I need to do something else for this to work?

@daver77 You need to follow @danroth27 suggestion, Blazor apps follow a different model than traditional web pages and performing a traditional ASP.NET Core challenge is not possible. As an alternative, add an additional endpoint, redirect the user there as @danroth27 suggests. perform the challenge from there and redirect back to the blazor app at the end of the login flow.

// razor page login page
    public class LoginModel : PageModel
    {
        public async Task OnGet(string redirectUri)
        {
            await HttpContext.ChallengeAsync(new AuthenticationProperties
            {
                RedirectUri = redirectUri
            });
        }
    }

You should do validation over that redirect uri to ensure its local, otherwise you are opening yourself to open redirect attacks.

@javiercn thanks, that may be a work-around but the fact is that the Authorize attribute does not work.

@daver77 That is not the case.

The Authorize attribute is just metadata that each framework decides how to interpret. Performing a challenge is just the most common behavior, but it is not a prescriptive one.

@daver77 That is not the case.

The Authorize attribute is just metadata that each framework decides how to interpret. Performing a challenge is just the most common behavior, but it is not a prescriptive one.

I think the issue is that Blazor simply doesn't interpret the behavior as expected. I just started porting a bunch of our apps over and ran into this myself and was pretty surprised that it did not follow the same behavior we see in controllers and razor pages. Not a big deal to resolve it, but the documentation should be updated at the very least with the information on how to make it work generically.

This scenario can be accomplished by first defining a RedirectToLogin component like this:

@inject NavigationManager Navigation
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo("Identity/Account/Login", true);
    }
}

and then use AuthorizeRouteView like this:

<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
    <NotAuthorized>
        <RedirectToLogin />
    </NotAuthorized>
</AuthorizeRouteView>

Hi Dan,

I tried your suggestion, but it doesn't work, there is nothing happen when I come to an authorized page. Seems like "NotAuthorized" only displays content instead of a component.

Thanks.

image

image

image

I appreciate the workaround shown by @danroth27 and @ryanelian but seen from a .NET Core enthusiast we really need [Authorize] attribute in Blazor components to redirect to Challenge. That is primarily because of consistency with the way MVC works with [Authorize].

Also, it seems that @attribute [AllowAnonymous] has no place in Blazor Components as it is today, as we can not place a global Authorize on all components at once. Aka. instead of having everything open and then closing with [Authorize], I would prefer to have everything closed and then open with [AllowAnonymous]

@quoctuancqt You need to add a "using" statement to wherever you added the RedirectToLogin component.

If you added it to a folder called Components in your app (called BlazorApp2) then add the following to the _Imports.razor file:

@using BlazorApp2.Components

The solution above will result in an "Microsoft.AspNetCore.Components.NavigationException" because you are disrupting the page build process.
If you want to secure the whole bloazor page use the app.net security mechanisms. This could be done by simply requesting an authorization for the whole _Host.cshtml file.
For example by adding: @attribute [Authorize]

Not sure if people are aware, you can use this Razor Page method to secure Blazor Pages

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/razor-pages-authorization?view=aspnetcore-3.1#require-authorization-to-access-a-folder-of-pages

Not sure if people are aware, you can use this Razor Page method to secure Blazor Pages

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/razor-pages-authorization?view=aspnetcore-3.1#require-authorization-to-access-a-folder-of-pages

But this isn't relevant to Blazor webassembly: those razor page options aren't available as methods on the WebAssemblyHostBuilder.Services property. However, it's difficult to ascertain whether or not the original question is regarding Blazor webassembly or not. I'm also encountering the same issue with the AuthroiseRouteView not redirecting, 'NotAuthorized' requests to my custom login component.
Apart from that, I'm really loving Blazor.

Putting this code

@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

in the Host.cshtml, provides the behavior that navigating to the main / page redirects to the login as @StaticBR mentioned above.

However, putting that same code in a razor component such as fetchdata.razor from the sample, does not produce a redirect, but instead produces the <NotAuthorized> markup from the App.razor page:

            <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <h1>Sorry</h1>
                    <p>You're not authorized to reach this page.</p>
                    <p>You may need to log in as a different user.</p>
                </NotAuthorized>
                <Authorizing>
                    <h1>Authentication in progress</h1>
                    <p>Only visible while authentication is in progress.</p>
                </Authorizing>
            </AuthorizeRouteView>
            </Found>

I would have expected a redirect in either case.

I am not a very experienced webdeveloper but it seems to me that the [Authorize] attribute not directly redirecting to the login page, is far more flexible because you can decide what happens when the user tries to access a page that requires authentication. Alternate content that explains the problem and offers a login button is far more user-friendly than redirect and gives the user an option to bail out and select non-authorized content instead of beeing confronted with a login dialog.

The solution as described here ( the razor page as model in _host.cshtml ) combined with both the component and maybe some manual login / logout links on the top navbar is far more flexible.

I've been busy with this problem for about 3 days now and the solution here is workable and flexible because it does not affect the authorization strategy of Blazor.

Hi,

I'm trying to make my app available only for logged in users.

I set App.razor:

 
< CascadingAuthenticationState>
    < AuthorizeView>
        < Authorized>
            < Router AppAssembly="@typeof(Program).Assembly">
                < Found Context="routeData">
                    < AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
                < /Found>
                < NotFound>
                    < LayoutView Layout="@typeof(MainLayout)">
                        < p>Sorry, there's nothing at this address.< /p>
                    < /LayoutView>
                < /NotFound>
            < /Router>
        < /Authorized>
        < NotAuthorized>
            < RedirectToLogin/>
        < /NotAuthorized>
    < /AuthorizeView>
< /CascadingAuthenticationState>
 

And I get this exception:
image

When I click Continue application is redirected to login page. So it's working ok except this exception.

I'm doing something wrong ?

As far as i can see, the error is in the app.razor. You are using the Navigation Manager to navigate to the login page but for that to work, routing should be initialized... The problem is in the fact that you have the Routing initialization within an AuthorizeView which, of which the NotAuthorized section is triggered directly... before routing is initialized...

So, you should remove the AuthorizeView from the app.razor.

Another thing is that, if you use Role authorization, the application will check that if you reach a section that has an authorization attribute. If you are logged in, but not authorized for a section, the current implementation of the RedirecToLogin component will end up in a loop logging you in.

A way to resolve this is to create a check inside the RedirecToLogin component in order to determine if the user is currently logged in. A redirection is not necessary in that case.

Also, if you want to secure your entire (hosted) application, you should configure it in the or Startup.cs and modify:

app.UseEndpoints(endpoints => { endpoints.MapBlazorHub().RequireAuthorization(); });

Anybody who thinks this reaction sucks, please correct me...

image

Adding RequireAuthorization() didn't do nothing :/ You can see index page without log in.

I also change App.razor, now routing is initialized:

image

And I still got exception described before.

Sorry, my fail

services.AddMvc(config =>
{
//only allow authenticated users
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();

config.Filters.Add(new AuthorizeFilter(policy));

});

Regards,
Evert

Verzonden vanuit Mail voor Windows 10

Van: Tom Thunderforest
Verzonden: woensdag 6 mei 2020 07:42
Aan: dotnet/aspnetcore
CC: everttimmer; Comment
Onderwerp: Re: [dotnet/aspnetcore] Blazor Authorization Should Redirect toChallenge When Default Challenge Scheme is Set (#13709)

Adding RequireAuthorization() didn't do nothing :/ You can see index page without log in.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or unsubscribe.

@TomaszGrzmilas I think the NavigationException you are getting is normal - that's how the NavigationManager aborts the operation when Navigating to another view. You would only see it in debug, and you can select the VS option to not break on such navigation exceptions.

If you want to implement the login view also as a Blazor component, rather than a razor page, for consistent look and feel, then you can use the SignInManager from the Xomega.Framework.Blazor package to do the Challenge for the login redirect, as well as SignIn and SignOut.

Here is the full explanation of how it works with references to the appropriate code: https://github.com/dotnet/aspnetcore/issues/19148#issuecomment-624862445

Thanks to this post, I was able to achieve forcing the user to log in, after the MainLayout.razor has been loaded, which was not my desired behaviour. I was able to overcome this with the help of a couple SO answers.

  1. Changing the MainLayout.razor

```@inherits LayoutComponentBase





@Body


This will immedietaly redirect to RedirectToLogin.razor, which then redirects to Authentication.razor. This component tries to render the layout, which creates an infinite [loop](https://stackoverflow.com/a/61565996). To overcome that behaviour, you can create a seperate component redirecting to your Log In page, or change the Authentication.razor component, which I will show here.

2. [Changing the Authentication.razor component, by adding an Empty Layout](https://stackoverflow.com/questions/59518988/disable-layout-for-page-under-blazor)
```@page "/authentication/{action}"
@using Shared
@layout EmptyLayout 
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

   @code{
            [Parameter] public string Action { get; set; }
        }
  1. EmptyLayout.razor

```inherits LayoutComponentBase

@Body

```

When use you My Code if Authorized&NotAuthorized work
only for manage redirect page logout or any page NotAuthorized

App.razor

`    <CascadingAuthenticationState>
        <Router AppAssembly="@typeof(Program).Assembly">
            <Found Context="routeData">
                <AuthorizeView>
                    <Authorized>
                        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
                    </Authorized>
                    <NotAuthorized>
                        <CascadingAuthenticationState>
                            <RedirectToLogin />
                        </CascadingAuthenticationState>
                    </NotAuthorized>
                </AuthorizeView>
            </Found>
            <NotFound>
                <CascadingAuthenticationState>
                    <LayoutView Layout="@typeof(EmptyLayout)">
                        <p>No Found</p>
                    </LayoutView>
                </CascadingAuthenticationState>
            </NotFound>
        </Router>
    </CascadingAuthenticationState>`

RedirectToLogin .razor

@*  only write redirect to login or any page you need *@
<iServicePayroll.Pages.Home.Login />

So what's the official solution for webassembly and third party authentication? I'm trying these various workaround, seems to getting infinite loop or not working.

Thanks dan, it's close what I have pieced together over the internet but it's really not situation. I'm not using ODIC, it's a custom authentication library with jwtoken/refresh tokens.

I used this which gets me closer , but it's missing quite a few bits.
https://www.mikesdotnetting.com/article/342/managing-authentication-token-expiry-in-webassembly-based-blazor

Thank you for contacting us. Due to a lack of activity on this discussion issue we're closing it in an effort to keep our backlog clean. If you believe there is a concern related to the ASP.NET Core framework, which hasn't been addressed yet, please file a new issue.

This issue will be locked after 30 more days of inactivity. If you still wish to discuss this subject after then, please create a new issue!

Was this page helpful?
0 / 5 - 0 ratings