Skip to content

Blazor Server auth improvements #38111

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

Open
javiercn opened this issue Nov 5, 2021 · 14 comments
Open

Blazor Server auth improvements #38111

javiercn opened this issue Nov 5, 2021 · 14 comments
Labels
area-blazor Includes: Blazor, Razor Components Blazor ♥ SignalR This issue is related to the experience of Signal R and Blazor working together enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-server feature-blazor-server-auth partner Partner ask Pillar: Complete Blazor Web Priority:1 Work that is critical for the release, but we could probably ship without triaged
Milestone

Comments

@javiercn
Copy link
Member

javiercn commented Nov 5, 2021

Looking at the feedback in these area there are several recurring pain points affecting users.

It has come up several times that there is no good mechanism to update the expiration of the cookie when using cookie authentication (our default implementation choice uses ASP.NET Identity). This is something we might want to consider improving by some sort of pinging mechanism to one of Identity's urls (like Account/Manage) via a fetch request using JS interop.

The other major challenge here is with regards to how AuthenticationService interoperates with other parts of the user app, like scoped services in their own "sub-scope". This is relevant when users are leveraging things like Entity Framework for which we recommend using owning component scope as well as other libraries like HttpClientFactory that create their own scopes. In these cases, users are not able to access the current authenticated user within those services or any information associated with the circuit. The challenge here involves "being able" to have a "per circuit" scope that flows to any "nested scope".

The other piece of feedback we've received in this area involves how to communicate information to a given circuit from outside of the context of the circuit. This involves passing information during "circuit" startup and while the circuit is running. We might be able to create some documentation to specifically define how these pattern should work or streamline it in our default implementation.

@javiercn javiercn added this to the .NET 7 Planning milestone Nov 5, 2021
@mkArtakMSFT mkArtakMSFT added the Priority:2 Work that is important, but not critical for the release label Nov 11, 2021
@mkArtakMSFT mkArtakMSFT modified the milestones: .NET 7 Planning, Backlog Nov 11, 2021
@ghost
Copy link

ghost commented Nov 11, 2021

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@Sebazzz
Copy link

Sebazzz commented Feb 10, 2022

I'd like to add my own perspective on this: there is no good framework provided way to allow SPA Blazor-server applications to work with Cookie Authentication.

Cookie refresh

When Blazor Server works in the most ideal situation, you have a persistent websockets connection with the server. That means during your usage of the web application you might never be in the situation where you make a connection to an endpoint that requires authentication. You will probably request assets like static files, but those generally do not require authentication. Only endpoints that call AuthenticateAsync will have any sliding expiration cookie refreshed.

You can't say that you don't need any authentication cookie refreshed once you're using the web application because:

  1. When you accidently or intentionally refresh the page, it is undesirable to get logged out. Users do not like that.
  2. Some 3rd party components, like Kendo Blazor upload require a separate endpoint to handle the file upload. Obviously we need to have authentication on that endpoint - so a non-expired cookie.

Keep alive

To keep the cookie updated you need a lot of extra machinery, for instance in the form of a keep-alive mechanism. That means you'Il get something like this:

public static void MapKeepAlive(this IEndpointRouteBuilder endpointBuilder, string path) {
    endpointBuilder.MapGet(path, async (ctx) => {
            // Authentication keeps the cookie alive. KeepAlive is only used when the user is active in the application.
            AuthenticateResult result = await ctx.AuthenticateAsync();

            string? userName = result.Principal?.Identity?.Name;

            HttpResponse response = ctx.Response;
            response.ContentType = "text/plain";
            
            // Make sure keepalive url is not cached
            ResponseHeaders typedHeaders = response.GetTypedHeaders();
            typedHeaders.CacheControl = new() {NoCache = true, NoStore = true, MaxAge = TimeSpan.Zero, MustRevalidate = true};

            await response.WriteAsync($"OK - {userName ?? "(not authenticated)"}");
        }
    ).WithDisplayName("KeepAlive");
}

And of course, at the client side you need to call this keep-alive either through an iframe or asynchronous javascript call. At minimum you should call it at [cookie expiration time] divided by 2 so it will fall into the sliding cookie expiration window. However, you don't want a persistent keep alive but only when the user is actually doing something in the application (like navigating, clicking on something that doesn't necessary result in navigation, or typing).

However, this is quite difficult to get that stable so eventually we resorted to just make sure that the keepalive endpoint is called at least every [interval] when it is detected that the user is doing something.

Anti Forgery Tokens

In some cases it is necessary to make requests to non-Blazor endpoints (for instance the Kendo Upload I mentioned earlier). In that case you'd like those endpoints to be protected by CSRF tokens. There is no framework built-in (recommended) way that allows these anti forgery tokens to be made available.

Also when it comes to authentication, it is very awkward to need to have separate Razor forms (which separate layout pages, possibly Javascript libraries to augment the forms, not being able to share common form components, etc) to allow authentication in an otherwise SPA Blazor application. We eventually figured out a way, by using a form that posts to an endpoint and allow that endpoint to redirect back to the Blazor page.

In the _Host.cshtml:

@inject IAntiforgery Antiforgery

// ...

 @(await Html.RenderComponentAsync<App>(renderMode: RenderMode.Server, new { AntiforgeryInformation = Antiforgery.GetAndStoreTokens(HttpContext) }))

In App.razor:

<CascadingValue IsFixed="true" Value="AntiforgeryInformation">
    @* The Router, AuthorizedRouteView, CascadingAuthenticationState, etc here *@
</CascadingValue>

@code {
    [Parameter]
    public AntiforgeryTokenSet AntiforgeryInformation { get; set; } = null!;
}

On the login form component:

<CascadingValue IsFixed="true" Value="EditContext">
<form method="post" action="/toegangscontrole/inloggen" @ref="@_formRef">
    <AntiforgeryToken/>

    @*Form fields here *@

   <button class="button button__primary" id="login-button" type="button" @onclick="this.OnLogin" @ref="@_submitButtonRef">Inloggen</button>
</form>
</CascadingValue>

@code {
 private async Task OnLogin() {
        EditContext.Validate();

        if (EditContext.GetValidationMessages().Any() == false)
        {
            await JSRuntime.InvokeVoidAsync("app.submitForm", _formRef);
        }
    }
}

And with the AntiForgeryToken component being:

public sealed class AntiforgeryToken : ComponentBase {
    [CascadingParameter]
    public AntiforgeryTokenSet? Antiforgery { get; set; }

    /// <inheritdoc />
    protected override void BuildRenderTree(RenderTreeBuilder builder) {
        if (this.Antiforgery == null) throw new InvalidOperationException("Anti forgery information is not present - please use this component only within a context where anti forgery information is present");

        builder.OpenElement(0, "input");
        builder.AddAttribute(1, "type", "hidden");
        builder.AddAttribute(2, "name", this.Antiforgery.FormFieldName);
        builder.AddAttribute(3, "value", this.Antiforgery.RequestToken);
        builder.CloseElement();
    }

}

If you take a upload component you take the cascading anti forgery token and pass it into the HTTP request using headers.

This does work. and is protecting all endpoints from CSRF - but a framework provided way that is also supported by Microsoft would be much better.

Session hijacking / CSRF

Blazor allows a fallback to long polling using POST requests to the Blazor endpoint. However, this also opens
the possibility to session hijacking an authenticated session. If one existing circuit is considered to be authenticated,
and you can guess the session ID, you can access that circuit without the passing any authentication token.

You can argue that the session ID is random enough that it cannot be guessed, but some infosec consultant do not like that you can get into an authenticated session by just guessing a session ID without any need to authenticate like through a cookie. Likewise, there isn't any CSRF token necessary either.

If you do manage to break in, you can send abritrary commands in the POST payload just like this and manipulating the running circuit:

image

Final words

Not intented to be negative or anything: There are some things that need improvement on the framework level when it comes to authentication and security.

However, we ♥ Blazor and like it very much! It is a refreshing framework in the world of overengineered overly complex Javascript frameworks and the build systems that come with it. It has made us more productive on a scale I would never have expected.

I just hope the ASP.NET Core devs see this and understand the necessity of some improvements in regard to the points above.

Related issues: #34095 and #36030 (via @marwalsch) , #39932 (via @javiercn)

@javiercn
Copy link
Member Author

#39932 is meant to capture this in more detail

@javiercn
Copy link
Member Author

This does work. and is protecting all endpoints from CSRF - but a framework provided way that is also supported by Microsoft would be much better.

Our recommended way of handling things like file uploads is via the Stream APIs that we added to JS interop, which leverage the current authentication context and don't require additional measures like CSRF protection.

Blazor allows a fallback to long polling using POST requests to the Blazor endpoint. However, this also opens
the possibility to session hijacking an authenticated session. If one existing circuit is considered to be authenticated,
and you can guess the session ID, you can access that circuit without the passing any authentication token.

We don't recommend people using long polling with Blazor Server for the experience, and even if you do, the Circuit ID is a data protected identifier generated from 32 bytes of entropy using cryptographic APIs, so its as guessable as the key you use in your symmetric encryption, which means even when you use long polling you are perfectly safe.

@Sebazzz
Copy link

Sebazzz commented Feb 10, 2022

This does work. and is protecting all endpoints from CSRF - but a framework provided way that is also supported by Microsoft would be much better.

Our recommended way of handling things like file uploads is via the Stream APIs that we added to JS interop, which leverage the current authentication context and don't require additional measures like CSRF protection.

I understand, as I mentioned: I can't control 3rd party components or other use cases where you might want to do requests outside of an active circuit.

Blazor allows a fallback to long polling using POST requests to the Blazor endpoint. However, this also opens
the possibility to session hijacking an authenticated session. If one existing circuit is considered to be authenticated,
and you can guess the session ID, you can access that circuit without the passing any authentication token.

We don't recommend people using long polling with Blazor Server for the experience, and even if you do, the Circuit ID is a data protected identifier generated from 32 bytes of entropy using cryptographic APIs, so its as guessable as the key you use in your symmetric encryption, which means even when you use long polling you are perfectly safe.

Don't recommend, perhaps, but the fallback is enabled by default.

I agree that the chance is very small that anything can be guessed, but it still can be seen as circumvention of authentication controls.

To clarify: In Javascript, authorization and CSRF cookies are generally not accessible because we place them with the HttpOnly flag. The circuit session ID however is accessible with Javascript, and is thus exploitable. If something would happen that caused this session ID to leak to an external server (via a form post, perhaps), then it is possible for that external server to do requests using that session ID. No cookies or CSRF token necessary.

@javiercn
Copy link
Member Author

I agree that the chance is very small that anything can be guessed, but it still can be seen as circumvention of authentication controls.

It's not a very small chance, its an infinitesimal chance that can't be practically accomplished in a reasonable amount of time. You effectively have a similar chance of guessing the HTTP only cookie that you have of guessing the Circuit ID

To clarify: In Javascript, authorization and CSRF cookies are generally not accessible because we place them with thr HttpOnly flag. The circuit session ID however is accessible with Javascript, and is thus exploitable. If something would happen that caused this session ID to leak to an external server (via a form post, perhaps), then it is possible for that external server to do requests using that session ID. No cookies or CSRF token necessary.

The moment you have hostile code running in your app, your are already the victim of an XSS. There's no need for an attacker to try and guess or leak the circuit ID elsewhere as they can already operate from that context.

@Sebazzz
Copy link

Sebazzz commented Feb 10, 2022

The moment you have hostile code running in your app, your are already the victim of an XSS. There's no need for an attacker to try and guess or leak the circuit ID elsewhere as they can already operate from that context.

I don't agree on that. Security is provided by having security on multiple layers. Preventing XSS exploits is one of them, but if that fails we prevent leaking cookies by setting a HttpOnly flag, if that fails we have short-lived cookies that are refreshed in 10 minutes, if that fails we might have another layer of protection (for instance a Web Application Firewall).

We run our web applications with minimal privileges so they can't infect the web server in case of an exploit. We use encrypted connection strings, Azure Key Vault or integrated security so that in the case of an exploit, we don't have any password leakage. We have a firewall so that in the case of password leakage, an attacker still does not have access to the database. Security builds on layers - it is not a single supposedly airtight hatch.

And that comes back to this:

its an infinitesimal chance that can't be practically accomplished in a reasonable amount of time

The session ID is leakable, so you might be in a situation where it is leaked (and not guessed).

@javiercn
Copy link
Member Author

I got busy with other work and couldn't get back to this thread. I would like to make sure I give you a bit more of a detailed answer but at the same time I would like to avoid turning this issue into an ad-hoc security discussion about Blazor.

We do analyze security in depth before we release any product and we go through several reviews where we create threat models and review attack vectors and mitigations. I'm working with folks on the team to find a way to make these public so that you and anyone else interested can have an in-depth look and in the future I'm working on a plan to see if we can make these things public as we work through them.

I would also like to encourage you and anyone else, to take the opportunity to report any security related concern you might have through our secure channels, as if it turns out that there is an issue we overlooked, it's under normal circumstances eligible for our bug bounty program.

With that said, let me hopefully bring some clarity on some of the remaining open questions on the thread.

The session ID is leakable, so you might be in a situation where it is leaked (and not guessed).

For a session ID to leak an attacker needs access to the unencrypted requests and responses from the browser and the server or needs to be able to run code in the context of the active document.

In the first case, you should be protected the confidentiality and integrity of your communications with HTTPs.

On the second case, which is an XSS attack there are several layers of protection that we put in place as well as recommend you do so too in your app. Blazor HTML encodes things like user input on the page by default, which is the standard protection frameworks offer against XSS.

In addition to that, we recommend using CSP to ensure the integrity of all the resources loaded into your app, such as third-party scripts.

Those two measures together are what we recommend to protect your app against potentially running unknown code in the browser.

Cookies are normally used in the context of CSRF where the vector is a document from a different (malicious) origin trying to cause some side effect on your origin. The important thing to point out, is that there are two parts to this. A request token and a cookie token and the critical part is the request token, which is what the malicious origin can't replicate. The reason we use CSRF is because things like cookies (unlike tokens) get appended automatically to the requests and in some cases it is not safe to do so (hence why we also have things like same site cookies to mitigate this in a different way).

It's tempting to think of the circuit ID in similar terms, but the scenario is not the same, and that's what makes a difference.

We don't have to protect our Session ID from external origins as it is never part of any cookie, our only concern is about someone getting a hold of it by running code in the context of the document.

At that point, assuming that someone was able to do this, we look at the ramifications and the impact.

In this scenario, the moment you run code in the context of the user session, you can already do anything you want with the application as if you were the user. You might be able to send the circuit ID to an external server but we think there is no value in doing so since you can perform the attack directly. As a result, we don't think there is any need to further protect the session ID in this scenario.

One additional note in the context of authentication is that, even in this case, we always re-evaluate the authentication context on the reconnection to the server and update the application accordingly.

We run our web applications with minimal privileges so they can't infect the web server in case of an exploit. We use encrypted connection strings, Azure Key Vault or integrated security so that in the case of an exploit, we don't have any password leakage. We have a firewall so that in the case of password leakage, an attacker still does not have access to the database. Security builds on layers - it is not a single supposedly airtight hatch.

I absolutely agree with this, however different measures protect against different threats and we don't apply mitigations just because we can, but when they offer value. Each mitigation that we put in place needs to have an associated threat and impact that justifies the need for it. Otherwise we pay the cost in complexity and we don't improve the security of the application in a measurable way.

We think this is one of such cases, where if someone is already running code and can impersonate you, protecting against sending the session ID elsewhere is not worth it.

To put it in perspective of some of the things you mentioned, each of those things are meant to protect against different threats, like someone eavesdroping on your connection, storage or hijacking your process. They aren't protections meant to counter the same threat, but different threats, and there are other threats they can be potentially useless against, like if an attacker is able to run code with administrative priviledges.

The point being that those measures are put in place for a reason after evaluating the risk and impact of specific threats and the impact of the additional complexity added to the application while also taking into account that there might be other threats against which they might not effective. The context is critical for assessing whether a mitigation makes sense or not.

We think that the current mitigations we have in place offer a reasonable balance between security and complexity for applications out of the box and offer a significant amount of guidance here on how to further protect your app should you think you need to do so.

I hope this helps clarify our position around this specific aspect of how Blazor Server works and even if you don't agree with our conclusions it at least provides some insight into our analysis/decission process.






PD: If you have more questions follow-ups I'll be happy to continue the discussion, however it might take some time before I can get back with answers as we are currently focused on other work.

@mkArtakMSFT mkArtakMSFT added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Oct 11, 2022
@mkArtakMSFT mkArtakMSFT modified the milestones: Backlog, BlazorPlanning Nov 5, 2023
@mkArtakMSFT mkArtakMSFT modified the milestones: Planning: WebUI, Backlog Dec 19, 2023
@ghost
Copy link

ghost commented Dec 19, 2023

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@mkArtakMSFT mkArtakMSFT modified the milestones: Backlog, .NET 10 Planning Oct 30, 2024
@mkArtakMSFT mkArtakMSFT added triaged Blazor ♥ SignalR This issue is related to the experience of Signal R and Blazor working together and removed Priority:2 Work that is important, but not critical for the release labels Nov 4, 2024
@mkArtakMSFT mkArtakMSFT added the partner Partner ask label Nov 11, 2024
@mkArtakMSFT
Copy link
Member

Potentially related to #5297

@Kumima
Copy link

Kumima commented Nov 26, 2024

@mkArtakMSFT, can this one have a higher priority? The authentication part for Blazor has been a pain for a long time, especially now that we have Blazor Web. And to build an application, I think the authentication part is the very first consideration.

@danroth27 danroth27 added the Priority:1 Work that is critical for the release, but we could probably ship without label Jan 13, 2025
@danroth27 danroth27 modified the milestones: .NET 10 Planning, Backlog Mar 19, 2025
@danroth27
Copy link
Member

At this point we don't think this set of Blazor Server auth improvements is going to land for .NET 10, so moving this back to the Backlog. See the sub issues for their status.

@Kumima
Copy link

Kumima commented Mar 20, 2025

@danroth27 Really? The authentication part of Blazor has been a mess for a long time, which makes a lot of ppl get rid of Blazor. .NET11? Another two years?

@MichaelHochriegl
Copy link

Moving this out of .Net10 is a huge mistake IMO. The auth story with Blazor is pretty horrendous to be honest and as Blazor Server is mostly used as business apps auth should be a top priority.

The hacky ways I had to do to get refreshtokens working in Blazor Server are done with the perspective to get the removed once .Net10 drops, guess I have to explain to my PO that these will be in for another couple of years.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components Blazor ♥ SignalR This issue is related to the experience of Signal R and Blazor working together enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-server feature-blazor-server-auth partner Partner ask Pillar: Complete Blazor Web Priority:1 Work that is critical for the release, but we could probably ship without triaged
Projects
None yet
Development

No branches or pull requests

6 participants