How does DelegatingPasswordEncoder work, and what is the significance of the {id} prefix in a stored hash?
answer
- {id}hash format e.g. {bcrypt}$2a$...
- map of id -> encoder, idForEncode default bcrypt
- matches() reads prefix, delegates
- algorithm agility / gradual migration
- no prefix -> IllegalArgumentException
basics
~20 sDelegatingPasswordEncoder stores each hash with a prefix like {bcrypt} or {argon2} that names the algorithm. On matches(), it reads the prefix, picks the matching encoder, and delegates. This lets one bean verify hashes made by different algorithms.
solid answer
~40 sDelegatingPasswordEncoder wraps a map of algorithm-id to PasswordEncoder. When it encodes, it prefixes the hash with the default algorithm's id, e.g. {bcrypt}$2a$10$.... When it matches, it parses the {id} at the front of the stored hash, looks up the corresponding encoder, and delegates verification to it. This gives you algorithm agility: a single bean can verify old {bcrypt} hashes while encoding new ones with {argon2}, so you can migrate algorithms without a big-bang re-hash. You create it via PasswordEncoderFactories.createDelegatingPasswordEncoder(), which pre-populates encoders for bcrypt, argon2, pbkdf2, scrypt, and legacy ones. The catch: a stored hash without a recognized {id} prefix causes matches() to throw IllegalArgumentException ("no PasswordEncoder mapped for the id null"), which matters when importing legacy unprefixed hashes.
code
java · 12 lines// Encode with Argon2 going forward, still verify legacy BCrypt hashes
String idForEncode = "argon2";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("bcrypt", new BCryptPasswordEncoder());
DelegatingPasswordEncoder enc = new DelegatingPasswordEncoder(idForEncode, encoders);
// Optional: treat bare (unprefixed) legacy hashes as bcrypt
enc.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
enc.encode("pw"); // -> {argon2}$argon2id$...
enc.matches("pw", "{bcrypt}$2a$10$..."); // delegates to BCryptgo deeper
Recognize the {bcrypt} prefix and that it names the algorithm.
Explain the id->encoder map, default encode id, and how matches() dispatches.
Discuss the unprefixed-hash failure, setDefaultPasswordEncoderForMatches, and its role in migration.
Position the prefix as the enabler of algorithm agility and zero-downtime credential migration.
## The problem it solves: algorithm agility Hashing algorithms and their cost parameters age. Ten years ago BCrypt strength 10 was fine; today you may want Argon2 or a higher strength. If your stored passwords have no record of *how* they were hashed, you cannot verify old logins after switching algorithms, and you cannot migrate gradually. `DelegatingPasswordEncoder` solves this by tagging every hash with the algorithm that produced it. ## Storage format Hashes are stored as `{id}encodedPassword`, where `id` is the algorithm identifier in curly braces. Examples: ``` {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG {argon2}$argon2id$v=19$m=16384,t=2,p=1$... {pbkdf2}5d923b44a6d129f3ddf3e3c8d46d2b... ``` The `{id}` is data stored in the database, not a Java annotation. ## How it dispatches Internally `DelegatingPasswordEncoder` holds: - `idForEncode` — the id used when **encoding** new passwords (default `bcrypt`). - a `Map<String, PasswordEncoder>` of id → encoder. `encode(raw)` calls the encoder for `idForEncode` and returns `"{" + idForEncode + "}" + inner.encode(raw)`. `matches(raw, stored)` extracts the id between the leading `{` and `}`, looks up that encoder in the map, strips the prefix, and delegates to `inner.matches(raw, remainder)`. ## Building it The idiomatic factory: ```java PasswordEncoder enc = PasswordEncoderFactories.createDelegatingPasswordEncoder(); ``` This registers encoders for ids including `bcrypt`, `argon2`, `pbkdf2`, `scrypt`, `sha256`, `ldap`, `MD4`, `MD5`, and `noop`, with `bcrypt` as the default encode id. You can also construct one manually to change the default or the map: ```java String idForEncode = "argon2"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("bcrypt", new BCryptPasswordEncoder()); PasswordEncoder enc = new DelegatingPasswordEncoder(idForEncode, encoders); ``` This encodes new passwords with `{argon2}` but still verifies existing `{bcrypt}` ones. ## Edge cases and gotchas - **Unprefixed / unknown id**: if the stored hash has no `{id}` or an id not in the map, `matches()` throws `IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"` (or the unknown id). This bites teams importing legacy hashes that were stored bare. Remedy: set a `DefaultPasswordEncoderForMatches` via `setDefaultPasswordEncoderForMatches(...)`, e.g. a `BCryptPasswordEncoder`, so bare hashes are treated as that algorithm. - **A literal `{` in a stored value** without a closing `}` is treated as having a null id and fails the same way. - The prefix is **not a secret** — it only names the algorithm and its public parameters, which are already visible inside the hash. Revealing you use BCrypt vs Argon2 does not weaken security. - Combined with `upgradeEncoding`, the prefix is what lets Spring detect that a hash was made by an old algorithm/strength and transparently re-hash it on next successful login.
- You migrated a legacy table whose hashes have no {id} prefix and now every login throws IllegalArgumentException. How do you fix it?Call setDefaultPasswordEncoderForMatches(...) on the DelegatingPasswordEncoder with the encoder that actually produced those hashes (e.g. new BCryptPasswordEncoder()). Bare hashes then verify against that encoder. A cleaner long-term fix is a data migration that prepends the correct {id} prefix.
- Is exposing the algorithm id like {bcrypt} in the database a security risk?No. The id only names the algorithm and its public cost parameters, which are already embedded in the hash itself. Security relies on the hash being slow and salted, not on hiding which algorithm was used.
saying these in an interview costs you the question
- Thinking {bcrypt} is a Java annotation rather than a stored string prefix
- Believing DelegatingPasswordEncoder can only handle one algorithm at a time
- Claiming the {id} prefix must be secret
- Not knowing unprefixed hashes throw IllegalArgumentException