Since #1387, GET /identity/profile publishes the user's ConcurrencyStamp as the ETag, and PUT /identity/profile answers 412 when If-Match does not match it. The stamp is not a profile version, though: ASP.NET Identity rotates it on every write to the AspNetUsers row, and some of those writes have nothing to do with the profile.
What rotates it
Measured against the Docker Compose stack built from main plus the open i18n PRs (which don't touch sign-in or the stamp), reading the ETag between steps:
| Step |
ETag changed |
| Two reads, nothing in between |
no |
| One sign-in with a wrong password |
yes |
| Token refresh |
no |
| A clean successful sign-in |
no |
| A successful sign-in right after a failed one |
yes (it resets AccessFailedCount) |
A failed sign-in goes through UserManager.AccessFailedAsync, which increments AccessFailedCount and saves the user, and the save rotates the stamp.
Effect
A user has Settings > Profile open. Anyone types a wrong password for that account: a bot, a colleague, or the user themselves on another device. The user presses Save, gets 412, and sees "Profile changed elsewhere", but nothing in the profile changed. The dashboard handles 412 correctly by re-reading and asking for a deliberate re-save, so no data is lost. The false conflict is still wrong, and anyone who can guess the email can cause it on demand.
Options
- Derive the ETag from the profile fields the PUT actually writes (first name, last name, phone, image, and locale once i18n lands), e.g. a hash, instead of from
ConcurrencyStamp. Writes that don't touch the profile then leave the tag alone. The precondition check would compare that hash, and the store's own ConcurrencyStamp check still catches a lost race at save time.
- Keep
ConcurrencyStamp and document that security-counter writes cause a spurious 412. This is the smallest change, but the behaviour stays surprising.
I'd go with option 1. Happy to send the PR if you agree.
Since #1387,
GET /identity/profilepublishes the user'sConcurrencyStampas the ETag, andPUT /identity/profileanswers 412 whenIf-Matchdoes not match it. The stamp is not a profile version, though: ASP.NET Identity rotates it on every write to theAspNetUsersrow, and some of those writes have nothing to do with the profile.What rotates it
Measured against the Docker Compose stack built from
mainplus the open i18n PRs (which don't touch sign-in or the stamp), reading the ETag between steps:AccessFailedCount)A failed sign-in goes through
UserManager.AccessFailedAsync, which incrementsAccessFailedCountand saves the user, and the save rotates the stamp.Effect
A user has Settings > Profile open. Anyone types a wrong password for that account: a bot, a colleague, or the user themselves on another device. The user presses Save, gets 412, and sees "Profile changed elsewhere", but nothing in the profile changed. The dashboard handles 412 correctly by re-reading and asking for a deliberate re-save, so no data is lost. The false conflict is still wrong, and anyone who can guess the email can cause it on demand.
Options
ConcurrencyStamp. Writes that don't touch the profile then leave the tag alone. The precondition check would compare that hash, and the store's ownConcurrencyStampcheck still catches a lost race at save time.ConcurrencyStampand document that security-counter writes cause a spurious 412. This is the smallest change, but the behaviour stays surprising.I'd go with option 1. Happy to send the PR if you agree.