You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Data Protection has no persisted key ring, so Identity's token providers break across restarts and replicas #794
Amended. The failure boundary below is overstated. An adversarial review of the #788 design
corrected it. ASP.NET Core Data Protection persists keys by default in an environment-dependent
location, and even an ephemeral ring survives across requests within one process. So "a second
replica breaks it" is not automatically true, and src/Cluckwork.Api/Dockerfile already
acknowledges default keys under the application user's home.
Still accurate and worth fixing. There is no explicit, durable, shared configuration. The
real risks are (a) container replacement losing an unmounted key ring, and (b) one instance being
unable to consume a token another instance protected, when no shared ring exists. Generation and
consumption are currently adjacent calls in the same operation, which is why nothing fails today.
Read the body below with that correction applied.
Found while evaluating what ASP.NET Core Identity offers for #788. This is independent of that work and is a latent defect today.
The gap
The repo has no Data Protection configuration anywhere. I checked for each of these and found none: AddDataProtection(), PersistKeysToDbContext, PersistKeysToFileSystem, IDataProtectionProvider/IDataProtector usage, and key-ring settings in any appsettings*.json.
Meanwhile, src/Cluckwork.Api/Hosting/CluckworkIdentityServiceCollectionExtensions.cs does register AddIdentityCore<ApplicationUser>()...AddDefaultTokenProviders(). AddDefaultTokenProviders() registers DataProtectorTokenProvider<TUser>, which uses IDataProtector. With no explicit configuration, ASP.NET Core falls back to a local, unpersisted key ring. That ring is ephemeral in a container, and it is per-process rather than shared.
Why it is latent rather than currently broken
IdentityProvider calls UserManager.GeneratePasswordResetTokenAsync today, so the app does exercise the data-protector path. It does not visibly break because ResetPasswordAsync consumes the generated token in the same request, in the Owner-driven "set a user's password" flow. The token never has to survive a restart, and a different process never has to validate it.
So this is a defect that needs a second condition to appear, not an outage. Two things would trigger it:
A real forgot-password flow. The app emails a token and redeems it minutes or hours later, possibly on a different replica. Tokens would fail validation after any restart or on any other instance.
Any future use of GenerateUserTokenAsync/VerifyUserTokenAsync.
It does not affect the OAuth work in #788 as designed. That design chose reference tokens, which are database lookups rather than protected payloads. OpenIddict does use Data Protection for some internal state, so fixing this first is still the safer order.
Fix
Configure a persisted, shared key ring. Two viable shapes:
Either way, keys should be encrypted at rest. On a host-agnostic deployment, that means a certificate or key supplied through config rather than a provider-specific KMS. This follows the repo's host-agnostic rule.
Not claimed
No user-visible bug exists today, and nothing is currently insecure. With an unpersisted key ring, tokens fail closed (validation fails), not open. The claim is narrower. A mechanism the app already registers cannot work across the restart or replica boundary that the project is deliberately moving toward, and two planned features (#320 and a real forgot-password flow) would expose it immediately.
Important
Amended. The failure boundary below is overstated. An adversarial review of the #788 design
corrected it. ASP.NET Core Data Protection persists keys by default in an environment-dependent
location, and even an ephemeral ring survives across requests within one process. So "a second
replica breaks it" is not automatically true, and
src/Cluckwork.Api/Dockerfilealreadyacknowledges default keys under the application user's home.
Still accurate and worth fixing. There is no explicit, durable, shared configuration. The
real risks are (a) container replacement losing an unmounted key ring, and (b) one instance being
unable to consume a token another instance protected, when no shared ring exists. Generation and
consumption are currently adjacent calls in the same operation, which is why nothing fails today.
Read the body below with that correction applied.
Found while evaluating what ASP.NET Core Identity offers for #788. This is independent of that work and is a latent defect today.
The gap
The repo has no Data Protection configuration anywhere. I checked for each of these and found none:
AddDataProtection(),PersistKeysToDbContext,PersistKeysToFileSystem,IDataProtectionProvider/IDataProtectorusage, and key-ring settings in anyappsettings*.json.Meanwhile,
src/Cluckwork.Api/Hosting/CluckworkIdentityServiceCollectionExtensions.csdoes registerAddIdentityCore<ApplicationUser>()...AddDefaultTokenProviders().AddDefaultTokenProviders()registersDataProtectorTokenProvider<TUser>, which usesIDataProtector. With no explicit configuration, ASP.NET Core falls back to a local, unpersisted key ring. That ring is ephemeral in a container, and it is per-process rather than shared.Why it is latent rather than currently broken
IdentityProvidercallsUserManager.GeneratePasswordResetTokenAsynctoday, so the app does exercise the data-protector path. It does not visibly break becauseResetPasswordAsyncconsumes the generated token in the same request, in the Owner-driven "set a user's password" flow. The token never has to survive a restart, and a different process never has to validate it.So this is a defect that needs a second condition to appear, not an outage. Two things would trigger it:
Why it matters beyond password reset
Anything using Identity's in-box token providers inherits this:
GenerateUserTokenAsync/VerifyUserTokenAsync.It does not affect the OAuth work in #788 as designed. That design chose reference tokens, which are database lookups rather than protected payloads. OpenIddict does use Data Protection for some internal state, so fixing this first is still the safer order.
Fix
Configure a persisted, shared key ring. Two viable shapes:
PersistKeysToDbContext<AppDbContext>()stores keys in Postgres, which every replica already shares and which is already backed up. It adds one table, so it needs one migration (feat(eggs): make cracked and dirty eggs sellable stock via condition grades (#396) #407-compatible) and a regenerateddocs/schema/(chore(schema): generate PostgreSQL schema documentation #417). This is the option I would default to. It adds no new infrastructure dependency, and operations already look after the storage.Either way, keys should be encrypted at rest. On a host-agnostic deployment, that means a certificate or key supplied through config rather than a provider-specific KMS. This follows the repo's host-agnostic rule.
Not claimed
No user-visible bug exists today, and nothing is currently insecure. With an unpersisted key ring, tokens fail closed (validation fails), not open. The claim is narrower. A mechanism the app already registers cannot work across the restart or replica boundary that the project is deliberately moving toward, and two planned features (#320 and a real forgot-password flow) would expose it immediately.