Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blazor Authorization Should Redirect to Challenge When Default Challenge Scheme is Set #13709

Closed
ryanelian opened this issue Sep 5, 2019 · 29 comments
Labels
area-blazor Includes: Blazor, Razor Components question
Milestone

Comments

@ryanelian
Copy link

ryanelian commented Sep 5, 2019

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 secure@microsoft.com. 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:

@ryanelian ryanelian changed the title Razor Authorization Does Not Redirect To Challenge? Blazor Authorization Does Not Redirect To Challenge? Sep 5, 2019
@Tratcher Tratcher added the area-blazor Includes: Blazor, Razor Components label Sep 5, 2019
@mkArtakMSFT
Copy link
Member

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?

@mkArtakMSFT mkArtakMSFT added this to the Discussions milestone Sep 5, 2019
@ryanelian
Copy link
Author

ryanelian commented Sep 5, 2019

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

@ryanelian ryanelian changed the title Blazor Authorization Does Not Redirect To Challenge? Blazor Authorization Should Redirect to Challenge When Default Challenge Scheme is Set Sep 5, 2019
@danroth27
Copy link
Member

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>

@ryanelian
Copy link
Author

ryanelian commented Sep 6, 2019

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.

@daver77
Copy link

daver77 commented Oct 11, 2019

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?

@javiercn
Copy link
Member

@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.

@javiercn
Copy link
Member

// 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.

@daver77
Copy link

daver77 commented Oct 11, 2019

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

@javiercn
Copy link
Member

@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.

@los93sol
Copy link

@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.

@quoctuancqt
Copy link

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

@HrDahl
Copy link

HrDahl commented Nov 11, 2019

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]

@oatsoda
Copy link

oatsoda commented Dec 2, 2019

@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

@StaticBR
Copy link

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]

@daver77
Copy link

daver77 commented Jan 20, 2020

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

@AlMorton
Copy link

AlMorton commented Mar 1, 2020

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.

@mjnorman
Copy link

mjnorman commented Mar 3, 2020

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.

@everttimmer
Copy link

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.

@TomaszGrzmilas
Copy link

TomaszGrzmilas commented May 5, 2020

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 ?

@everttimmer
Copy link

everttimmer commented May 5, 2020

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...

@TomaszGrzmilas
Copy link

TomaszGrzmilas commented May 6, 2020

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.

@everttimmer
Copy link

everttimmer commented May 6, 2020 via email

@XomegaNet
Copy link

@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: #19148 (comment)

@philipgierszal
Copy link

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
<AuthorizeView>
    <NotAuthorized>
        <RedirectToLogin/>
    </NotAuthorized>
    <Authorized>
        @Body
    </Authorized>
</AuthorizeView>

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. 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.

  1. Changing the Authentication.razor component, by adding an Empty Layout
@using Shared
@layout EmptyLayout 
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

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

<div class="main">
    <div class="content px-4">
        @Body
    </div>
</div>

@zanyar3
Copy link

zanyar3 commented Jun 25, 2020

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 />

@imtrobin
Copy link

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.

@danroth27
Copy link
Member

@imtrobin
Copy link

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

@ghost
Copy link

ghost commented Sep 20, 2020

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!

@ghost ghost closed this as completed Sep 20, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Oct 20, 2020
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components question
Projects
None yet
Development

No branches or pull requests