-
Notifications
You must be signed in to change notification settings - Fork 0
Migration Guide
v2 ships intentional behaviour changes. Most are bug fixes that have a visible BC impact; a handful are deliberate hardening of defaults. This page is the full list, in the order most callers will hit them.
If you only want the short version: bump your constraints,
re-generate your cookie salt, regenerate any persisted v1 cookies
(they will be silently dropped and re-issued), and find/replace
getPermission() with getPermissions(). Everything else either
works the same or only affects edge cases.
| v1 | v2 | |
|---|---|---|
| PHP | >=7.4 |
^8.0 (tested on 8.0 – 8.4) |
initphp/parameterbag |
^1.0 |
^2.0 |
ext-json, ext-hash
|
not strictly required | required (bundled with default PHP builds) |
composer require initphp/auth:^2.0
composer require initphp/parameterbag:^2.0 # if you depend on it directlyv1 bug: the constructor stored permissions verbatim while
is()/push()/remove() lower-cased the needle, so a mixed-case
permission supplied at construction time could never match.
// v1
$perm = new Permission(['Editor']);
$perm->is('editor'); // falsev2 fix: the constructor normalizes (lower-case + trim) and
deduplicates, the same way push() does.
// v2
$perm = new Permission(['Editor']);
$perm->is('editor'); // trueAction: if you worked around the bug by lower-casing input before passing it in, that workaround now becomes a no-op (still correct). Nothing to change.
See Permissions for the full discussion.
Renamed for plural consistency. The old method survives as a deprecated alias and will be removed in v3.
$perm->getPermission(); // still works in v2, raises @deprecated notice in IDEs
$perm->getPermissions(); // preferredAction: find/replace at your leisure.
Underscore-prefixed properties were a v1 PSR-12 violation. Anything
that touched $_perms directly (subclasses, reflection, serialized
payloads from v1) will need to be updated.
__sleep() now lists permissions, so v1 serialized blobs cannot be
reinflated under v2 — re-serialize them when you next hydrate.
v1 bug: remove() left a hole in the internal array because
unset() does not reindex. A subsequent getPermission() returned a
non-list array which broke JSON encoding.
v2 fix: remove() calls array_values() after unset(), so the
list<string> invariant always holds.
v1: always returned true, which combined with get() always
returning the default produced the inconsistent pair
has(x) === true && get(x) === null.
v2: has() returns false — nothing is ever present in a Null
Object store.
Action: verify that no production code relies on the buggy
has() === true to satisfy a guard. The kind of guard you wrote was
almost certainly meant to fall through, which is exactly what v2 makes
it do.
See Null Adapter for the detail.
v1: base64(serialize([data, hash])) with md5(sha1(...)).
v2: base64url(json_encode($data)) . "." . hash_hmac('sha256', $json, $salt).
Why:
- HMAC +
hash_equals()instead of a hand-rolled hash with!=(constant-time comparison, no timing side-channel). - JSON instead of
serialize()(no object instantiation path during decode, no POP-gadget risk). - Hash verified before the payload is parsed.
Action: v2 cannot read v1 cookies — they are silently dropped and
the user is issued a fresh v2 cookie on their next write. Plan for a
quiet logout of everyone holding a v1 cookie. There is no migration
path that would risk running unserialize() against attacker-controlled
bytes.
See Cookie Adapter → Wire format and Security.
v1: minimum 8 characters.
v2: minimum 32 bytes (matches the SHA-256 output length).
// Generate one
echo bin2hex(random_bytes(32)); // 64 hex characters, 32 bytesAction: if your existing salt is shorter, generate a longer one and update your environment. (You will need to do this anyway because the wire format changed.)
| Option | v1 default | v2 default |
|---|---|---|
secure |
false |
true |
samesite |
'None' |
'Lax' |
SameSite=None requires Secure=true per the modern cookie spec.
v2 rejects the unsafe combination with InvalidArgumentException
instead of silently emitting a cookie the browser will drop.
Action: if you were running on plain HTTP in development, opt
back into secure=false explicitly and drop SameSite back to
Lax or Strict. Production should run on HTTPS.
v1 bug: the deletion setcookie() call only set expires, so the
browser refused to delete a cookie originally written with a
non-default path.
v2 fix: the deletion reuses $this->options and only overrides
expires. No action required; cookies that previously refused to
delete will now delete.
The constructor gained an optional third argument:
new CookieAdapter(
string $name,
array $options = [],
?CookieWriterInterface $writer = null,
);Default behaviour is unchanged (NativeCookieWriter wraps
setcookie()). Tests can inject InMemoryCookieWriter to capture
calls without touching response headers. See Cookie
Writer and Testing.
v1: SessionAdapter::__call($name, $args) forwarded to the
internal ParameterBag. Calls like $adapter->merge([...]) mutated
the bag but never synced back to $_SESSION — silent data loss.
v2: the magic is gone. Use the documented
get/set/has/remove/collective/destroy methods, all of which sync
$_SESSION after every write.
Action: if you reached into bag methods through __call, switch
to $adapter->collective([...]) or to one of the explicit methods.
The constructor's second argument is now forwarded straight to the
underlying ParameterBag. The biggest practical effect is that you
can opt into dotted-path access:
$auth = new SessionAdapter('auth', ['isMulti' => true]);
$auth->get('profile.name');In v1 the options array was accepted and ignored. See Session Adapter → Constructor options.
Segment::create() and the constructor still take an int|string
adapter (kept for v1 BC), but new code should use the typed factories:
Segment::session($name, $options);
Segment::cookie($name, $options);
Segment::custom($name, $adapterClass, $options);The error messages on misuse are also more helpful — passing an
unknown integer constant or a class that does not extend
AbstractAdapter now tells you exactly which case it hit. See
Exceptions.
In v1, Segment delegated to the underlying adapter via __call()
only. In v2 it implements AdapterInterface itself, so the six
contract methods are explicit and visible to IDEs / static analysis.
For non-interface adapter methods (a custom adapter that exposes
refreshToken(), say), reach the concrete adapter via the new
adapter() escape hatch, or rely on the __call() magic that is
still in place:
$segment->adapter(); // returns AdapterInterface
$segment->refreshToken('exp'); // delegated through __callSee Segment.
v1: the interface declared __construct(string $name, array $options = []),
which is a PSR anti-pattern and forced every implementation to take
options through an array even when it wanted to inject a PDO handle
or a Redis client directly.
v2: constructors are out of the contract. Segment::custom() still
invokes new YourClass($name, $options), so the convention if you want
Segment compatibility is unchanged — but you can hand-build adapters
with any constructor signature now.
Action: existing adapters that extend AbstractAdapter keep
working. Adapters that implemented the interface directly without
extending the abstract can drop the __construct declaration if they
want.
See Adapter Interface.
v1 redeclared every interface method as abstract in the base class
without adding any shared behaviour. v2 keeps only a default
collective() that iterates set() for adapters that cannot commit
atomically; everything else is satisfied by implementing the
interface. See
Adapter Interface → AbstractAdapter.
v2 ships with the same dev workflow as the rest of the InitPHP ecosystem:
composer install
composer test # PHPUnit (78 tests)
composer analyse # PHPStan level 8, 0 errors
composer cs:check # PHP-CS-Fixer dry-run
composer cs:fix # PHP-CS-Fixer applyThe test suite covers Permission, SessionAdapter, CookieAdapter,
Segment, and NullAdapter. CI runs on PHP 8.0 through 8.4.
- Bump
phpto^8.0in your project'scomposer.json. - Bump
initphp/authto^2.0. - Bump
initphp/parameterbagto^2.0if you depend on it directly. - Generate a fresh cookie salt (
bin2hex(random_bytes(32))) and store it in your environment. - Verify your cookie
secure/samesitedefaults still match your deployment (production: keep the v2 defaults). - Find/replace
getPermission()withgetPermissions()at your leisure. - Replace any
SessionAdapter::__callusage withcollective()or the explicitget/set/has/remove/destroymethods. - Re-run your test suite. The new exception types are stricter; anything that previously fell through silently will now throw.
- Plan a quiet logout window for users currently holding a v1 cookie.
- FAQ — common post-upgrade questions.
- Security — the new threat model and operational practices.
- Cookie Adapter — the biggest user-visible change.
initphp/auth · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Core Types
Adapters
Reference
Recipes
Migration & Help