← Duende SkillsCONTENT HISTORY

Update to Duende Skills

Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Setting up Duende User Management with IdentityServer: passwordless authentication (OTP, TOTP, passkeys), storage configuration, user lifecycle, and migration from ASP.NET Identity.",
  "included_files": [],
  "name": "identityserver-usermanagement",
  "skill_md_contents": "---\nname: identityserver-usermanagement\ndescription: \"Setting up Duende User Management with IdentityServer: passwordless authentication (OTP, TOTP, passkeys), storage configuration, user lifecycle, and migration from ASP.NET Identity.\"\ninvocable: false\n---\n\n# User Management\n\n## When to Use This Skill\n\n- Adding user management to a Duende IdentityServer project\n- Setting up passwordless authentication (OTP, TOTP, passkeys)\n- Configuring storage providers (PostgreSQL, SQL Server, SQLite)\n- Integrating User Management with IdentityServer for claims and login/logout\n- Managing user profiles, roles, and groups\n- Migrating users from ASP.NET Identity\n\n## Core Principles\n\n- Duende User Management is **passwordless-first** — OTP email/SMS is the default flow\n- Requires `Duende.UserManagement.IdentityServer8` NuGet package + .NET 10\n- Storage is **document-based** (no EF migrations needed) — schema auto-creates at startup\n- Configuration goes **inside** `AddUserManagement()`, not at top level\n- Use `app.UseIdentityServer()` (not `UseAuthentication()` separately)\n\nDocs: https://docs.duendesoftware.com/identityserver/usermanagement\n\n## Setup\n\n### 1. Add Packages\n\n```bash\ndotnet add package Duende.IdentityServer\ndotnet add package Duende.UserManagement.IdentityServer8\ndotnet add package Duende.Storage.Sqlite  # or .PostgreSQL, .Mssql\n```\n\n### 2. Configure Program.cs\n\n```csharp\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddIdentityServer(options =>\n{\n    options.UserInteraction.LoginUrl = \"/Account/Login\";\n    options.UserInteraction.LogoutUrl = \"/Account/Logout\";\n})\n    .AddInMemoryClients(Config.Clients)\n    .AddInMemoryIdentityResources(Config.IdentityResources)\n    .AddUserManagement(options =>\n    {\n        // Storage (pick one)\n        options.AddSqliteStore(\"Data Source=users.db\");\n        // options.AddPostgreSqlStore(connectionString);\n        // options.AddSqlServerStore(connectionString);\n\n        // OTP delivery\n        options.UseSmtpOtpDispatcher(smtp =>\n            builder.Configuration.GetSection(\"Smtp\").Bind(smtp));\n    });\n\nvar app = builder.Build();\n\n// Auto-create database schema\nvar schema = app.Services.GetRequiredService<IDatabaseSchema>();\nawait schema.CreateIfNotExistsAsync();\n\napp.UseIdentityServer();\napp.MapRazorPages();\napp.Run();\n```\n\n### 3. OTP Dispatcher\n\n**Console (development):**\n```csharp\nbuilder.Services.AddSingleton<IOtpDispatcher, ConsoleOtpDispatcher>();\n```\n\n**SMTP (production):**\n```csharp\noptions.UseSmtpOtpDispatcher(x =>\n{\n    x.Host = \"smtp.example.com\";\n    x.Port = 587;\n    x.Username = \"noreply@example.com\";\n    x.Password = \"secret\";\n    x.FromAddress = \"noreply@example.com\";\n});\n```\n\n## Authentication Methods\n\n| Method | Description | Setup |\n|--------|-------------|-------|\n| **OTP** (default) | One-time codes via email/SMS | `IOtpDispatcher` implementation |\n| **TOTP** | Authenticator apps (RFC 6238) | Built-in, user enrollment required |\n| **Passkeys** | WebAuthn/FIDO2 phishing-resistant | Built-in, browser support required |\n| **Passwords** | Traditional username/password (PBKDF2) | Opt-in, not recommended as primary |\n| **External** | OAuth 2.0 / OIDC federated login | Standard ASP.NET Core auth handlers |\n| **Recovery codes** | Single-use backup codes | Auto-generated during 2FA setup |\n\n## IdentityServer Integration\n\n`AddUserManagement()` is called on the IdentityServer builder — it automatically:\n- Registers `IProfileService` for claims delivery\n- Handles login/logout flows\n- Maps user attributes to identity token claims\n\n### Claims Mapping\n\nUser profile attributes are mapped to claims based on requested scopes:\n- `openid` → `sub`\n- `profile` → `name`, `given_name`, `family_name`, etc.\n- `email` → `email`, `email_verified`\n\nCustom attributes are available through custom identity resources.\n\n## Storage\n\n| Provider | Package | Connection |\n|----------|---------|------------|\n| SQLite | `Duende.Storage.Sqlite` | `Data Source=users.db` |\n| PostgreSQL | `Duende.Storage.PostgreSQL` | Standard connection string |\n| SQL Server | `Duende.Storage.Mssql` | Standard connection string |\n| In-Memory | (built-in) | `Data Source=:memory:` (testing only) |\n\nStorage is document-based — no EF Core migrations needed. Call `IDatabaseSchema.CreateIfNotExistsAsync()` at startup to ensure schema exists.\n\n## User Lifecycle\n\n- **Creation**: Users are created on first authentication (passwordless) or via admin APIs\n- **Profiles**: Custom attributes stored as key-value pairs, organized in attribute groups\n- **Roles & Groups**: RBAC support with group membership and role inheritance\n- **Deletion**: Full user deletion with cascade\n\n## Migration from ASP.NET Identity\n\n```csharp\noptions.AddAspNetIdentityMigration(migrationOptions =>\n{\n    migrationOptions.ConnectionString = \"existing-aspnet-identity-db\";\n});\n```\n\nKey points:\n- Imports users, roles, and claims from existing ASP.NET Identity tables\n- Password hashes are preserved (users can still log in with existing passwords)\n- Migration runs once; subsequent runs skip already-imported users\n- After migration, users can enroll in passwordless methods\n\n## Common Anti-Patterns\n\n❌ Configuring storage outside `AddUserManagement()` — storage config must be inside the options lambda\n❌ Using `UseAuthentication()` instead of `UseIdentityServer()` — IdentityServer middleware handles auth\n❌ Skipping `CreateIfNotExistsAsync()` — database tables won't exist on first run\n❌ Using in-memory storage in production — data is lost on restart\n\n## Common Pitfalls\n\n1. **Storage configuration location**: `AddSqliteStore()`/`AddPostgreSqlStore()` must be called inside the `AddUserManagement(options => { })` lambda, not on the top-level builder.\n2. **.NET 10 required**: User Management requires .NET 10 SDK or later.\n3. **OTP dispatcher required**: Without an `IOtpDispatcher`, the default OTP flow cannot send codes. Register `ConsoleOtpDispatcher` for development.\n4. **LoginUrl/LogoutUrl**: Must be set in IdentityServer options to point to your account pages.\n5. **Schema creation**: Call `IDatabaseSchema.CreateIfNotExistsAsync()` before the app starts handling requests.\n\n## Related Skills\n\n- `identityserver-configuration` — IdentityServer host configuration and options\n- `identityserver-ui-flows` — Login/logout UI flows\n- `identityserver-upgrade-v7-to-v8` — Migration guide for v8 (includes User Management as new feature)\n- `aspnetcore-authentication` — ASP.NET Core authentication fundamentals\n"
}

SHA-256 of public snapshot: 3bb02ce6750f31ec857e0d984a5f6c32177e9e970fd52c698cfd1dec27fa27cb