externalUserService.createUser mutates the in-memory user array before the write to security.json is known to have succeeded, and does not undo the mutation if it fails:
// src/tokensecurity.ts
options.users.push(newUser)
return new Promise((resolve, reject) => {
saveSecurityConfig(app, options, (err) => {
if (err) {
reject(err) // options.users still holds newUser
} else {
resolve()
}
})
})
updateUser has the same shape — it assigns user.type and user.oidc in place, then saves.
On a full or read-only filesystem (not exotic on a Raspberry Pi with a failing SD card) the promise rejects and the caller reports failure, but the record stays in memory. That produces three follow-on effects:
- The record authenticates. A retry finds it via
findUserByProvider and login succeeds, even though no account exists on disk.
- It vanishes without warning.
setConfig, addUser, deleteUser and others reassign options from a fresh read of security.json, which does not contain the unsaved record. Any token already issued for it stops resolving, and the user starts getting 401s with no explanation.
- It does not survive restart, so the same account works or does not depending on what else has happened since.
saveSecurityConfig writes atomically via tmp+rename, so the file itself is never torn. The inconsistency is purely memory-versus-disk, and nothing reconciles the two until a config reload or a restart.
Persisting first and mutating second — or splicing the entry back out in the rejection branch — would keep the in-memory array consistent with what is actually stored.
Worth noting saveSecurityConfig also calls back with an error if the post-rename chmodSync fails, in which case the data did persist but the caller is told it did not. That inverts the same inconsistency.
externalUserService.createUsermutates the in-memory user array before the write tosecurity.jsonis known to have succeeded, and does not undo the mutation if it fails:updateUserhas the same shape — it assignsuser.typeanduser.oidcin place, then saves.On a full or read-only filesystem (not exotic on a Raspberry Pi with a failing SD card) the promise rejects and the caller reports failure, but the record stays in memory. That produces three follow-on effects:
findUserByProviderand login succeeds, even though no account exists on disk.setConfig,addUser,deleteUserand others reassignoptionsfrom a fresh read ofsecurity.json, which does not contain the unsaved record. Any token already issued for it stops resolving, and the user starts getting 401s with no explanation.saveSecurityConfigwrites atomically via tmp+rename, so the file itself is never torn. The inconsistency is purely memory-versus-disk, and nothing reconciles the two until a config reload or a restart.Persisting first and mutating second — or splicing the entry back out in the rejection branch — would keep the in-memory array consistent with what is actually stored.
Worth noting
saveSecurityConfigalso calls back with an error if the post-renamechmodSyncfails, in which case the data did persist but the caller is told it did not. That inverts the same inconsistency.