Authentication architecture¶
Auth separates credential verification, identity loading, authorization and persistence. Authentication explains usage; this chapter describes how its services cooperate and which guarantees depend on the application provider.
Required packages
In an existing NAF application:
composer require naf/auth
One implementation per question¶
Auth owns permission checks and the current identity. auth()->user() returns the
provider's identity object. IdentityInterface exposes the identifier, roles and permissions;
UserInterface adds activity and profile information.
can('a', 'b') requires both permissions; canAny('a', 'b') accepts either. Per-object
allows($action, $resource) checks a registered policy independently of global grants.
No policy means denial. Applications register providers and policies in their bootstrap.
Provider responsibilities¶
ProviderInterface declares authenticate() and find(). Everything a backend needs — a PDO
connection, an LDAP handle, an HTTP client — is a constructor dependency of the provider, resolved
through the container when it is registered by class name.
PasswordProviderverifies passwords and uses a decoy hash for unknown accounts to reduce timing differences between unknown-account and incorrect-password paths.OrmProvideris the ORM implementation. It maps a repository and three column names onto those two questions, and requires the model to implementIdentityInterface— that requirement is what keepsauth()->user()returning the application's own class instead of a wrapper.- Providers registered by name are resolved lazily and cached, so registering five sources in
bootstrap.phpdoes not construct them until authentication or identity loading needs them.
Persistence¶
StateStoreInterface reads, writes and clears exactly two values: the source name and the
identifier. Never the model, never the grants.
- Restoration reloads through
find()and compares the returned identifier against the stored one, so a provider that hands back somebody else cannot silently swap the person. A missing account, an unregistered source or a malformed record clears the record instead of falling back. - That comparison lives in
load(), andrestore()calls it. The public method exists because external logins and CLI tools need the same reload, and a second copy of the check is a second place for it to drift.load()returns null where restoration would clear the record; deciding what that means is the caller's job, so it touches no session state of its own. SessionStateStorerotates the session ID on login and verifies that it actually changed. A rotation that does not happen aborts the login rather than publishing a person onto the guest's ID.SessionStateStorereceives its session through constructor injection. The bootstrap creates the store lazily when the manager needs persistence. No service starts a session in its constructor.- The manager factory prefers a custom
StateStoreInterfacebinding, then honoursauth:session: false disables persistence, null enables it whennaf/sessionis installed, true requires it.
Wiring¶
bootstrap.php is the composition root. It registers Auth, PasswordHasher, SessionStateStore,
DatabaseProvider and OrmProvider as lazy container factories. The manager factory visibly calls
addProvider() for auth:providers and policy() for auth:policies. No source is enabled by default.
Custom bindings take precedence. auth() only gets the manager; it never registers services.
The providers receive their dependencies explicitly. The ORM repository is resolved or created by
RepositoryFactory in the bootstrap; neither the ORM provider nor the session adapter accesses
the global container. Providers registered by class name use the resolver injected into Auth,
which calls container get(). There is no implicit new $class() or make() fallback.
The PDO identity factory is also supplied by the bootstrap. It creates a fresh identity per lookup; identities and credentials are data, not container singletons. Exceptions are created where errors occur. Service construction and registration belong in the bootstrap, including application-owned provider factories and any imperative policy/provider registrations.
Boot the auth plugin before using auth(), and resolve it after dependency plugins have booted.
Lazy factories allow registration before the session, PDO or ORM services exist. Missing or
incorrectly ordered dependencies fail when the manager is resolved.
Authorization exceptions expose
getStatusCode() so the framework error handler renders 401 and 403 instead of a generic 500; they
carry the same value as their exception code and stay usable outside HTTP.
What the plugin guarantees¶
- With
SessionStateStore, a login rotates the session ID or fails. - Restoration distinguishes sources even when identifiers collide, and re-reads grants every request.
- A resource policy that denies — or the absence of one — is never bypassed by a global permission.
- Password providers verify a decoy hash for unknown usernames to reduce timing differences. Credentials are marked
#[\SensitiveParameter]andPasswordCredentialsredacts its debug output. Nothing but a source name and an identifier is persisted. - A failed attempt leaves an existing login intact. Call
logout()first if reauthentication should end the current session.
What it leaves to the application¶
Password policy, rate limiting, lockout, MFA and invitation flows are account-lifecycle decisions.
Business rules such as last-admin protection or delegation caps belong in policies, next to the
mutation they guard. The session adapter inherits the storage and cookie guarantees of
naf/session; there is no token revocation store and no distributed limiter here.
Plain PDO accounts¶
DatabaseProvider extends the existing password provider and accepts a PDO connection directly.
It provides parameterized lookup by username/identifier and password-hash upgrades without ORM
or database-plugin dependencies. A row mapper optionally creates the application identity or
rejects disabled accounts; the password hash stays out of the mapped row. The provider retains the hash of the current lookup for the synchronous PasswordProvider
verification flow, avoiding a second query. The mapper runs before that hash is assigned, so
a nested lookup from the mapper cannot replace the outer authentication's hash. Rehash updates
compare the original hash so concurrent resets are not overwritten. Table/column names are validated and quoted; ambiguous accounts fail.