What does PasswordEncoder.upgradeEncoding do, and how does Spring Security use it to migrate stored passwords transparently?
answer
- upgradeEncoding -> 'is this hash stale?'
- Delegating: id != idForEncode => true
- BCrypt: stored cost < configured strength
- rehash on successful login only
- needs UserDetailsPasswordService.updatePassword
basics
~20 supgradeEncoding(storedHash) returns true when a hash was made with an older algorithm or lower strength than the current default. After a successful login, DaoAuthenticationProvider checks it and, if true, re-hashes the password with the new settings via a UserDetailsPasswordService.
solid answer
~40 supgradeEncoding is a default method on PasswordEncoder that answers 'should this stored hash be re-encoded with stronger parameters?'. For DelegatingPasswordEncoder it returns true when the stored {id} isn't the current default encode id, otherwise it delegates to that encoder's own check; BCryptPasswordEncoder returns true when the stored cost is below the configured strength. The migration is opportunistic and lazy: DaoAuthenticationProvider, after a successful matches(), calls upgradeEncoding(oldHash); if true and a UserDetailsPasswordService bean exists, it re-encodes the plaintext (which it still has in memory from the login) and calls updatePassword() to persist the stronger hash. This lets you raise strength or switch from {bcrypt} to {argon2} and have credentials migrate silently as users log in — no forced reset, no plaintext ever stored. Users who never log in keep old hashes.
code
java · 18 lines// 1) Encoder now defaults to a higher cost (or argon2)
@Bean PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // old hashes are $2a$10$
}
// 2) Provide the service that persists the re-hash
@Service
class MyPasswordService implements UserDetailsPasswordService {
private final UserRepo repo;
MyPasswordService(UserRepo repo) { this.repo = repo; }
@Override public UserDetails updatePassword(UserDetails user, String newHash) {
repo.updateHash(user.getUsername(), newHash); // persist stronger hash
return User.withUserDetails(user).password(newHash).build();
}
}
// On next successful login, DaoAuthenticationProvider sees
// upgradeEncoding(oldHash)==true and calls updatePassword(...)go deeper
Know it flags stale hashes for re-encoding after login.
Explain the DaoAuthenticationProvider hook and that plaintext is only available at login.
Detail per-encoder logic (id mismatch, cost comparison) and the UserDetailsPasswordService requirement.
Design a full migration policy: rollout, users who never log in, auditing, and eventual forced-reset cutoff.
## The method ```java default boolean upgradeEncoding(String encodedPassword) { return false; } ``` It asks: *given this already-stored hash, would encoding the same password today produce something stronger?* A `true` answer means 'this credential is stale, re-hash it.' The base default is `false` (never upgrade). ## Per-encoder behavior - **DelegatingPasswordEncoder**: extracts the `{id}` of the stored hash. If the id differs from `idForEncode` (the current default algorithm), it returns `true` — the hash was made by a non-default (usually older) algorithm and should move to the new one. If the id **matches** the default, it delegates to that encoder's own `upgradeEncoding`. - **BCryptPasswordEncoder**: parses the cost embedded in the stored `$2a$NN$` hash and returns `true` if that cost is **less than** the encoder's configured strength. So bumping strength from 10 → 12 makes every `$2a$10$` hash eligible for upgrade. - **Argon2PasswordEncoder / Pbkdf2 / SCrypt**: similarly compare stored parameters against configured ones. ## How Spring wires it into login The upgrade is **lazy and opportunistic** — it happens only during a successful authentication, because that is the one moment the server legitimately holds the plaintext password: 1. `DaoAuthenticationProvider` loads the `UserDetails` and calls `passwordEncoder.matches(presented, stored)`. 2. On success, it calls `passwordEncoder.upgradeEncoding(stored)`. 3. If that returns `true` **and** a `UserDetailsPasswordService` bean is present, it calls `userDetailsPasswordService.updatePassword(user, passwordEncoder.encode(presented))`. 4. `updatePassword` persists the freshly, more-strongly encoded hash and returns an updated `UserDetails`. Without a `UserDetailsPasswordService` bean, `upgradeEncoding` is effectively a no-op signal — nothing gets rewritten. ```java @Service class JdbcUserPasswordService implements UserDetailsPasswordService { @Override public UserDetails updatePassword(UserDetails user, String newHash) { repo.updatePasswordHash(user.getUsername(), newHash); return User.withUserDetails(user).password(newHash).build(); } } ``` ## When to use it - Raising BCrypt strength over time as hardware gets faster. - Migrating algorithm, e.g. `{bcrypt}` → `{argon2}`, by changing `idForEncode` while keeping the old encoder in the map for verification. - Retiring a compromised/weak legacy scheme gradually. ## Edge cases and gotchas - **The plaintext requirement**: re-hashing needs the raw password, so it can only happen at login. Users who never log in keep their old hashes forever — plan a cutoff/forced-reset if you must fully retire an algorithm. - **You must provide `UserDetailsPasswordService`** — `InMemoryUserDetailsManager` and `JdbcUserDetailsManager` implement it, but a custom `UserDetailsService` does not unless you add it. - **Idempotency/concurrency**: two concurrent logins could both try to update; the write should be safe to repeat. - **Auditing**: silent password rewrites can surprise change-data-capture or audit systems; make sure a hash change on login is expected. - It only strengthens; it never *downgrades*, and it does nothing if `matches()` fails.
- Why can password upgrading only happen at login and not in a nightly batch job?Re-hashing requires the plaintext password to feed into encode(). The server only holds the plaintext momentarily during a login attempt; stored hashes are one-way and cannot be reversed, so a batch job has nothing to re-hash with.
- You set BCryptPasswordEncoder(12) and expect old hashes to upgrade, but nothing changes on login. What is likely missing?There is no UserDetailsPasswordService bean. DaoAuthenticationProvider computes upgradeEncoding()==true but has nowhere to persist the new hash, so it silently skips the update. Implement UserDetailsPasswordService (or use a manager that already does) and wire it in.
saying these in an interview costs you the question
- Thinking upgradeEncoding re-hashes all users immediately or via a batch
- Believing it works without a UserDetailsPasswordService
- Assuming Spring can decrypt old hashes to migrate them
- Not knowing users who never log in keep old hashes