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.