Skip to content

Update Identity Components in Blazor project template #51134

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

Merged
merged 19 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Microsoft.AspNetCore.Components.Rendering.ComponentState.LogicalParentComponentS
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParameter(int sequence, string! name, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
*REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary<string!, object!>! routeValues) -> void
*REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode? renderMode) -> void
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddNamedEvent(string! eventType, string! assignedName) -> void
Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags.HasCallerSpecifiedRenderMode = 1 -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public void AppendRegion(int sequence)
};
}

public void AppendComponentRenderMode(IComponentRenderMode renderMode)
public void AppendComponentRenderMode(IComponentRenderMode? renderMode)
{
if (_itemsInUse == _items.Length)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -682,10 +682,8 @@ public void AddComponentReferenceCapture(int sequence, Action<object> componentR
/// Adds a frame indicating the render mode on the enclosing component frame.
/// </summary>
/// <param name="renderMode">The <see cref="IComponentRenderMode"/>.</param>
public void AddComponentRenderMode(IComponentRenderMode renderMode)
public void AddComponentRenderMode(IComponentRenderMode? renderMode)
{
ArgumentNullException.ThrowIfNull(renderMode);

// Note that a ComponentRenderMode frame is technically a child of the Component frame to which it applies,
// hence the terminology of "adding" it rather than "setting" it. For performance reasons, the diffing system
// will only look for ComponentRenderMode frames:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2141,18 +2141,27 @@ public void CannotAddComponentRenderModeToElement()
}

[Fact]
public void CannotAddNullComponentRenderMode()
public void CanAddNullComponentRenderMode()
{
// Arrange
var builder = new RenderTreeBuilder();

// Act
builder.OpenComponent<TestComponent>(0);
builder.AddComponentParameter(1, "param", 123);
builder.AddComponentRenderMode(null);
builder.CloseComponent();

// Act/Assert
var ex = Assert.Throws<ArgumentNullException>(() =>
{
builder.AddComponentRenderMode(null);
});
Assert.Equal("renderMode", ex.ParamName);
// Assert
Assert.Collection(
builder.GetFrames().AsEnumerable(),
frame =>
{
AssertFrame.Component<TestComponent>(frame, 3, 0);
Assert.True(frame.ComponentFrameFlags.HasFlag(ComponentFrameFlags.HasCallerSpecifiedRenderMode));
},
frame => AssertFrame.Attribute(frame, "param", 123, 1),
frame => AssertFrame.ComponentRenderMode(frame, null));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ internal void SetRequestContext(HttpContext context)
{
if (_context == null)
{
return null;
// We're in an interactive context. Use the token persisted during static rendering.
return base.GetAntiforgeryToken();
}

// We already have a callback setup to generate the token when the response starts if needed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state)
{
state.PersistAsJson(PersistenceKey, GetAntiforgeryToken());
return Task.CompletedTask;
}, RenderMode.InteractiveWebAssembly);
}, RenderMode.InteractiveAuto);

state.TryTakeFromJson(PersistenceKey, out _currentToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,21 @@ public void CanUseAntiforgeryTokenInWasm()
DispatchToFormCore(dispatchToForm);
}

[Fact]
public void CanUseAntiforgeryTokenWithServerInteractivity()
{
var dispatchToForm = new DispatchToForm(this)
{
Url = "forms/antiforgery-server-interactive",
FormCssSelector = "form",
InputFieldId = "value",
InputFieldValue = "stranger",
SuppressEnhancedNavigation = true,
Ready = "really-ready",
};
DispatchToFormCore(dispatchToForm);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Components.TestServer.RazorComponents;
using Components.TestServer.RazorComponents.Pages.Forms;
using Components.TestServer.Services;
using Microsoft.AspNetCore.Components.WebAssembly.Server;
using Microsoft.AspNetCore.Mvc;

namespace TestServer;
Expand Down Expand Up @@ -73,6 +72,19 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
InteractiveStreamingRenderingComponent.MapEndpoints(endpoints);

MapEnhancedNavigationEndpoints(endpoints);

endpoints.MapPost("/verify-antiforgery", (
[FromForm] string value,
[FromForm(Name = "__RequestVerificationToken")] string csrfToken) =>
{
// We shouldn't get this far without a valid CSRF token, but we double check it's there.
if (string.IsNullOrEmpty(csrfToken))
{
throw new Exception("Invalid POST to /verify-antiforgery!");
}

return TypedResults.Text($"<p id='pass'>Hello {value}!</p>", "text/html");
});
});
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@page "/forms/antiforgery-server-interactive"

@using Microsoft.AspNetCore.Components.Forms

@attribute [RenderModeInteractiveServer]

<h3>FormRenderedWithServerInteractivityCanUseAntiforgeryToken</h3>

<form action="verify-antiforgery" method="post" @formname="verify-antiforgery">
<AntiforgeryToken />
<input type="text" id="value" name="value" />
<input id="send" name="send" type="submit" value="Send" />
</form>

@if (HttpContext is null)
{
<p id="really-ready">Interactively rendered!</p>
}

@code {
[CascadingParameter]
HttpContext? HttpContext { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{
@if (_succeeded)
{
<p id="pass">Posting the value succeded.</p>
<p id="pass">Posting the value succeeded.</p>
}
else
{
Expand All @@ -42,7 +42,7 @@ else
if (OperatingSystem.IsBrowser())
{
var antiforgery = AntiforgeryState.GetAntiforgeryToken();
_token = antiforgery.Value;
_token = antiforgery.Value;
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/ProjectTemplates/README-BASELINES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generating template-baselines.json

For small project template changes, you may be able to edit the `template-baselines.json` file manually. This is a good way to ensure you have correct expectations about the effects of your changes.

For larger changes such as adding entirely new templates, it may be impractical to type out the changes to `template-baselines.json` manually. In those cases you can follow a procedure like the following.

1. Ensure you've configured the necessary environment variables:
- `set PATH=c:\git\dotnet\aspnetcore\.dotnet\;%PATH%` (update path as needed)
- `set DOTNET_ROOT=c:\git\dotnet\aspnetcore\.dotnet` (update path as needed)
2. Get to a position where you can execute the modified template(s) locally, i.e.:
- Use `dotnet pack ProjectTemplatesNoDeps.slnf` (possibly with `--no-restore --no-dependencies`) to regenerate `Microsoft.DotNet.Web.ProjectTemplates.*.nupkg`
- Run one of the `scripts/*.ps1` scripts to install your template pack and execute your chosen template. For example, run `powershell .\scripts\Run-BlazorWeb-Locally.ps1`
- Once that has run, you should see your updated template listed when you execute `dotnet new list` or `dotnet new YourTemplateName --help`. At the point you can run `dotnet new YourTemplateName -o SomePath` directly if you want. However each time you edit template sources further, you will need to run `dotnet new uninstall Microsoft.DotNet.Web.ProjectTemplates.8.0` and then go back to the start of this whole step.
- Tip: the following command combines the above steps, to go directly from editing template sources to an updated local project output: `dotnet pack ProjectTemplatesNoDeps.slnf --no-restore --no-dependencies && dotnet new uninstall Microsoft.DotNet.Web.ProjectTemplates.8.0 && rm -rf scripts\MyBlazorApp && powershell .\scripts\Run-BlazorWeb-Locally.ps1`
3. After generating a particular project's output, the following can be run in a Bash prompt (e.g., using WSL):
- `cd src/ProjectTemplates/scripts`
- `export PROJECT_NAME=MyBlazorApp` (update as necessary - note this is the name of the directly under `scripts` containing your project output)
- `find $PROJECT_NAME -type f -not -path "*/obj/*" -not -path "*/bin/*" -not -path "*/.publish/*" | sed -e "s/^$PROJECT_NAME\///" | sed -e "s/$PROJECT_NAME/{ProjectName}/g" | sed 's/.*/ "&",/' | sort -f`
- This will emit the JSON-formatted lines you can manually insert into the relevant place inside `template-baselines.json`
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@
"condition": "(UseWebAssembly && InteractiveAtRoot)",
"rename": {
"BlazorWeb-CSharp/Components/Layout/": "./BlazorWeb-CSharp.Client/Layout/",
"BlazorWeb-CSharp/Components/Pages/": "./BlazorWeb-CSharp.Client/Pages/",
"BlazorWeb-CSharp/Components/Pages/Home.razor": "./BlazorWeb-CSharp.Client/Pages/Home.razor",
"BlazorWeb-CSharp/Components/Pages/Home.razor.css": "./BlazorWeb-CSharp.Client/Pages/Home.razor.css",
"BlazorWeb-CSharp/Components/Pages/Weather.razor": "./BlazorWeb-CSharp.Client/Pages/Weather.razor",
"BlazorWeb-CSharp/Components/Pages/Weather.razor.css": "./BlazorWeb-CSharp.Client/Pages/Weather.razor.css",
"BlazorWeb-CSharp/Components/Pages/Counter.razor": "./BlazorWeb-CSharp.Client/Pages/Counter.razor",
"BlazorWeb-CSharp/Components/Pages/Counter.razor.css": "./BlazorWeb-CSharp.Client/Pages/Counter.razor.css",
"BlazorWeb-CSharp/Components/Routes.razor": "./BlazorWeb-CSharp.Client/Routes.razor"
}
},
Expand Down Expand Up @@ -113,12 +118,8 @@
{
"condition": "(!IndividualLocalAuth)",
"exclude": [
"BlazorWeb-CSharp/Components/Identity/**",
"BlazorWeb-CSharp/Components/Layout/ManageLayout.razor",
"BlazorWeb-CSharp/Components/Layout/ManageNavMenu.razor",
"BlazorWeb-CSharp/Components/Pages/Account/**",
"BlazorWeb-CSharp/Components/Account/**",
"BlazorWeb-CSharp/Data/**",
"BlazorWeb-CSharp/Identity/**",
"BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs",
"BlazorWeb-CSharp.Client/UserInfo.cs",
"BlazorWeb-CSharp.Client/Pages/Auth.razor"
Expand All @@ -127,7 +128,7 @@
{
"condition": "(!(IndividualLocalAuth && !UseLocalDB))",
"exclude": [
"BlazorWeb-CSharp/app.db"
"BlazorWeb-CSharp/Data/app.db"
]
},
{
Expand All @@ -139,19 +140,19 @@
{
"condition": "(!(IndividualLocalAuth && UseServer && UseWebAssembly))",
"exclude": [
"BlazorWeb-CSharp/Identity/PersistingRevalidatingAuthenticationStateProvider.cs"
"BlazorWeb-CSharp/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs"
]
},
{
"condition": "(!(IndividualLocalAuth && UseServer && !UseWebAssembly))",
"exclude": [
"BlazorWeb-CSharp/Identity/IdentityRevalidatingAuthenticationStateProvider.cs"
"BlazorWeb-CSharp/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs"
]
},
{
"condition": "(!(IndividualLocalAuth && !UseServer && UseWebAssembly))",
"exclude": [
"BlazorWeb-CSharp/Identity/PersistingServerAuthenticationStateProvider.cs"
"BlazorWeb-CSharp/Components/Account/PersistingServerAuthenticationStateProvider.cs"
]
},
{
Expand Down Expand Up @@ -189,6 +190,12 @@
"exclude": [
"BlazorWeb-CSharp/Data/SqlServer/**"
]
},
{
"condition": "(IndividualLocalAuth && UseWebAssembly)",
"rename": {
"BlazorWeb-CSharp/Components/Account/Shared/RedirectToLogin.razor": "BlazorWeb-CSharp.Client/RedirectToLogin.razor"
}
}
]
}
Expand Down Expand Up @@ -349,7 +356,7 @@
"defaultValue": "InteractivePerPage",
"displayName": "_Interactivity location",
"description": "Chooses which components will have interactive rendering enabled",
"isEnabled": "(InteractivityPlatform != \"None\" && auth == \"None\")",
"isEnabled": "(InteractivityPlatform != \"None\")",
"choices": [
{
"choice": "InteractivePerPage",
Expand Down Expand Up @@ -413,7 +420,7 @@
"AllInteractive": {
"type": "parameter",
"datatype": "bool",
"isEnabled": "(InteractivityPlatform != \"None\" && auth == \"None\")",
"isEnabled": "(InteractivityPlatform != \"None\")",
"defaultValue": "false",
"displayName": "_Enable interactive rendering globally throughout the site",
"description": "Configures whether to make every page interactive by applying an interactive render mode at the top level. If false, pages will use static server rendering by default, and can be marked interactive on a per-page or per-component basis."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@

namespace BlazorWeb_CSharp.Client;

// This is a client-side AuthenticationStateProvider that determines the user's authentication state by
// looking for data persisted in the page when it was rendered on the server. This authentication state will
// be fixed for the lifetime of the WebAssembly application. So, if the user needs to log in or out, a full
// page reload is required.
//
// This only provides a user name and email for display purposes. It does not actually include any tokens
// that authenticate to the server when making subsequent requests. That works separately using a
// cookie that will be included on HttpClient requests to the server.
public class PersistentAuthenticationStateProvider(PersistentComponentState persistentState) : AuthenticationStateProvider
{
private static readonly Task<AuthenticationState> _unauthenticatedTask =
private static readonly Task<AuthenticationState> unauthenticatedTask =
Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));

public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
if (!persistentState.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
{
return _unauthenticatedTask;
return unauthenticatedTask;
}

Claim[] claims = [
Expand All @@ -26,4 +34,3 @@ public override Task<AuthenticationState> GetAuthenticationStateAsync()
authenticationType: nameof(PersistentAuthenticationStateProvider)))));
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace BlazorWeb_CSharp.Client;

// Add properties to this class and update the server and client AuthenticationStateProviders
// to expose more information about the authenticated user to the client.
public class UserInfo
{
public required string UserId { get; set; }
Expand Down
Loading