friendsofsymfony/advanced-encoder-bundle
A Symfony bundle that provides an advanced password encoder factory, letting different users use different encoders. Useful for migrating legacy password hashes and gradually upgrading users to stronger algorithms without forcing password resets or locking accounts out.
What it actually solves. Symfony's stock security.encoder_factory resolves a password encoder by class (get_class($user)). That means every Acme\UserBundle\Entity\User row gets the same algorithm. This bundle swaps in a factory that first asks the user object itself which encoder to use, falling back to the class-based lookup. The canonical use case is a password migration: old rows hashed with md5 (or sha512), new rows with bcrypt or pbkdf2, both verified correctly while you backfill.
Install (legacy Symfony 2.x only).
composer require friendsofsymfony/advanced-encoder-bundle
// app/AppKernel.php
new FOS\AdvancedEncoderBundle\FOSAdvancedEncoderBundle(),
Nothing else is required if the bundle's compiler pass wires itself over security.encoder_factory; otherwise import the service config from Resources/config/.
Version note (v1.1.0). The 1.1.0 release adds explicit support for Symfony 2.3+'s bcrypt and pbkdf2 encoders and adds CI coverage on PHP 5.5, 5.6 and HHVM. This does not change the integration contract — it only means the modern-encoder side of a migration can now be bcrypt/pbkdf2 rather than being limited to sha512-style digest encoders. If you are on Symfony < 2.3, those two encoder types are unavailable and the release note does not apply to you.
Where to look first, in this order:
README.md — short, but it states the intended contract.DependencyInjection/Configuration.php — the only trustworthy source for the config tree keys; do not guess them.Security/Encoder/ — the factory implementation and the marker interface your User entity must implement (EncoderAwareInterface-style, exposing getEncoderName()).Resources/config/*.xml — confirms whether the bundle decorates or replaces security.encoder_factory, which determines whether you need to keep security.encoders populated.First use case. Add a nullable encoder column to your user table. Seed legacy rows with the legacy encoder name (sha512, or whatever your pre-migration default was), new rows with bcrypt. Implement the interface to return that value (or null to fall through). On successful login, if the stored name differs from the current default, rehash and persist. That's the whole win: no forced password resets.
Per-instance override on the entity. The cleanest integration is on a base user class so every concrete user inherits it:
use FOS\AdvancedEncoderBundle\Security\Encoder\EncoderAwareInterface;
class User implements EncoderAwareInterface
{
/** @ORM\Column(type="string", nullable=true) */
private $encoder;
public function getEncoderName()
{
return $this->encoder ?: null; // null => default class-based encoder
}
}
Everything downstream (DaoAuthenticationProvider, your custom login form, security.encoder_factory consumers) picks this up without changes — that's the main appeal of doing it at the factory layer rather than in your auth code.
Migration listener, not a controller. Hook security.interactive_login (or your success handler) and only after authentication succeeds:
public function onLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
$current = $this->defaultEncoderName; // e.g. 'bcrypt', from your security config
if ($user->getEncoderName() !== $current) {
$encoder = $this->encoderFactory->getEncoder($user);
$user->setPassword($encoder->encodePassword(
$event->getRequest()->get('_password'),
$user->getSalt() // must be null/'' for bcrypt & pbkdf2
));
$user->setEncoderName($current);
$this->em->flush();
}
}
Never rehash before verification — you'd be trusting an unauthenticated password.
Configuring the modern encoders. In security.yml the 2.3+ encoder keys are bcrypt and pbkdf2; you can also use the FQCNs (Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder, Pbkdf2PasswordEncoder) if you need to pass constructor arguments. Keep the class-level entry pointing at the new encoder so freshly created users and null-returning users get the modern hash:
security:
encoders:
Acme\UserBundle\Entity\User:
algorithm: bcrypt
cost: 12
Multi-tenant / externally provisioned users. If users arrive with hashes produced elsewhere, store the algorithm identifier alongside the hash and let the entity report it. This is the second legitimate day-to-day use: one user table, several provisioning sources, each with its own hashing policy. With 1.1.0 those sources can now include bcrypt/PBKDF2 systems without you writing a custom encoder wrapper.
Keep the class-based default in security.encoders. Even when overriding per instance, leave a sane default configured for the class so users that return null still authenticate. The bundle's factory should fall back; verify that against the source before relying on it.
Test at the factory boundary. Unit-test getEncoder($user) with plain mock users returning different names — and include bcrypt and pbkdf2 in the matrix, now that 1.1.0 claims support. Add one functional login test per encoder. This is cheap and catches config typos that otherwise only surface on a real user's first login.
MigratingPasswordEncoder/PasswordHasherFactory; on Symfony 2.6+ an EncoderAwareInterface exists in the core. 1.1.0 shows the project was still receiving occasional maintenance at that point, but it does not extend the supported platform — before adopting, check whether your Symfony version already does this, since you may only need the ~50 lines of the factory, copied and owned.security.encoder_factory class parameter or uses a compiler pass. Confirm which, because a replace-style override silently breaks if a second bundle does the same.BCryptPasswordEncoder rejects a non-null salt passed to encodePassword(), and PBKDF2 likewise generates its own. Users on these encoders must have getSalt() return null/empty — the classic failure is a legacy salt column that is still populated, so login works for legacy users and hashing throws for new ones. Decide this per encoder, not globally.cost for bcrypt rather than accepting defaults — this is the one place where 1.1.0's new support is a config decision, not a code one.bcrypt/pbkdf2 names) and treat unknown values as null on read; never let a user-editable field reach the factory unchecked.null. The fallback contract depends on returning a falsy value. getEncoderName() returning '' from a default-initialized column is the classic bug — normalize in the getter.isGranted decisions; keep it out of anything security-sensitive beyond password checking.Serializable implementation, add the property explicitly or the override disappears mid-session.debug:container security.encoder_factory to confirm the override is live; dump get_class($user) and getEncoderName() in the login path; run a password_verify-equivalent check manually against a known legacy row and a known bcrypt row.password_hash()/password_verify() are available. If you are not on Symfony's encoder API anyway, a thin custom factory wrapping the native functions is a viable, dependency-free alternative — and sidesteps the salt-column and cost-config friction above.getEncoderName() method discovered by reflection) instead of forcing the interface onto every user class.symfony/security-bundle and PHP 7+. Budget for a fork or vendoring rather than expecting a clean resolve; 1.1.0 does not loosen any of those constraints.How can I help you explore Laravel packages today?