Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
01104ac
Initial commit
MackinnonBuck May 27, 2025
8294e9e
Remove extra entry in Version.Details.xml
MackinnonBuck May 27, 2025
725a304
Fix Program.Main.cs
MackinnonBuck May 27, 2025
f86515f
Fix failing tests
MackinnonBuck May 27, 2025
f828930
Merge remote-tracking branch 'origin/main' into mbuck/passkeys
MackinnonBuck May 28, 2025
ee5e007
Add passkey sample app + E2E tests
MackinnonBuck Jun 3, 2025
e23320c
Correctly specify transports in generated credential options
MackinnonBuck Jun 3, 2025
32d3f5f
Merge branch 'main' into mbuck/passkeys
MackinnonBuck Jun 3, 2025
b07c980
Merge branch 'main' into mbuck/passkeys
MackinnonBuck Jun 5, 2025
5d3b864
Undo changes to Components.slnf
MackinnonBuck Jun 5, 2025
af27faf
Move core passkey implementation to Microsoft.AspNetCore.Identity
MackinnonBuck Jun 5, 2025
65e6a37
Update Passkeys.razor
MackinnonBuck Jun 6, 2025
bb187c7
PR feedback
MackinnonBuck Jun 6, 2025
d178068
Update template passkey functionality
MackinnonBuck Jun 6, 2025
331b5f6
PR feedback
MackinnonBuck Jun 9, 2025
beecc32
PR feedback
MackinnonBuck Jun 9, 2025
2636c5e
Remove PasskeyRequestContext
MackinnonBuck Jun 9, 2025
ce0f0b1
API cleanups
MackinnonBuck Jun 10, 2025
d2cb5f5
Merge branch 'main' into mbuck/passkeys
MackinnonBuck Jun 10, 2025
7f63102
Try to fix CI
MackinnonBuck Jun 10, 2025
4711627
Merge branch 'mbuck/passkeys' of https://github.com/dotnet/aspnetcore…
MackinnonBuck Jun 10, 2025
8ee6b19
Update Versions.props
MackinnonBuck Jun 10, 2025
e82ddcb
Update Directory.Build.targets.in
MackinnonBuck Jun 10, 2025
b114f44
Better exceptions + clean up JSON representation
MackinnonBuck Jun 10, 2025
c11365d
Merge branch 'main' into mbuck/passkeys
MackinnonBuck Jun 10, 2025
03d8a62
PR feedback
MackinnonBuck Jun 11, 2025
cabe3ee
PR feedback
MackinnonBuck Jun 11, 2025
75fa369
Merge branch 'main' into mbuck/passkeys
MackinnonBuck Jun 11, 2025
927596b
Update template
MackinnonBuck Jun 11, 2025
c232673
PR feedback
MackinnonBuck Jun 11, 2025
f5d78e2
PR feedback
MackinnonBuck Jun 11, 2025
e7ccd11
Update baselines
MackinnonBuck Jun 12, 2025
5a91bbe
Merge branch 'main' into mbuck/passkeys
MackinnonBuck Jun 12, 2025
ac1bd58
PR feedback + a few tests
MackinnonBuck Jun 12, 2025
9c23ecd
Merge branch 'mbuck/passkeys' of https://github.com/dotnet/aspnetcore…
MackinnonBuck Jun 12, 2025
231cc6f
Add BOM to template files
MackinnonBuck Jun 12, 2025
29c008d
Add passkey autofill
MackinnonBuck Jun 13, 2025
8f16a1a
Fix E2E tests
MackinnonBuck Jun 13, 2025
7aa5a11
Merge branch 'main' into mbuck/passkeys
MackinnonBuck Jun 18, 2025
4964087
Cleanups
MackinnonBuck Jun 18, 2025
130fa9b
Update SignInManager.cs
MackinnonBuck Jun 19, 2025
c528deb
Merge branch 'main' into mbuck/passkeys
MackinnonBuck Jun 25, 2025
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
Prev Previous commit
Next Next commit
Add passkey sample app + E2E tests
  • Loading branch information
MackinnonBuck committed Jun 3, 2025
commit ee5e0076657c6633715534ac9a266031ae46d561
1 change: 1 addition & 0 deletions AspNetCore.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@
<Project Path="src/Identity/samples/IdentitySample.DefaultUI/IdentitySample.DefaultUI.csproj" />
<Project Path="src/Identity/samples/IdentitySample.Mvc/IdentitySample.Mvc.csproj" />
<Project Path="src/Identity/samples/IdentitySample.PasskeyConformance/IdentitySample.PasskeyConformance.csproj" />
<Project Path="src/Identity/samples/IdentitySample.PasskeyUI/IdentitySample.PasskeyUI.csproj" />
</Folder>
<Folder Name="/src/Identity/Specification.Tests/" Id="f8dfe8a4-1d8c-9e84-e870-8ba24ebd08ff">
<Project Path="src/Identity/Specification.Tests/src/Microsoft.AspNetCore.Identity.Specification.Tests.csproj" />
Expand Down
4 changes: 2 additions & 2 deletions eng/tools/GenerateFiles/Directory.Build.targets.in
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,14 @@
</ItemGroup>

<!-- When building and running locally, manually resolve the just-built frameworks. On Helix, let the SDK resolve the packs itself (they're laid out on top of the .NET SDK in the work items) -->
<PropertyGroup Condition="$(UpdateAspNetCoreKnownFramework) and '$(HELIX_CORRELATION_PAYLOAD)' == ''">
<PropertyGroup Condition="'$(UpdateAspNetCoreKnownFramework)' == 'true' AND '$(HELIX_CORRELATION_PAYLOAD)' == ''">
<!-- Allow additional targeting and runtime packs to be downloaded only if required by a test. -->
<EnableTargetingPackDownload Condition="'$(TestRequiresTargetingPackDownload)' != 'true'">false</EnableTargetingPackDownload>
<EnableRuntimePackDownload Condition="'$(TestRequiresRuntimePackDownload)' != 'true'">false</EnableRuntimePackDownload>
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
</PropertyGroup>

<Target Name="ResolveLiveBuiltAspnetCoreKnownFramework" Condition="$(UpdateAspNetCoreKnownFramework) and '$(HELIX_CORRELATION_PAYLOAD)' == ''" AfterTargets="ResolveFrameworkReferences">
<Target Name="ResolveLiveBuiltAspnetCoreKnownFramework" Condition="'$(UpdateAspNetCoreKnownFramework)' == 'true' AND '$(HELIX_CORRELATION_PAYLOAD)' == ''" BeforeTargets="ProcessFrameworkReferences">
<Error Text="Requested Microsoft.AspNetCore.App v${MicrosoftAspNetCoreAppRefVersion} ref pack does not exist."
Condition="!Exists('$(TargetingPackLayoutRoot)packs\Microsoft.AspNetCore.App.Ref\${MicrosoftAspNetCoreAppRefVersion}\data\FrameworkList.xml') " />
<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Identity/Extensions.Core/src/UserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2229,7 +2229,7 @@ public virtual async Task<PasskeyCreationOptions> GeneratePasskeyCreationOptions
var challenge = GetRandomChallenge(Options.Passkey.ChallengeSize);
var options = new PublicKeyCredentialCreationOptions(rpEntity, userEntity, BufferSource.FromBytes(challenge))
{
Timeout = (uint)Options.Passkey.Timeout.Milliseconds,
Timeout = (uint)Options.Passkey.Timeout.TotalMilliseconds,
ExcludeCredentials = excludeCredentials,
PubKeyCredParams = PublicKeyCredentialParameters.AllSupportedParameters,
AuthenticatorSelection = creationArgs.AuthenticatorSelection,
Expand Down Expand Up @@ -2279,7 +2279,7 @@ public virtual async Task<PasskeyRequestOptions> GeneratePasskeyRequestOptionsAs
var options = new PublicKeyCredentialRequestOptions(BufferSource.FromBytes(challenge))
{
RpId = requestContext.Domain,
Timeout = (uint)Options.Passkey.Timeout.Milliseconds,
Timeout = (uint)Options.Passkey.Timeout.TotalMilliseconds,
AllowCredentials = allowCredentials,
};
if (requestArgs is not null)
Expand Down
1 change: 1 addition & 0 deletions src/Identity/Identity.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"src\\Identity\\samples\\IdentitySample.DefaultUI\\IdentitySample.DefaultUI.csproj",
"src\\Identity\\samples\\IdentitySample.Mvc\\IdentitySample.Mvc.csproj",
"src\\Identity\\samples\\IdentitySample.PasskeyConformance\\IdentitySample.PasskeyConformance.csproj",
"src\\Identity\\samples\\IdentitySample.PasskeyUI\\IdentitySample.PasskeyUI.csproj",
"src\\Identity\\test\\Identity.FunctionalTests\\Microsoft.AspNetCore.Identity.FunctionalTests.csproj",
"src\\Identity\\test\\Identity.Test\\Microsoft.AspNetCore.Identity.Test.csproj",
"src\\Identity\\test\\InMemory.Test\\Microsoft.AspNetCore.Identity.InMemory.Test.csproj",
Expand Down
20 changes: 20 additions & 0 deletions src/Identity/samples/IdentitySample.PasskeyUI/Components/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<ImportMap />
<HeadOutlet />
</head>

<body>
<Routes />
<script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="@Assets["app.js"]"></script>
</body>

</html>

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@page "/authenticated"

@using Microsoft.AspNetCore.Authorization

@attribute [Authorize]

<PageTitle>Authenticated</PageTitle>

<h1>You are authenticated!</h1>

<AuthorizeView>
<p>Hello, @context.User.Identity?.Name!</p>
</AuthorizeView>

<form id="logout-form" action="account/logout" method="post">
<AntiforgeryToken />
<button type="submit">Log out</button>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
@page "/"

@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Identity.Test

@inject NavigationManager NavigationManager
@inject SignInManager<PocoUser> SignInManager
@inject UserManager<PocoUser> UserManager
@inject IUserPasskeyStore<PocoUser> PasskeyStore

<h1>Welcome!</h1>

<h3>Log in or register here</h3>

<form id="auth-form" method="post" @formname="auth" @onsubmit="OnSubmitAsync">
<AntiforgeryToken />
<input type="text" id="input-username" name="Username" placeholder="username" autocomplete="username webauthn"/>
<hr />
<input type="hidden" id="input-credential" name="CredentialJson" />
<input type="hidden" id="input-action" name="Action" />
<button type="submit" id="input-register">Register</button>
<button type="submit" id="input-authenticate">Authenticate</button>
</form>

<p id="status-message">@statusMessage</p>

@code {
private string? statusMessage;

[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;

[SupplyParameterFromForm]
private string? Username { get; set; }

[SupplyParameterFromForm]
private string? CredentialJson { get; set; }

[SupplyParameterFromForm]
private string? Action { get; set; }

private Task OnSubmitAsync()
=> Action switch
{
"register" => RegisterAsync(),
"authenticate" => AuthenticateAsync(),
var x => throw new InvalidOperationException($"Unknown action '{x}'"),
};

private async Task RegisterAsync()
{
if (string.IsNullOrWhiteSpace(Username))
{
statusMessage = "Error: A username is required for registration.";
return;
}

if (string.IsNullOrWhiteSpace(CredentialJson))
{
statusMessage = "Error: No credential was submitted by the browser.";
return;
}

var options = await SignInManager.RetrievePasskeyCreationOptionsAsync();
if (options is null)
{
statusMessage = "Error: There are no original passkey options present.";
return;
}

var attestationResult = await UserManager.PerformPasskeyAttestationAsync(CredentialJson, options);
if (!attestationResult.Succeeded)
{
statusMessage = $"Error: Could not validate credential: {attestationResult.Failure.Message}";
return;
}

// Create the user if they don't exist yet.
var userEntity = options.UserEntity;
var user = await UserManager.FindByIdAsync(userEntity.Id);
if (user is null)
{
user = new PocoUser(userName: userEntity.Name)
{
Id = userEntity.Id,
};
var createUserResult = await UserManager.CreateAsync(user);
if (!createUserResult.Succeeded)
{
statusMessage = "Error: Could not create a new user.";
return;
}
}

await PasskeyStore.SetPasskeyAsync(user, attestationResult.Passkey, CancellationToken.None);
var updateResult = await UserManager.UpdateAsync(user);
if (!updateResult.Succeeded)
{
statusMessage = "Error: Could not update the user with the new passkey.";
return;
}

statusMessage = "Registration successful! You can now authenticate with your passkey.";
}

private async Task AuthenticateAsync()
{
if (string.IsNullOrWhiteSpace(CredentialJson))
{
statusMessage = "Error: No credential was submitted by the browser.";
return;
}

var options = await SignInManager.RetrievePasskeyRequestOptionsAsync();
if (options is null)
{
statusMessage = "Error: There are no original passkey options present.";
return;
}

var signInResult = await SignInManager.PasskeySignInAsync(CredentialJson, options);
if (!signInResult.Succeeded)
{
statusMessage = "Error: Could not sign in with the provided credential.";
return;
}

NavigationManager.NavigateTo("authenticated", forceLoad: true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@page "/not-found"

<h3>Not Found</h3>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@inject NavigationManager NavigationManager

@code {
protected override void OnInitialized()
{
NavigationManager.NavigateTo("/");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@inject NavigationManager NavigationManager

<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData">
<NotAuthorized>
<RedirectToHome />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<Description>Passkey conformance testing for ASP.NET Core Identity</Description>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<IsShippingPackage>false</IsShippingPackage>
<Nullable>enable</Nullable>
<NoWarn>$(TS6385);$(NoWarn)</NoWarn>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Components.Endpoints" />
<Reference Include="Microsoft.AspNetCore.Identity" />
<Reference Include="Microsoft.AspNetCore.HttpsPolicy" />
<Reference Include="Microsoft.AspNetCore.Identity" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
<Reference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
<Reference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<Reference Include="Microsoft.EntityFrameworkCore.Tools" />
<Reference Include="Microsoft.AspNetCore.HttpsPolicy" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.AspNetCore.StaticAssets" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(IdentityTestSharedSourceRoot)PocoModel\**\*.cs" LinkBase="Shared" />
</ItemGroup>

<!-- Workaround to add Blazor framework static assets without requiring a package reference -->
<PropertyGroup>
<UseBlazorFrameworkDebugAssets>true</UseBlazorFrameworkDebugAssets>
<BlazorFrameworkStaticWebAssetRoot Condition="'$(Configuration)' == 'Debug'">$(RepoRoot)src\Components\Web.JS\dist\Debug</BlazorFrameworkStaticWebAssetRoot>
<BlazorFrameworkStaticWebAssetRoot Condition="'$(Configuration)' == 'Release'">$(RepoRoot)src\Components\Web.JS\dist\Release</BlazorFrameworkStaticWebAssetRoot>
</PropertyGroup>

<Import Project="$(RepoRoot)\src\Assets\build\Microsoft.AspNetCore.App.Internal.Assets.targets" />

</Project>
Loading