Step-by-step guide to setting up authentication and identity in a Pragmatic.Design application, from zero to working auth in both development and production.
Pragmatic.Identity supports a config-driven approach where the same codebase can run with development headers or production JWT, controlled by configuration.
Authorization is configured alongside authentication. The UseAuthorization() extension on IPragmaticBuilder lets you define roles, groups, and policies.
usingPragmatic.Authorization;
usingPragmatic.Authorization.Configuration;
await PragmaticApp.RunAsync(args, app=>
{
// ... authentication setup from Step 1 ...
app.UseAuthorization(authz=>
{
// Default policy: require authentication for all endpoints
// Endpoints require authentication by default (PragmaticEndpointsOptions.RequireAuthorizationByDefault);
// opt a single endpoint out with [AllowAnonymous].
// - SignInUser → the same check, then the access token (needs Pragmatic.Identity.Local.Jwt)
// - ChangePassword → changes password for the current user (requires the ChangePassword permission)
// - RequestPasswordReset → issues a reset token, delivered via notifier (does not reveal email existence)
// - ConfirmPasswordReset → validates token and sets new password
// - RequestEmailVerification → issues an email-verification token via notifier
// - ConfirmEmail → validates token and marks the email verified
These actions are exposed as endpoints by the source generator when the host references the Pragmatic.Identity.Local package. The route prefix defaults to identity/local.
Register a notifier for reset / verification tokens.RequestPasswordReset and
RequestEmailVerification never return the token in the response: they hand the plaintext to
IPasswordResetNotifier / IEmailVerificationNotifier for out-of-band delivery. The defaults
(LogOnlyPasswordResetNotifier, LogOnlyEmailVerificationNotifier) do not deliver anything and
only log a warning. Register a real email/SMS-backed implementation, otherwise users never receive
their tokens:
When the [PragmaticUser] entity owns a LocalIdentity (a property of that type, stored in the user’s
row) and the project references Pragmatic.Persistence.EFCore, the generator writes
{User}.LocalIdentityStore and registers it as ILocalIdentityStore. Its finders load the user,
tracked, and hand back its identity, so UpdateAsync saves what the actions changed; every save goes
through the boundary’s unit of work. The emails it receives are already in their stored form
(LocalIdentity.NormalizeEmail): the actions normalize before they ask.
Self-registration (RegisterUser) has to create a user, and what else a new user needs is the
application’s to say. The entity says it by implementing ISelfRegisteringUser<TUser>:
Without it the store’s CreateAsync throws NotSupportedException: the application provisions its
users itself (an administrator creates them), as Time off does.
A project that declares its own class implementing ILocalIdentityStore keeps it, and nothing is
generated.
With Pragmatic.Identity.Local.Jwt, the package’s SignInUser checks the credentials exactly as
LoginUser does and returns the token. UseJwtAuthentication registers the IAccessTokenIssuer it signs
with, and the token always carries the account’s key and its security stamp: without the stamp,
UseJwtAuthentication refuses a token by default (see The security stamp below).
Expose it from the module that imports the package:
[UsePackage<LocalIdentityPackage, AppBoundary>]
[ExposeEndpoint<SignInUser>(HttpVerb.Post, "sign-in", AllowAnonymous=true)] // POST identity/local/sign-in
publicsealedclassAppModule;
The answer is an AccessToken: { "token": "…", "expiresAt": "…" }.
What the token says about the user beyond the account (a display name, roles, a tenant, or a subject
other than the account’s key) is the application’s to say, with an IUserClaimsContributor:
A contributor adds to the token; the account’s key and the stamp are not its to change. A module that
has to sign a token itself depends on IAccessTokenIssuer, never on JwtTokenGenerator: the contract is
[ProvidedByHost], so the module’s generator does not report it as an unregistered dependency.
LocalIdentity.SecurityStamp is an opaque value the token carries as its sstamp claim. A password change
or reset rotates it, and every token issued before the rotation stops being accepted. It is how a local
sign-in revokes the sessions of a user whose password was compromised.
What UseJwtAuthentication does with it, on every request:
A token with a stamp is checked against the store. The identity is loaded from ILocalIdentityStore
by the external identity key the token carries (its pragmatic_eid claim, else its sub), and the token
is refused unless the identity is active and its current stamp equals the token’s. So an
ILocalIdentityStore has to be registered; without one, a stamped token is refused.
The subject can be a pseudonym.Generate(subject, …, externalIdentityKey: …) writes the two
separately: the account is found by the key, and ICurrentUser.Id (what ownership stamps and the audit
trail record as the actor) is the subject. Pass a reference rather than the key, and nothing that
records who acted records an email.
A token without a stamp is refused while JwtOptions.RequireSecurityStamp is true, which is the
default. A token that carries no stamp cannot be revoked, and would outlive the password change meant to
end it. This is the 401 a token minted without securityStamp: gets on every request.
RequireSecurityStamp = false is right in two cases, and only these:
A migration window: tokens minted before stamps existed are still in circulation and must keep
working until they expire. Turn it back on once every live token carries a stamp.
A host whose tokens come from an issuer that holds the credentials and owns their revocation: an
external identity provider, or a separate token service. This host stores no password, so it has no
stamp to rotate and no store to check one against. Revoking a session is the issuer’s job, and the
token’s lifetime is the bound on how long a revoked session keeps working. Keep that lifetime short.
// Case 2: the tokens are issued elsewhere, and revocation is the issuer's.
For production scenarios where role-permission mappings need to be managed at runtime (not just at compile time), add Pragmatic.Identity.Persistence. See Persistence for full details.
The middleware must run beforeUseAuthentication(). You must register it manually in your IStartupStep.ConfigurePipeline (e.g., app.UseMiddleware<Pragmatic.Identity.HeaderUserMiddleware>();).
ClaimsPrincipalUserAccessor resolves IUserAuthorization lazily via IServiceProvider to break the circular dependency: ICurrentUser -> CachedPermissionResolver -> ICurrentUser. This is handled internally; no user action is needed.