Skip to content

Commit 3c4f3ec

Browse files
Add passkey count and name length limits (#62979)
1 parent 9a3d379 commit 3c4f3ec

File tree

10 files changed

+45
-16
lines changed

10 files changed

+45
-16
lines changed

src/Identity/Core/src/IdentityPasskeyOptions.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,16 @@ public class IdentityPasskeyOptions
6969
/// </para>
7070
/// <para>
7171
/// Possible values are "required", "preferred", and "discouraged".
72+
/// If set to <see langword="null"/>, the effective value is "preferred".
7273
/// </para>
7374
/// <para>
74-
/// If left <see langword="null"/>, the browser defaults to "preferred".
75+
/// The default value is "required".
7576
/// </para>
7677
/// <para>
7778
/// See <see href="https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement"/>.
7879
/// </para>
7980
/// </remarks>
80-
public string? UserVerificationRequirement { get; set; }
81+
public string? UserVerificationRequirement { get; set; } = "required";
8182

8283
/// <summary>
8384
/// Gets or sets the extent to which the server desires to create a client-side discoverable credential.
@@ -87,16 +88,17 @@ public class IdentityPasskeyOptions
8788
/// This option only applies when creating a new passkey, and is not enforced on the server.
8889
/// </para>
8990
/// <para>
90-
/// Possible values are "discouraged", "preferred", or "required".
91+
/// Possible values are "discouraged", "preferred", "required", or <see langword="null"/>.
92+
/// If set to <see langword="null"/>, the effective value is "discouraged".
9193
/// </para>
9294
/// <para>
93-
/// If left <see langword="null"/>, the browser defaults to "preferred".
95+
/// The default value is "preferred".
9496
/// </para>
9597
/// <para>
9698
/// See <see href="https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement"/>.
9799
/// </para>
98100
/// </remarks>
99-
public string? ResidentKeyRequirement { get; set; }
101+
public string? ResidentKeyRequirement { get; set; } = "preferred";
100102

101103
/// <summary>
102104
/// Gets or sets the attestation conveyance preference.

src/Identity/Extensions.Stores/src/IdentityPasskeyData.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5-
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Text;
8-
using System.Threading.Tasks;
95

106
namespace Microsoft.AspNetCore.Identity;
117

src/Identity/test/Identity.Test/IdentityPasskeyOptionsTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ public void VerifyDefaultOptions()
1212

1313
Assert.Equal(TimeSpan.FromMinutes(5), options.AuthenticatorTimeout);
1414
Assert.Equal(32, options.ChallengeSize);
15+
Assert.Equal("preferred", options.ResidentKeyRequirement);
16+
Assert.Equal("required", options.UserVerificationRequirement);
1517
Assert.Null(options.ServerDomain);
16-
Assert.Null(options.UserVerificationRequirement);
17-
Assert.Null(options.ResidentKeyRequirement);
1818
Assert.Null(options.AttestationConveyancePreference);
1919
Assert.Null(options.AuthenticatorAttachment);
2020
Assert.Null(options.IsAllowedAlgorithm);

src/Identity/test/Identity.Test/Passkeys/PasskeyHandlerAssertionTest.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,10 @@ private sealed class AssertionTest : PasskeyScenarioTest<PasskeyAssertionResult<
980980
{
981981
private static readonly byte[] _defaultCredentialId = [1, 2, 3, 4, 5, 6, 7, 8];
982982

983-
public IdentityPasskeyOptions PasskeyOptions { get; } = new();
983+
public IdentityPasskeyOptions PasskeyOptions { get; } = new()
984+
{
985+
UserVerificationRequirement = "preferred",
986+
};
984987
public string Origin { get; set; } = "https://example.com";
985988
public PocoUser User { get; set; } = new()
986989
{

src/Identity/test/Identity.Test/Passkeys/PasskeyHandlerAttestationTest.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,10 @@ private sealed class AttestationTest : PasskeyScenarioTest<PasskeyAttestationRes
978978
private static readonly byte[] _defaultAaguid = new byte[16];
979979
private static readonly byte[] _defaultAttestationStatement = [0xA0]; // Empty CBOR map
980980

981-
public IdentityPasskeyOptions PasskeyOptions { get; } = new();
981+
public IdentityPasskeyOptions PasskeyOptions { get; } = new()
982+
{
983+
UserVerificationRequirement = "preferred",
984+
};
982985
public string? UserId { get; set; } = "df0a3af4-bd65-440f-82bd-5b839e300dcd";
983986
public string? UserName { get; set; } = "johndoe";
984987
public string? UserDisplayName { get; set; } = "John Doe";

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,20 @@ else
4848

4949
<form @formname="add-passkey" @onsubmit="AddPasskey" method="post">
5050
<AntiforgeryToken />
51-
<PasskeySubmit Operation="PasskeyOperation.Create" Name="Input" class="btn btn-primary">Add a new passkey</PasskeySubmit>
51+
@if (currentPasskeys is { Count: >= MaxPasskeyCount })
52+
{
53+
<p class="text-danger">You have reached the maximum number of allowed passkeys. Please delete one before adding a new one.</p>
54+
}
55+
else
56+
{
57+
<PasskeySubmit Operation="PasskeyOperation.Create" Name="Input" class="btn btn-primary">Add a new passkey</PasskeySubmit>
58+
}
59+
5260
</form>
5361

5462
@code {
63+
private const int MaxPasskeyCount = 100;
64+
5565
private ApplicationUser? user;
5666
private IList<UserPasskeyInfo>? currentPasskeys;
5767

@@ -100,6 +110,12 @@ else
100110
return;
101111
}
102112

113+
if (currentPasskeys!.Count >= MaxPasskeyCount)
114+
{
115+
RedirectManager.RedirectToCurrentPageWithStatus($"Error: You have reached the maximum number of allowed passkeys.", HttpContext);
116+
return;
117+
}
118+
103119
var attestationResult = await SignInManager.PerformPasskeyAttestationAsync(Input.CredentialJson);
104120
if (!attestationResult.Succeeded)
105121
{

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
private sealed class InputModel
9090
{
9191
[Required]
92+
[StringLength(200, ErrorMessage = "Passkey names must be no longer than {1} characters.")]
9293
public string Name { get; set; } = "";
9394
}
9495
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@
3535

3636
protected override void OnInitialized()
3737
{
38-
tokens = Services.GetRequiredService<IAntiforgery>()?.GetTokens(HttpContext);
38+
tokens = Services.GetService<IAntiforgery>()?.GetTokens(HttpContext);
3939
}
4040
}

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//#if (UseLocalDB)
55
// "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-BlazorWebCSharp__1-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true"
66
//#else
7-
// "DefaultConnection": "DataSource=Data\\app.db;Cache=Shared"
7+
// "DefaultConnection": "DataSource=Data/app.db;Cache=Shared"
88
//#endif
99
// },
1010
////#endif

src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorTemplateTest.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,14 @@ await page.EvaluateAsync("""
233233
await page.ClickAsync("text=Add a new passkey");
234234

235235
await page.WaitForSelectorAsync("text=Enter a name for your passkey");
236+
237+
// First check that we can't register a passkey with a long name.
238+
var longName = new string('a', count: 201);
239+
await page.FillAsync("[name=\"Input.Name\"]", longName);
240+
await page.ClickAsync("text=Continue");
241+
await page.WaitForSelectorAsync("text=Passkey names must be no longer than 200 characters.");
242+
243+
// Now register a passkey with a valid name
236244
await page.FillAsync("[name=\"Input.Name\"]", "My passkey");
237245
await page.ClickAsync("text=Continue");
238246

0 commit comments

Comments
 (0)