Table of Contents

ASP.NET Core Identity

New in 0.111.0, the package Duende ships as Duende.IdentityServer.AspNetIdentity: with it, ASP.NET Core Identity's users and cookie become this server's. The code on this page is included from samples/Server.AspNetIdentity, which a test boots, and which keeps its users in an in-memory IUserStore standing in for Entity Framework Core.

dotnet add package Fdyay.IdentityServer.AspNetIdentity
// ASP.NET Core Identity first: AddAspNetIdentity decorates the claims factory AddIdentity registers.
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddUserStore<InMemoryUserStore>()      // or AddEntityFrameworkStores<ApplicationDbContext>(), or any IUserStore
    .AddRoleStore<InMemoryRoleStore>();

var identityServer = builder.Services
    .AddIdentityServer(options => options.KeyManagement.Enabled = false)
    .AddInMemoryIdentityResources([new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email()])
    .AddInMemoryClients(
    [
        new Client
        {
            ClientId = "web",
            ClientSecrets = { new Secret("web-secret".Sha256()) },
            AllowedGrantTypes = { OidcConstants.GrantTypes.AuthorizationCode },
            RedirectUris = { "https://web.example.com/signin-oidc" },
            AllowedScopes = { "openid", "profile", "email" },
        },
    ])
    .AddAspNetIdentity<ApplicationUser>()

    // Identity's users, with this deployment's rule for who is still active.
    .AddProfileService<LockoutAwareProfileService>();

// The sign-in pages, which sign Identity's users in.
builder.Services.AddRazorPages().AddIdentityServerUi();

Call AddAspNetIdentity after AddIdentity, whose claims factory it decorates (called first, or twice, it refuses at startup in Duende's words). What it changes:

Claims Identity's claim types become sub, name, role, email. Its claims factory also adds preferred_username, email_verified, phone_number and phone_number_verified
Tokens and userinfo ProfileService<TUser> finds the user by sub and issues the requested claims; a user Identity cannot find is inactive. Derive from it and override IsUserActiveAsync for lockout and the like, as below
The session Where this server's idsrv cookie was the default scheme, Identity's Identity.Application cookie becomes it, and so becomes this server's session. sid, idp, amr and auth_time are added at sign-in, as for any sign-in to the session
Security stamp When Identity refreshes the principal from the security stamp, the session's own claims are carried onto the new one (ISessionClaimsFilter, replaceable), so a refreshed session is the same session
Cookies Identity's cookies are essential, and its application and external cookies SameSite=None, so a sign-in survives the cross-site redirects a protocol makes
Stored providers A dynamic provider's sign-out ends Identity's session, unless you set DynamicProviderOptions.SignOutScheme yourself
The shipped pages Since 0.112.0, the sign-in and external sign-in pages sign Identity's users in — see below

Duende's package also registers a resource owner password validator; the password grant is not one this server has.

Who is still active

/// <summary>
/// Identity's users and claims, as the package's profile service issues them, and one rule of the deployment's own:
/// a locked-out user's tokens and session stop working wherever the server asks whether the user is active.
/// </summary>
public sealed class LockoutAwareProfileService(
    UserManager<ApplicationUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory)
    : ProfileService<ApplicationUser>(userManager, claimsFactory)
{
    /// <inheritdoc />
    public override async Task<bool> IsUserActiveAsync(ApplicationUser user)
        => !await UserManager.IsLockedOutAsync(user);
}

ProfileService<TUser> answers active for every user Identity finds. Overriding IsUserActiveAsync puts a deployment's own rule — here, lockout — everywhere the server asks: at the token endpoint, for a signed-in user at the authorize endpoint, and wherever an access token is validated (the profile service). Register it after AddAspNetIdentity, which registers the default.

The shipped pages

Since 0.112.0, with AddAspNetIdentity called, the sign-in and external sign-in pages sign in through SignInManager<TUser>, and a host no longer needs AddTestUsers for them. Duende ships its pages as templates, one per kind of account; these are a package, so the choice is a service, IUserSignInService, which a host with accounts of another kind implements and registers:

Page With ASP.NET Core Identity
Sign-in PasswordSignInAsync with lockout on failure. A locked-out account, one Identity does not allow to sign in and a wrong password all read "Invalid username or password."; the UserLoginFailureEvent says which. A user who needs a second factor is refused — there is no second-factor page — and Identity's half-sign-in cookie is cleared
External sign-in The user is found by the provider's login, or provisioned as Duende's template provisions one: a generated username, the provider's email and name, and the login. A user Identity would not let sign in is refused, which the template does not do
Sign-out Unchanged: it ends the session, which is Identity's cookie

Since 0.175.0 the pages and AddAspNetIdentity can be registered in either order, and so can the pages and an IUserSignInService of your own. The pages' default — the test users — is registered by a factory, built only when it is the registration in force; until 0.175.0 it was registered by its constructor, so with the pages registered first it stayed in the container beside Identity's, and a host that validates its container at startup, as ASP.NET Core does in the Development environment, refused to start for want of an ITestUserStore. A host with the pages and no accounts at all now starts, and the first page that needs them says to call AddTestUsers, call AddAspNetIdentity or register an IUserSignInService.