Skip to content

Commit 9b0676c

Browse files
committed
Require antiforgery in options endpoints
1 parent 109d53a commit 9b0676c

File tree

3 files changed

+49
-11
lines changed

3 files changed

+49
-11
lines changed

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Security.Claims;
22
using System.Text.Json;
3+
using Microsoft.AspNetCore.Antiforgery;
34
using Microsoft.AspNetCore.Authentication;
45
using Microsoft.AspNetCore.Components.Authorization;
56
using Microsoft.AspNetCore.Http.Extensions;
@@ -52,8 +53,11 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn
5253
accountGroup.MapPost("/PasskeyCreationOptions", async (
5354
HttpContext context,
5455
[FromServices] UserManager<ApplicationUser> userManager,
55-
[FromServices] SignInManager<ApplicationUser> signInManager) =>
56+
[FromServices] SignInManager<ApplicationUser> signInManager,
57+
[FromServices] IAntiforgery antiforgery) =>
5658
{
59+
await antiforgery.ValidateRequestAsync(context);
60+
5761
var user = await userManager.GetUserAsync(context.User);
5862
if (user is null)
5963
{
@@ -72,10 +76,14 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn
7276
});
7377

7478
accountGroup.MapPost("/PasskeyRequestOptions", async (
79+
HttpContext context,
7580
[FromServices] UserManager<ApplicationUser> userManager,
7681
[FromServices] SignInManager<ApplicationUser> signInManager,
82+
[FromServices] IAntiforgery antiforgery,
7783
[FromQuery] string? username) =>
7884
{
85+
await antiforgery.ValidateRequestAsync(context);
86+
7987
var user = string.IsNullOrEmpty(username) ? null : await userManager.FindByNameAsync(username);
8088
var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user);
8189
return TypedResults.Content(optionsJson, contentType: "application/json");

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
<button type="submit" name="__passkeySubmit" @attributes="AdditionalAttributes">@ChildContent</button>
2-
<passkey-submit operation="@Operation" name="@Name" email-name="@EmailName"></passkey-submit>
1+
@using Microsoft.AspNetCore.Antiforgery
2+
@inject IServiceProvider Services
3+
4+
<button type="submit" name="__passkeySubmit" @attributes="AdditionalAttributes">@ChildContent</button>
5+
<passkey-submit
6+
operation="@Operation"
7+
name="@Name"
8+
email-name="@EmailName"
9+
request-token-name="@tokens?.HeaderName"
10+
request-token-value="@tokens?.RequestToken">
11+
</passkey-submit>
312

413
@code {
14+
private AntiforgeryTokenSet? tokens;
15+
16+
[CascadingParameter]
17+
private HttpContext HttpContext { get; set; } = default!;
18+
519
[Parameter]
620
[EditorRequired]
721
public PasskeyOperation Operation { get; set; }
@@ -18,4 +32,9 @@
1832

1933
[Parameter(CaptureUnmatchedValues = true)]
2034
public IDictionary<string, object>? AdditionalAttributes { get; set; }
35+
36+
protected override void OnInitialized()
37+
{
38+
tokens = Services.GetRequiredService<IAntiforgery>()?.GetTokens(HttpContext);
39+
}
2140
}

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,21 @@ async function fetchWithErrorHandling(url, options = {}) {
1717
return response;
1818
}
1919

20-
async function createCredential(signal) {
20+
async function createCredential(headers, signal) {
2121
const optionsResponse = await fetchWithErrorHandling('/Account/PasskeyCreationOptions', {
2222
method: 'POST',
23+
headers,
2324
signal,
2425
});
2526
const optionsJson = await optionsResponse.json();
2627
const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
2728
return await navigator.credentials.create({ publicKey: options, signal });
2829
}
2930

30-
async function requestCredential(email, mediation, signal) {
31+
async function requestCredential(email, mediation, headers, signal) {
3132
const optionsResponse = await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${email}`, {
3233
method: 'POST',
34+
headers,
3335
signal,
3436
});
3537
const optionsJson = await optionsResponse.json();
@@ -46,6 +48,8 @@ customElements.define('passkey-submit', class extends HTMLElement {
4648
operation: this.getAttribute('operation'),
4749
name: this.getAttribute('name'),
4850
emailName: this.getAttribute('email-name'),
51+
requestTokenName: this.getAttribute('request-token-name'),
52+
requestTokenValue: this.getAttribute('request-token-value'),
4953
};
5054

5155
this.internals.form.addEventListener('submit', (event) => {
@@ -67,12 +71,16 @@ customElements.define('passkey-submit', class extends HTMLElement {
6771
throw new Error('Some passkey features are missing. Please update your browser.');
6872
}
6973

74+
const headers = {
75+
[this.attrs.requestTokenName]: this.attrs.requestTokenValue,
76+
};
77+
7078
if (this.attrs.operation === 'Create') {
71-
return await createCredential(signal);
79+
return await createCredential(headers, signal);
7280
} else if (this.attrs.operation === 'Request') {
7381
const email = new FormData(this.internals.form).get(this.attrs.emailName);
7482
const mediation = useConditionalMediation ? 'conditional' : undefined;
75-
return await requestCredential(email, mediation, signal);
83+
return await requestCredential(email, mediation, headers, signal);
7684
} else {
7785
throw new Error(`Unknown passkey operation '${this.attrs.operation}'.`);
7886
}
@@ -88,11 +96,14 @@ customElements.define('passkey-submit', class extends HTMLElement {
8896
const credentialJson = JSON.stringify(credential);
8997
formData.append(`${this.attrs.name}.CredentialJson`, credentialJson);
9098
} catch (error) {
99+
if (error.name === 'AbortError') {
100+
// The user explicitly canceled the operation - return without error.
101+
return;
102+
}
91103
console.error(error);
92-
if (useConditionalMediation || error.name === 'AbortError') {
93-
// We do not relay the error to the user if:
94-
// 1. We are attempting conditional mediation, meaning the user did not initiate the operation.
95-
// 2. The user explicitly canceled the operation.
104+
if (useConditionalMediation) {
105+
// An error occurred during conditional mediation, which is not user-initiated.
106+
// We log the error in the console but do not relay it to the user.
96107
return;
97108
}
98109
const errorMessage = error.name === 'NotAllowedError'

0 commit comments

Comments
 (0)