Skip to content
Introducing the next era of Duende IdentityServer. Read our CEO’s announcement

User Management Operations

User Management exposes two service interfaces for managing user accounts: IUserSelfService for operations users perform on their own accounts, and IUserAdmin for administrative operations. Both interfaces work with strongly-typed value objects and return bool results to indicate success or failure.

IUserSelfService is what you inject when a user needs to manage their own account. It handles deregistration directly and exposes profile and authenticator operations as sub-service properties, so you don’t need to inject each one separately.

public interface IUserSelfService
{
Task<bool> TryDeleteAsync(UserSubjectId subjectId, Ct ct);
IUserProfileSelfService Profiles { get; }
IUserAuthenticatorsSelfService Authenticators { get; }
}
  • TryDeleteAsync: Permanently removes the user identified by subjectId and all associated data. Returns false if the user does not exist.
  • Profiles: Provides access to IUserProfileSelfService for reading and updating the user’s profile.
  • Authenticators: Provides access to IUserAuthenticatorsSelfService for managing OTP, TOTP, passkeys, passwords, and recovery codes.
// Deregister the user
var deregistered = await userSelfService.TryDeleteAsync(subjectId, ct);
// Access sub-services through the parent interface
var profile = await userSelfService.Profiles.TryGetAsync(subjectId, ct);
var authenticators = await userSelfService.Authenticators.TryGetAsync(subjectId, ct);

IUserAdmin is the administrative counterpart. Inject it into admin interfaces or background jobs that manage users on their behalf.

public interface IUserAdmin
{
Task<bool> TryRemoveAsync(UserSubjectId subjectId, Ct ct);
IMembershipAdmin Membership { get; }
IUserProfileAdmin Profiles { get; }
IUserAuthenticatorsAdmin Authenticators { get; }
}
  • TryRemoveAsync: Permanently removes the user identified by subjectId and all associated data. Returns false if the user does not exist.
  • Membership: Provides access to IMembershipAdmin for role and group assignment.
  • Profiles: Provides access to IUserProfileAdmin for reading, creating, and querying profiles.
  • Authenticators: Provides access to IUserAuthenticatorsAdmin for managing authenticators on behalf of users.

IUserAdmin and IUserSelfService expose similar capabilities. The difference is who the actor is: admin code managing users on their behalf vs. users managing their own accounts. Apply appropriate authorization to each in your application.

UserAuthenticators is a read-only snapshot of all authenticators registered for a user. It is returned by the Authenticators sub-service on IUserSelfService and IUserAdmin (i.e., IUserAuthenticatorsSelfService and IUserAuthenticatorsAdmin).

public sealed record UserAuthenticators
{
public UserSubjectId SubjectId { get; }
public IReadOnlyCollection<OtpAddress> OtpAddresses { get; }
public IReadOnlyCollection<ExternalAuthenticatorAddress> ExternalAuthenticatorAddresses { get; }
public IReadOnlyCollection<TotpDeviceName> TotpDeviceNames { get; }
public IReadOnlyCollection<UserPasskey> Passkeys { get; }
public int RecoveryCodeCount { get; }
public bool HasPassword { get; }
}
PropertyTypeDescription
SubjectIdUserSubjectIdThe unique identifier of the user
OtpAddressesIReadOnlyCollection<OtpAddress>Email addresses and phone numbers registered for One-Time Password (OTP) delivery
ExternalAuthenticatorAddressesIReadOnlyCollection<ExternalAuthenticatorAddress>External identity providers linked to this account (for example, Google, GitHub)
TotpDeviceNamesIReadOnlyCollection<TotpDeviceName>Names of registered Time-Based One-Time Password (TOTP) authenticators; a non-empty collection indicates two-factor authentication is enabled
PasskeysIReadOnlyCollection<UserPasskey>Registered passkeys, each with a credential ID, display name, and creation timestamp
RecoveryCodeCountintNumber of unused recovery codes remaining
HasPasswordboolWhether the user has a password set
var authenticators = await userAuthenticatorsSelfService.TryGetAsync(subjectId, ct);
if (authenticators is not null)
{
// Check whether two-factor authentication is enabled
var hasTwoFactor = authenticators.TotpDeviceNames.Count > 0
|| authenticators.Passkeys.Count > 0;
// Check remaining recovery codes
if (authenticators.RecoveryCodeCount < 3)
{
// Prompt user to regenerate recovery codes
}
}

User Management uses strongly-typed value objects for all identifiers. These types prevent mixing up different kinds of identifiers at compile time and enforce format constraints at parse time.

The unique identifier for a user. Stored as a string value (maximum 200 characters), compliant with RFC 9493.

// Create from an existing string identifier
var subjectId = UserSubjectId.Create("some-existing-id");
// Generate a new unique identifier (creates a GUID string internally)
var newId = UserSubjectId.New();
// Access the underlying string value
string value = subjectId.Value;

A combination of an OtpChannel (email or phone) and a SubjectId (the address itself). Represents a delivery channel for one-time passwords.

// Construct from a channel and address
var emailAddress = EmailAddress.Create("jane@example.com");
var otpAddress = new OtpAddress(OtpChannel.Email, emailAddress);
var phoneNumber = PhoneNumber.Create("+12025550100");
var otpPhone = new OtpAddress(OtpChannel.Sms, phoneNumber);

A validated email address. Whitespace is trimmed automatically. Minimum length is 3 characters; maximum length is 320 characters.

// Create: throws FormatException on invalid input
var email = EmailAddress.Create("jane@example.com");
// TryCreate: returns false on invalid input
if (EmailAddress.TryCreate("jane@example.com", out var result))
{
// result is valid here
}

A validated phone number. Leading + and 0 characters are stripped, whitespace is removed, and only digit characters are accepted. Maximum length is 15 digits (per ITU-T E.164).

// Create: throws FormatException on invalid input
var phone = PhoneNumber.Create("+12025550100");
// TryCreate: returns false on invalid input
if (PhoneNumber.TryCreate("+12025550100", out var result))
{
// result is valid here
}

The name of an external identity provider (for example, "Google" or "GitHub"). Whitespace is trimmed automatically. Maximum length is 255 characters.

// Create: throws FormatException on invalid input
var name = ExternalAuthenticatorName.Create("Google");
// TryCreate: returns false on invalid input
if (ExternalAuthenticatorName.TryCreate("Google", out var result))
{
// result is valid here
}

An opaque string identifier, used as the subject ID issued by an external identity provider. Whitespace is trimmed automatically. Maximum length is 255 characters.

// Create: throws FormatException on invalid input
var id = OpaqueSubjectId.Create("1234567890");
// TryCreate: returns false on invalid input
if (OpaqueSubjectId.TryCreate("1234567890", out var result))
{
// result is valid here
}

UserSubjectId and OpaqueSubjectId both implement the ISubjectId interface but are independent types. Use UserSubjectId when referring to users within User Management, and OpaqueSubjectId when working with external provider subject IDs.

Knowing what Duende maintains versus what you can customize helps you build integrations correctly and avoid reimplementing things that already exist.

These are implemented and maintained by Duende and updated with each release. You call these interfaces but don’t implement them:

  • IUserSelfService: lifecycle operations users perform on their own accounts (TryDeleteAsync), with sub-services for profiles (Profiles) and authenticators (Authenticators)
  • IUserAdmin: administrative lifecycle operations (TryRemoveAsync), with sub-services for membership (Membership), profiles (Profiles), and authenticators (Authenticators)
  • IUserAuthenticatorsSelfService / IUserAuthenticatorsAdmin: authenticator management (OTP addresses, TOTP, passkeys, recovery codes), accessible directly or via the parent interfaces
  • Core storage: the underlying user store, credential storage, and session state are internal to Duende and not designed for replacement or override
  • Authentication logic and lifecycle state machine: the rules governing registration, login, MFA enrollment, and deregistration are managed internally and are not extensible

You don’t need to implement any of these. Inject them where needed and call their methods.

These extension points are designed for you to implement or configure:

Extension pointInterfacePurpose
OTP deliveryIOtpDispatcherImplement to deliver one-time password codes via your preferred channel (email, SMS, push notification, etc.)
Password validationIPasswordValidatorImplement custom password strength or policy rules beyond the built-in defaults
Custom profile attributesIUserProfileSchemaAdminAdd application-specific attributes to the user profile schema

Register your implementation with the service provider at startup to override the default behavior.

The following are internal to Duende and not designed for override or extension:

  • Core user storage and the database schema backing it
  • The authentication and credential verification logic
  • The lifecycle state machine (registration flow, deregistration cascade, authenticator enrollment rules)

Attempting to replace these by intercepting internal services is unsupported and may break across releases.

The subject identifier value objects follow the RFC 9493 subject identifier specification. They all implement the ISubjectId interface:

  • OpaqueSubjectId: opaque string identifier (max 255 characters)
  • UserSubjectId: User Management user identifier (max 200 characters)
  • EmailAddress: validated email address (max 320 characters)
  • PhoneNumber: validated phone number in E.164 format (max 15 digits)

These are all record types. There is no inheritance between them: they are independent types that share the ISubjectId contract.