Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion src/Identity/Core/src/PasskeyHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,10 @@ await VerifyClientDataAsync(
isBackupEligible: authenticatorData.IsBackupEligible,
isBackedUp: authenticatorData.IsBackedUp,
attestationObject: response.AttestationObject.ToArray(),
clientDataJson: response.ClientDataJSON.ToArray());
clientDataJson: response.ClientDataJSON.ToArray())
{
Aaguid = attestedCredentialData.Aaguid.ToArray(),
};

// 28. Process the client extension outputs in clientExtensionResults and the authenticator extension
// outputs in the extensions in authData as required by the Relying Party.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ internal static class IdentityUserPasskeyExtensions
{
public void UpdateFromUserPasskeyInfo(UserPasskeyInfo passkeyInfo)
{
// We only mutate properties that can be updated after passkey creation.
// See https://www.w3.org/TR/webauthn-3/#authn-ceremony-update-credential-record
passkey.Data.Name = passkeyInfo.Name;
passkey.Data.SignCount = passkeyInfo.SignCount;
passkey.Data.IsBackedUp = passkeyInfo.IsBackedUp;
Expand All @@ -29,7 +31,8 @@ public UserPasskeyInfo ToUserPasskeyInfo()
passkey.Data.AttestationObject,
passkey.Data.ClientDataJson)
{
Name = passkey.Data.Name
Name = passkey.Data.Name,
Aaguid = passkey.Data.Aaguid,
};
}
}
2 changes: 2 additions & 0 deletions src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Identity.UserPasskeyInfo.Aaguid.get -> byte[]?
Microsoft.AspNetCore.Identity.UserPasskeyInfo.Aaguid.set -> void
12 changes: 10 additions & 2 deletions src/Identity/Extensions.Core/src/UserPasskeyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ public sealed class UserPasskeyInfo
/// <param name="publicKey">The public key for the passkey.</param>
/// <param name="createdAt">The time when the passkey was created.</param>
/// <param name="signCount">The signature counter for the passkey.</param>
/// <param name="attestationObject">The passkey's attestation object.</param>
/// <param name="clientDataJson">The passkey's client data JSON.</param>
/// <param name="transports">The transports supported by this passkey.</param>
/// <param name="isUserVerified">Indicates if the passkey has a verified user.</param>
/// <param name="isBackupEligible">Indicates if the passkey is eligible for backup.</param>
/// <param name="isBackedUp">Indicates if the passkey is currently backed up.</param>
/// <param name="attestationObject">The passkey's attestation object.</param>
/// <param name="clientDataJson">The passkey's client data JSON.</param>
public UserPasskeyInfo(
byte[] credentialId,
byte[] publicKey,
Expand Down Expand Up @@ -110,4 +110,12 @@ public UserPasskeyInfo(
/// See <see href="https://www.w3.org/TR/webauthn-3/#dictdef-collectedclientdata"/>.
/// </remarks>
public byte[] ClientDataJson { get; }

/// <summary>
/// Gets or sets the AAGUID of the authenticator that created this passkey.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#aaguid"/>.
/// </remarks>
public byte[]? Aaguid { get; set; }
}
8 changes: 8 additions & 0 deletions src/Identity/Extensions.Stores/src/IdentityPasskeyData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,12 @@ public class IdentityPasskeyData
/// See <see href="https://www.w3.org/TR/webauthn-3/#dictdef-collectedclientdata"/>.
/// </remarks>
public virtual byte[] ClientDataJson { get; set; } = default!;

/// <summary>
/// Gets or sets the AAGUID of the authenticator that created this passkey.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#aaguid"/>.
/// </remarks>
public virtual byte[]? Aaguid { get; set; }
}
2 changes: 2 additions & 0 deletions src/Identity/Extensions.Stores/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
virtual Microsoft.AspNetCore.Identity.IdentityPasskeyData.Aaguid.get -> byte[]?
virtual Microsoft.AspNetCore.Identity.IdentityPasskeyData.Aaguid.set -> void
Original file line number Diff line number Diff line change
Expand Up @@ -819,5 +819,6 @@ private static void AssertPasskeysEqual(UserPasskeyInfo expected, UserPasskeyInf
Assert.Equal(expected.AttestationObject, actual.AttestationObject);
Assert.Equal(expected.ClientDataJson, actual.ClientDataJson);
Assert.Equal(expected.Transports, actual.Transports);
Assert.Equal(expected.Aaguid, actual.Aaguid);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ public Task RemovePasskeyAsync(TUser user, byte[] credentialId, CancellationToke
p.IsBackupEligible,
p.IsBackedUp,
p.AttestationObject,
p.ClientDataJson);
p.ClientDataJson)
{
Name = p.Name,
Aaguid = p.Aaguid,
};

[return: NotNullIfNotNull(nameof(p))]
private static PocoUserPasskey<string>? ToPocoUserPasskey(TUser user, UserPasskeyInfo? p)
Expand All @@ -131,6 +135,7 @@ public Task RemovePasskeyAsync(TUser user, byte[] credentialId, CancellationToke
IsBackedUp = p.IsBackedUp,
AttestationObject = p.AttestationObject,
ClientDataJson = p.ClientDataJson,
Aaguid = p.Aaguid,
};

public void Dispose()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public Task RemovePasskeyAsync(TUser user, byte[] credentialId, CancellationToke
p.ClientDataJson)
{
Name = p.Name,
Aaguid = p.Aaguid,
};

[return: NotNullIfNotNull(nameof(p))]
Expand All @@ -134,6 +135,7 @@ public Task RemovePasskeyAsync(TUser user, byte[] credentialId, CancellationToke
IsBackedUp = p.IsBackedUp,
AttestationObject = p.AttestationObject,
ClientDataJson = p.ClientDataJson,
Aaguid = p.Aaguid,
};

public void Dispose()
Expand Down
4 changes: 3 additions & 1 deletion src/Identity/test/InMemory.Test/InMemoryStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ private static UserPasskeyInfo ToUserPasskeyInfo(PocoUserPasskey<string> p)
p.AttestationObject,
p.ClientDataJson)
{
Name = p.Name
Name = p.Name,
Aaguid = p.Aaguid
};

private static PocoUserPasskey<string> ToPocoUserPasskey(TUser user, UserPasskeyInfo p)
Expand All @@ -233,6 +234,7 @@ private static PocoUserPasskey<string> ToPocoUserPasskey(TUser user, UserPasskey
IsBackedUp = p.IsBackedUp,
AttestationObject = p.AttestationObject,
ClientDataJson = p.ClientDataJson,
Aaguid = p.Aaguid,
};

public IQueryable<TRole> Roles
Expand Down
8 changes: 8 additions & 0 deletions src/Identity/test/Shared/PocoModel/PocoUserPasskey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,12 @@ public class PocoUserPasskey<TKey> where TKey : IEquatable<TKey>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dictdef-collectedclientdata"/>.
/// </remarks>
public virtual byte[] ClientDataJson { get; set; } = [];

/// <summary>
/// Gets or sets the AAGUID of the authenticator that created this passkey.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#aaguid"/>.
/// </remarks>
public virtual byte[] Aaguid { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
@foreach (var passkey in currentPasskeys)
{
<tr>
<td>@(passkey.Name ?? "Unnamed passkey")</td>
<td>
<div>@PasskeyAuthenticators.GetDisplayName(passkey)</div>
<small class="text-muted">Created @passkey.CreatedAt.UtcDateTime.ToString("MMM d, yyyy") (UTC)</small>
</td>
<td>
@{
var credentialId = Base64Url.EncodeToString(passkey.CredentialId);
Expand Down Expand Up @@ -123,14 +126,27 @@ else
return;
}

// If the authenticator is known, assign the inferred name
if (PasskeyAuthenticators.TryGetDefaultDisplayName(attestationResult.Passkey, out var defaultName))
{
attestationResult.Passkey.Name = defaultName;
}

var addPasskeyResult = await UserManager.AddOrUpdatePasskeyAsync(user, attestationResult.Passkey);
if (!addPasskeyResult.Succeeded)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: The passkey could not be added to your account.", HttpContext);
return;
}

// Immediately prompt the user to enter a name for the credential
// If a name was inferred, go back to the passkeys list
if (!string.IsNullOrEmpty(attestationResult.Passkey.Name))
{
RedirectManager.RedirectToCurrentPageWithStatus("Your passkey was added successfully.", HttpContext);
return;
}

// For unknown authenticators, prompt the user to enter a name
var credentialIdBase64Url = Base64Url.EncodeToString(attestationResult.Passkey.CredentialId);
RedirectManager.RedirectTo($"Account/Manage/RenamePasskey/{credentialIdBase64Url}");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Identity;

namespace BlazorWebCSharp._1.Components.Account;

// Maps passkey AAGUIDs to authenticator names.
// The AAGUID data is sourced from the community-maintained list at
// https://github.com/passkeydeveloper/passkey-authenticator-aaguids.
// You can add or modify authenticators as needed.
public static class PasskeyAuthenticators
{
private static readonly Dictionary<Guid, string> KnownAuthenticators = new()
{
// Google Password Manager
[new("ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4")] = "Google Password Manager",

// Apple iCloud Keychain
[new("fbfc3007-154e-4ecc-8c0b-6e020557d7bd")] = "iCloud Keychain",
[new("dd4ec289-e01d-41c9-bb89-70fa845d4bf2")] = "iCloud Keychain",

// Microsoft Windows Hello
[new("08987058-cadc-4b81-b6e1-30de50dcbe96")] = "Windows Hello",
[new("9ddd1817-af5a-4672-a2b9-3e3dd95000a9")] = "Windows Hello",
[new("6028b017-b1d4-4c02-b4b3-afcdafc96bb2")] = "Windows Hello",

// 1Password
[new("bada5566-a7aa-401f-bd96-45619a55120d")] = "1Password",
[new("b5397571-8af2-4d30-9d48-eeb8eee6e9c6")] = "1Password",

// Bitwarden
[new("d548826e-79b4-db40-a3d8-11116f7e8349")] = "Bitwarden",
[new("cc45f64e-52a2-451b-831a-4edd8022a202")] = "Bitwarden",
};
Comment thread
MackinnonBuck marked this conversation as resolved.

public static bool TryGetDefaultDisplayName(UserPasskeyInfo passkey, [NotNullWhen(true)] out string? defaultName)
{
if (passkey.Aaguid is { Length: 16 } aaguid &&
KnownAuthenticators.TryGetValue(new Guid(aaguid, bigEndian: true), out var name))
{
defaultName = name;
return true;
}

defaultName = null;
return false;
}

public static string GetDisplayName(UserPasskeyInfo passkey)
{
if (!string.IsNullOrEmpty(passkey.Name))
{
return passkey.Name;
}

if (TryGetDefaultDisplayName(passkey, out var name))
{
return name;
}

return "Unnamed passkey";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@
"Components/Account/Pages/ResetPassword.razor",
"Components/Account/Pages/ResetPasswordConfirmation.razor",
"Components/Account/Pages/_Imports.razor",
"Components/Account/PasskeyAuthenticators.cs",
"Components/Account/PasskeyInputModel.cs",
"Components/Account/PasskeyOperation.cs",
"Components/Account/Shared/ExternalLoginPicker.razor",
Expand Down Expand Up @@ -832,6 +833,7 @@
"Components/Account/Pages/ResetPassword.razor",
"Components/Account/Pages/ResetPasswordConfirmation.razor",
"Components/Account/Pages/_Imports.razor",
"Components/Account/PasskeyAuthenticators.cs",
"Components/Account/PasskeyInputModel.cs",
"Components/Account/PasskeyOperation.cs",
"Components/Account/Shared/ExternalLoginPicker.razor",
Expand Down Expand Up @@ -960,6 +962,7 @@
"Components/Account/Pages/ResetPassword.razor",
"Components/Account/Pages/ResetPasswordConfirmation.razor",
"Components/Account/Pages/_Imports.razor",
"Components/Account/PasskeyAuthenticators.cs",
"Components/Account/PasskeyInputModel.cs",
"Components/Account/PasskeyOperation.cs",
"Components/Account/Shared/ExternalLoginPicker.razor",
Expand Down Expand Up @@ -1172,6 +1175,7 @@
"{ProjectName}/Components/Account/Pages/ResetPassword.razor",
"{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor",
"{ProjectName}/Components/Account/Pages/_Imports.razor",
"{ProjectName}/Components/Account/PasskeyAuthenticators.cs",
"{ProjectName}/Components/Account/PasskeyInputModel.cs",
"{ProjectName}/Components/Account/PasskeyOperation.cs",
"{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor",
Expand Down Expand Up @@ -1383,6 +1387,7 @@
"{ProjectName}/Components/Account/Pages/ResetPassword.razor",
"{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor",
"{ProjectName}/Components/Account/Pages/_Imports.razor",
"{ProjectName}/Components/Account/PasskeyAuthenticators.cs",
"{ProjectName}/Components/Account/PasskeyInputModel.cs",
"{ProjectName}/Components/Account/PasskeyOperation.cs",
"{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor",
Expand Down Expand Up @@ -1842,6 +1847,7 @@
"{ProjectName}/Components/Account/Pages/ResetPassword.razor",
"{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor",
"{ProjectName}/Components/Account/Pages/_Imports.razor",
"{ProjectName}/Components/Account/PasskeyAuthenticators.cs",
"{ProjectName}/Components/Account/PasskeyInputModel.cs",
"{ProjectName}/Components/Account/PasskeyOperation.cs",
"{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor",
Expand Down Expand Up @@ -1918,6 +1924,7 @@
"Components/Account/Pages/ResetPassword.razor",
"Components/Account/Pages/ResetPasswordConfirmation.razor",
"Components/Account/Pages/_Imports.razor",
"Components/Account/PasskeyAuthenticators.cs",
"Components/Account/PasskeyInputModel.cs",
"Components/Account/PasskeyOperation.cs",
"Components/Account/Shared/ExternalLoginPicker.razor",
Expand Down Expand Up @@ -2063,6 +2070,7 @@
"{ProjectName}/Components/Account/Pages/ResetPassword.razor",
"{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor",
"{ProjectName}/Components/Account/Pages/_Imports.razor",
"{ProjectName}/Components/Account/PasskeyAuthenticators.cs",
"{ProjectName}/Components/Account/PasskeyInputModel.cs",
"{ProjectName}/Components/Account/PasskeyOperation.cs",
"{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor",
Expand Down Expand Up @@ -2198,6 +2206,7 @@
"{ProjectName}/Components/Account/Pages/ResetPassword.razor",
"{ProjectName}/Components/Account/Pages/ResetPasswordConfirmation.razor",
"{ProjectName}/Components/Account/Pages/_Imports.razor",
"{ProjectName}/Components/Account/PasskeyAuthenticators.cs",
"{ProjectName}/Components/Account/PasskeyInputModel.cs",
"{ProjectName}/Components/Account/PasskeyOperation.cs",
"{ProjectName}/Components/Account/Shared/ExternalLoginPicker.razor",
Expand Down
Loading