惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Last Week in AI
Last Week in AI
月光博客
月光博客
D
DataBreaches.Net
WordPress大学
WordPress大学
雷峰网
雷峰网
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
Recent Announcements
Recent Announcements
宝玉的分享
宝玉的分享
MyScale Blog
MyScale Blog
C
Check Point Blog
F
Fortinet All Blogs
B
Blog
小众软件
小众软件
Vercel News
Vercel News
罗磊的独立博客
有赞技术团队
有赞技术团队

Duende Software Official Site

WhatsApp One-Time Password (OTP) Login with Duende IdentityServer and User Management Planning a Successful Migration from IdentityServer3 to Duende IdentityServer Client Secrets, Mutual TLS and Private Key JWT, Oh My! How To Spell "Duende" Understanding .NET 11 Automatic CSRF Protection: A Guide for Identity Developers Security Lingo Explained: TOTP (Time-based One-Time Password) OAuth Identity Chaining, Transaction Tokens, and Human-in-the-Loop: Summer 2026 Identity Standards Recap What is Identity? - The Question Every Team Should Answer Before Writing Code Security Is a Spectrum: How to Choose Session Lifetimes in Duende IdentityServer Passkeys and WebAuthn with Duende IdentityServer and User Management Authenticating Players in Godot 4 with OAuth 2.0 and OpenID Connect Hardening OAuth in the newest 2026-07-28 MCP Release Candidate Unify Your SAML and OIDC Signing Keys with Automatic Rotation and Duende IdentityServer Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Stop AI Bots from Wasting Your Server How Duende IdentityServer Filters Claims (And Why It Matters) Core vs Extended Protocols in Duende IdentityServer v8: What You Get and When You Need More Your IdentityServer v8 Upgrade Checklist: A Quick Pre-Flight Guide Setting Up SAML Single Sign-On in ASP.NET with Duende IdentityServer Your Identity, Your Terms: Duende's Modular Identity Infrastructure and v8.x Release Duende Spring Launch '26: Identity Infrastructure That Expands With You SAML and OpenID Connect (OIDC): Coexistence, Not Competition The 9 Components of SAML You Need to Know, Ranked by Importance
Custom Passkey Attestation Policies: Restricting Login to...
Maarten Balliauw · 2026-08-25 · via Duende Software Official Site

A passkey on a YubiKey stays in tamper-resistant hardware. The private key never leaves the device. But a passkey synced through iCloud Keychain or Google Password Manager? That key material lives on every device linked to the account, backed up to a cloud service you don't control.

For regulated environments like finance, healthcare, or government, that difference matters because internal requirements or government regulations may add specific rules to what hardware keys your users are allowed to use.. Duende User Management lets you enforce the permitted key types at registration time.

Summary
• Synced passkeys (iCloud Keychain, Google Password Manager) store key material in the cloud; hardware keys keep it on-device.
• Duende User Management's IAttestationTrustPolicy interface lets you reject passkey registrations at the server before credentials are stored.
• Set AttestationConveyancePreference to "direct" so attestation data actually reaches your server.
• Build an AAGUID allowlist of approved hardware authenticator models and reject everything else.
• Move the allowlist to appsettings.json or another configuration store so your security team can update it without code deploys.

Platform Authenticators May Not Be Enough

As covered in our Introduction to Passkeys series, passkey authentication falls into two categories:

Platform authenticators (Windows Hello, iCloud Keychain, Google Password Manager) store credentials on the device or sync them across an ecosystem. Password managers like 1Password and Bitwarden do the same: they generate and sync passkeys across your devices, which is great for convenience but means your private key exists in multiple places. The risks:

  • Key material gets backed up to cloud services outside your control
  • A compromised cloud account (or password manager vault) exposes every synced passkey
  • No guarantee the authenticator is hardware-backed

Roaming authenticators (YubiKey, Google Titan, Feitian keys) are hardware tokens. The private key never leaves the secure element. You get:

  • Non-exportable keys
  • Physical presence required
  • Attestation certificates proving the authenticator's identity

If your organization's security policy requires credentials to live in hardware, you need to check attestation at registration and reject what doesn't qualify.

How Attestation and AAGUIDs Work

When a user registers a passkey, the browser calls navigator.credentials.create() and the authenticator generates a new key pair. The response that comes back to your server includes an attestation statement, which is the authenticator's way of proving what it is.

Buried in that attestation response is the AAGUID (Authenticator Attestation GUID): a 128-bit identifier the manufacturer assigns to each authenticator model. Think of it as a product SKU for security keys. A YubiKey 5 NFC has a different AAGUID than a YubiKey 5C, which has a different one from a Google Titan.

Some well-known hardware AAGUIDs:

Authenticator AAGUID

YubiKey 5 NFC

2fc0579f-8113-47ea-b116-bb5a8db9202a

YubiKey 5C

c1f9a0bc-1dd2-404a-b27f-8e29047a43fd

Google Titan (USB-A)

42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3

The FIDO Alliance publishes AAGUIDs through their Metadata Service (MDS). There's also a handy community-maintained AAGUID list with a visual explorer that covers both hardware keys and software providers like 1Password and Bitwarden.

By building an allowlist of AAGUIDs you trust, you can accept registrations from approved hardware keys and turn away everything else. One prerequisite: you need to set AttestationConveyancePreference to "direct" in your passkey configuration. Without that, browsers will strip the attestation data from the response before it reaches your server, and you'll have nothing to check.

Implementing IAttestationTrustPolicy

If you haven't set up User Management yet, the getting started guide walks you through it.

Duende User Management gives you a hook into the passkey registration flow through the IAttestationTrustPolicy interface. Your implementation gets called after the browser sends the attestation response but before the credential is stored in the database. This is where you decide: does this authenticator meet our requirements?

The AttestationTrustContext passed to your policy contains the AAGUID, the attestation format (like "packed", "tpm", or "none"), and the certificate chain if one was provided. For a hardware-only policy, the two things you care about are whether attestation was actually provided (the format isn't "none") and whether the AAGUID is on your approved list.

C#

using Duende.UserManagement.Authentication.Passkeys;

public class HardwareKeyOnlyPolicy : IAttestationTrustPolicy
{
    private static readonly HashSet<Guid> AllowedAaguids = new()
    {
        // YubiKey 5 Series
        Guid.Parse("2fc0579f-8113-47ea-b116-bb5a8db9202a"), // YubiKey 5 NFC
        Guid.Parse("c1f9a0bc-1dd2-404a-b27f-8e29047a43fd"), // YubiKey 5C
        Guid.Parse("cb69481e-8ff7-4039-93ec-0a2729a154a8"), // YubiKey 5Ci
        Guid.Parse("fa2b99dc-9e39-4257-8f92-4a30d23c4118"), // YubiKey 5 NFC FIPS
        
        // Google Titan
        Guid.Parse("42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3"), // Titan USB-A
        Guid.Parse("b93fd961-f2e6-462f-b122-82002247de78"), // Titan USB-C
    };

    public ValueTask<AttestationTrustPolicyResult> EvaluateAsync(
        AttestationTrustContext context,
        CancellationToken ct)
    {
        // No attestation means the authenticator didn't identify itself
        if (context.AttestationFormat == "none")
        {
            return ValueTask.FromResult(
                AttestationTrustPolicyResult.Reject(
                    "Direct attestation is required. Your authenticator did not provide identity proof."));
        }

        // Check the AAGUID against our allowlist
        if (!AllowedAaguids.Contains(context.Aaguid))
        {
            return ValueTask.FromResult(
                AttestationTrustPolicyResult.Reject(
                    "This authenticator model is not approved. Please use an approved hardware security key."));
        }

        return ValueTask.FromResult(AttestationTrustPolicyResult.Accept());
    }
}

The rejection messages matter. They'll end up in error responses to the client, so write them for the person holding the wrong key, not for a log file.

Registering the Policy

With the policy class in place, you need to tell Duende User Management to use it. The registration happens in Program.cs alongside the rest of your passkey configuration. There are two settings worth calling out beyond the policy itself:

  • AttestationConveyancePreference = "direct" tells the browser you want the raw attestation data, not a stripped-down version.
  • AuthenticatorAttachment = "cross-platform" filters the browser's registration UI so it only offers roaming authenticators (USB/NFC keys), not built-in platform authenticators like Windows Hello or Touch ID.

Together with the policy, this gives you two layers: the browser filters what the user sees, and the server validates what actually arrives.

C#

builder.Services
    .AddIdentityServer()
    .AddUserManagement(um => um
        .Authentication(auth =>
        {
            auth.AddAttestationTrustPolicy<HardwareKeyOnlyPolicy>();

            auth.Configure(options =>
            {
                // Request direct attestation from authenticators
                options.Passkeys.AttestationConveyancePreference = "direct";

                // Only allow cross-platform (roaming) authenticators
                options.Passkeys.AuthenticatorAttachment = "cross-platform";

                // ... other passkey settings (relying party, origins, etc.)
            });
        })
    );

Showing Useful Errors

When your policy rejects a registration, the user gets an error. Without some effort on your part, that error is going to be cryptic. You probably know the feeling from being on the receiving end of "An error occurred. Please try again."

Duende User Management ships a JavaScript helper that handles the WebAuthn browser calls for you. Include it from the /passkeys/js endpoint that User Management exposes, and then call registerPasskey from your own script, for example wired to a button click. Here's what that looks like on a Razor page or in a plain HTML view:

Html

<div id="status"></div>
<button id="register-key">Register security key</button>

<script src="/passkeys/js"></script>
<script>
    document.getElementById("register-key").addEventListener("click", () => {
        registerPasskey("My Security Key", {
            onStart() {
                document.getElementById("status").textContent = "Starting registration...";
            },
            onWaitingForAuthenticator() {
                document.getElementById("status").textContent = "Touch your security key...";
            },
            onSuccess(result) {
                document.getElementById("status").textContent = "Security key registered!";
            },
            onError(message) {
                const el = document.getElementById("status");
                if (message.includes("not approved")) {
                    el.innerHTML = `
                        <strong>This authenticator is not allowed.</strong><br>
                        Your organization requires a hardware security key (YubiKey or Google Titan).
                        <a href="/help/approved-keys">View approved devices</a>`;
                } else if (message.includes("attestation is required")) {
                    el.innerHTML = `
                        <strong>Authenticator identity could not be verified.</strong><br>
                        Please use a security key that supports attestation.`;
                } else {
                    el.textContent = "Registration failed: " + message;
                }
            }
        });
    });
</script>

Linking to an internal "approved devices" page is a nice touch, so your IT team can keep a list of supported authenticators.

Loading AAGUIDs from Configuration

The hardcoded AAGUID list in the example above works fine when your approved hardware is stable. But authenticator models change, new keys get certified, and your security team probably doesn't want to wait for a code deploy every time they approve a new device.

Moving the allowlist to appsettings.json keeps the policy code generic and lets you update the approved list through configuration alone. This is what your IAttestationTrustPolicy could look like:

C#

public class ConfigurableAttestationPolicy : IAttestationTrustPolicy
{
    private readonly HashSet<Guid> _allowedAaguids;

    public ConfigurableAttestationPolicy(IConfiguration configuration)
    {
        _allowedAaguids = configuration
            .GetSection("Passkeys:AllowedAaguids")
            .Get<string[]>()!
            .Select(Guid.Parse)
            .ToHashSet();
    }

    public ValueTask<AttestationTrustPolicyResult> EvaluateAsync(
        AttestationTrustContext context,
        CancellationToken ct)
    {
        if (context.AttestationFormat == "none")
        {
            return ValueTask.FromResult(
                AttestationTrustPolicyResult.Reject("Direct attestation is required."));
        }

        if (!_allowedAaguids.Contains(context.Aaguid))
        {
            return ValueTask.FromResult(
                AttestationTrustPolicyResult.Reject("Authenticator model not in allowlist."));
        }

        return ValueTask.FromResult(AttestationTrustPolicyResult.Accept());
    }
}

You can back it by adding entries to appsettings.json.

Json

{
  "Passkeys": {
    "AllowedAaguids": [
      "2fc0579f-8113-47ea-b116-bb5a8db9202a",
      "c1f9a0bc-1dd2-404a-b27f-8e29047a43fd",
      "42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3"
    ]
  }
}

You could take this further and load from a database or a remote configuration service if your deployment needs that, but appsettings.json covers most cases. Also keep in mind you likely don't want to make excessive I/O calls to validate the AAGUID, to make sure this doesn't become a denial-of-service opportunity for your identity provider.

Wrap-Up

The building blocks here are straightforward: request attestation, check the AAGUID against a list you control, and give users a clear message when their device doesn't make the cut. If you're in an environment where compliance requires hardware-bound credentials, this is a practical way to enforce it without making life harder for people who have the right key in their pocket. I'll end with a quick checklist for your implementation:

  1. Set AttestationConveyancePreference to "direct" so attestation data actually arrives
  2. Set AuthenticatorAttachment to "cross-platform" to filter the browser UI
  3. Reject "none" attestation to require identity proof
  4. Keep an AAGUID allowlist of approved models (the FIDO MDS and community AAGUID list are good starting points)
  5. Write error messages for humans, not log files

See the Duende User Management passkey documentation for the full API reference, and let us know in the comments what you're building with Duende!