A lightweight authentication service: verifies a login/password pair against a PDO-backed table and tracks the authenticated user id for the rest of the request.
use orange\auth\Auth;
use orange\auth\AuthError;
$auth = Auth::getInstance($config, $pdo); // config merges over auth/src/config/auth.php
if ($auth->login($_POST['email'], $_POST['password'])) {
$userId = $auth->userId();
} else {
echo $auth->error(); // e.g. "Login Error.", "Your user is not active."
// branch on the code, not the (configurable) message
if ($auth->errorCode() === AuthError::NotActivated) {
// offer to resend the activation email, say
}
}
$auth->logout(); // clears the error/code and resets userId() to 0AuthError is the stable machine-readable contract: None, EmptyFields, UnknownUser, BadPassword, NotActivated. Present UnknownUser and BadPassword identically to the end user — the distinct cases exist for server-side logging/metrics; rendering them differently would reveal which logins exist.
Configure table, username column, password column, is active column, and (optionally) is deleted column to match your schema — each is validated as a plain SQL identifier at construction, so a typo'd config value throws immediately. Passwords must be stored with PHP's password_hash() — login() verifies them with password_verify(), and only succeeds when the matching row's active column equals 1.
A cookbook of worked examples lives in example.md.
src/config/auth.php (merged under anything you pass to Auth::getInstance()):
| Key | Default | Purpose |
|---|---|---|
table |
orange_users |
credential table (matches orange/acl; DDL in support/) |
username column |
email |
login identifier column |
password column |
password |
password_hash() column |
is active column |
is_active |
must equal 1 to log in |
is deleted column |
is_deleted |
soft-deleted rows read as unknown; null disables |
normalize login |
true |
trim + lowercase the login before lookup |
empty fields error |
Missing Required Field. |
message for AuthError::EmptyFields |
general error |
Login Error. |
message for AuthError::UnknownUser |
incorrect password error |
Login Error. |
message for AuthError::BadPassword |
not activated error |
Your user is not active. |
message for AuthError::NotActivated |
The defaults target the same orange_users table orange/acl ships — the two packages are designed to pair (auth answers "who is this", acl answers "what may they do"). On a successful login pass userId() to the acl package's User::change(), which regenerates the session id itself (fixation defense):
if ($auth->login($email, $password)) {
$currentUser->change($auth->userId());
}- Logins are normalized before lookup: always trimmed, and lowercased by default (
'normalize login' => true) so matching doesn't depend on the database collation — store identifiers lowercase. Set the flagfalsefor case-sensitive username schemes. - Stale hashes upgrade themselves. After a successful verify,
password_needs_rehash()is checked and the stored hash transparently rewritten with current defaults — login is the only moment the plaintext is available, so imported or legacy hashes strengthen over time without any migration. - The PDO handle is switched to
ERRMODE_EXCEPTIONat construction — this class's SQL reports failures by throwing, never by returningfalsemid-flight.
- An unknown login and a soft-deleted account (
is deleted column, set it tonullfor tables without one) both report the genericgeneral error— indistinguishable from a wrong password by message. - The unknown-login path burns a
password_verify()against a dummy hash so it costs the same as a wrong password — response timing can't be used to enumerate accounts. - Passwords longer than 1024 bytes are rejected before hashing (bcrypt reads at most 72 anyway) — a megabyte "password" is a CPU-burn attempt, not a credential.
- Deliberate non-goals: rate limiting and account lockout need state shared across requests (a table or cache) and belong to the application layer — this class does not provide them.
cd unittest && sh runUnitTests.sh # runs against in-memory sqlite