Authentication
Credentials, JWT access & refresh tokens, device tracking, password resets and spam protection.
The Auth class is the entry point: it signs a credential in, issues
the JWT tokens, and answers the questions your code and routes ask about the current request.
Signing in
Resolve a credential by email, confirm it may sign in and that the password
matches, then call Auth::login() — which issues the tokens and starts the session.
Auth::logout() ends it:
use Framework\Auth\Auth;
$credential = Auth::getLoginCredential($email);
if (Auth::canLogin($credential) && Auth::isPasswordCorrect($password)) {
Auth::login($credential);
}
Once signed in, everything about the current request is available statically — you never thread the user through your code:
| Method | Returns |
|---|---|
getCredential() | The current credential entity. |
getID() / getAdminID() | The signed-in id (and the admin id while impersonating). |
getAccessName() | The current access level. |
getLanguage() | The user's language. |
isLoggedIn() / isAdmin() | Session checks. |
Access & refresh tokens
Sessions are stateless. Signing in issues a pair of tokens — a short-lived access token that
authorizes each request and a long-lived refresh token that renews it — and every request carries
them in its xAccessToken / xRefreshToken values.
Framework::execute() validates them and attaches fresh ones to
the response, so the client simply stores whatever it last received:
use Framework\Auth\Auth;
Auth::getAccessToken(); // the current JWT access token
Auth::getRefreshToken(); // the refresh token
The access token (JWT)
The access token is a JWT
(through firebase/php-jwt), signed HS256 with your AUTH_KEY. It carries the
standard iat (issued-at), nbf (not-before) and exp (expiry) claims plus a
data payload with the credential id, and expires AUTH_HOURS hours after it is issued.
Because it is signed, it is validated by decoding it with the key — no database lookup — which is what
keeps the request path stateless. AuthToken creates and verifies it.
The refresh token
The refresh token is the opposite: an opaque token stored in the database (one row per device),
valid for AUTH_DAYS days. Once the access token has expired, the client sends its refresh token and
Framework::execute() looks it up to resolve the credential and mint a new access token — so
the user stays signed in without re-entering a password. Being stored means they can be revoked: signing out
removes the row, and RefreshToken::removeOld() prunes expired ones on a
schedule.
Configuring the tokens
Three environment keys tune them:
| Key | Sets |
|---|---|
AUTH_KEY | The secret the JWTs are signed with — keep it private; changing it invalidates every issued token. |
AUTH_HOURS | Access-token lifetime, in hours (shorter is safer, but refreshes more often). |
AUTH_DAYS | Refresh-token lifetime, in days — how long a session survives before a full sign-in. |
API access
For server-to-server calls, set a static AUTH_API_TOKEN and send it as the token field
or an Authorization header. Framework::execute() validates it with
Auth::validateAPI() and marks the request as an API call — those may send a JSON body instead of form
fields. That token grants the API role; a token of its own grants any other.
Access roles & the Access class
Access levels are declared with AccessRole::register() and grouped in a
config file. The build turns them into a typed
Access enum — one case per role, ordered by the level you registered them at:
use Framework\System\Access;
Access::Admin; // an enum case
Access::getLevel(Access::Admin); // its numeric level
Access::getName(Access::Admin); // its localized name
For each role the class generates checks against the current user, so authorization reads
naturally — and because levels are ordered, you get OrHigher / OrLower variants:
Access::isAdmin(); // exactly Admin
Access::isAdminOrHigher(); // Admin or above
Access::isGeneralOrHigher();
The second argument to register() is a group — a named set of roles,
separate from the level ordering. Register several roles under the same group name to bundle them (a role always
belongs to exactly one group):
AccessRole::register("Owner", "Staff");
AccessRole::register("Manager", "Staff");
AccessRole::register("Client", "Customer");For every group the Access class generates membership and listing helpers, named after the group.
For the Staff group above:
Access::inStaffs(); // is the current user Owner or Manager?
Access::isValidStaff(Access::Owner); // is a given role part of the group?
Access::getStaffs(); // [Access::Owner, Access::Manager]
Access::getStaffSelect(); // a Select list of the group's roles
Use a group when a permission spans several roles that a single OrHigher check can't express —
membership is by set, not by level. The same enum is what every route
declares in its #[Route("/path", Access::Admin)] attribute, and
Framework::execute() enforces it before your method runs.
A role reached with a token
A role can be given a token, and a request that carries it is that role — no credential, no sign in. It is how an
integration of its own gets in: one token per role, each with the access that role has, so what it can reach is
declared the same way everything else is. The fourth argument to register() is the name
of the environment key that holds the token, never the token itself:
AccessRole::register("Zapier", "API", tokenKey: "ZAPIER_TOKEN");ZAPIER_TOKEN = "the-secret-value"The build writes the key into the Access class, and the value is read
from the environment as the request comes in, so no secret is ever written into
the code:
Access::getTokenKey(Access::Zapier); // "zapierToken"
Access::getTokenAccesses(); // [Access::Zapier]
The request sends it as the token field or an Authorization header, the same way the
API token is sent, and Auth::validateAPI() tries each registered token before the
one of the config. A match sets the access of the request to that role, and from there it is treated as an API call:
it may send a JSON body, and it is answered without tokens. An empty token is refused rather than compared, so an
app with none configured does not let in a request that brings none either.
Where the role sits decides what the token reaches. Registered in the API group, as above, it reaches
every route of that group and of General. Registered anywhere else, it reaches what its level reaches,
the way a signed-in user of that role would:
#[Route("/orders/import", Access::Zapier)] // only that token
#[Route("/orders/list", Access::API)] // any of the API roles
Password resets
Reset drives the forgot-password flow: create() issues a one-time code to email the
user, codeExists() validates the code they return, and getCredentialID() resolves it back
to an account so a new password can be set. deleteOld() prunes expired codes:
use Framework\Auth\Reset;
$code = Reset::create($credentialID); // email this code/link to the user
// …when they follow the link:
if (Reset::codeExists($code)) {
$credentialID = Reset::getCredentialID($code);
// set the new password, then:
Reset::delete($code);
}
Spam protection
To blunt brute-force and abuse, Spam::protect() rate-limits repeated attempts from the same source
and returns whether the action should be blocked; Spam::reset() clears the counter after a success.
Framework::execute() also runs a spam check on incoming requests:
use Framework\Auth\Spam;
if (Spam::protect()) {
return; // too many attempts — reject
}
// …on success:
Spam::reset();
Signing in as a user
An admin can act as another user — for support, say — without their password. loginAs() switches
the session to the target credential while remembering the admin (getAdminID() and
isLoggedAsUser() report it), and logoutAs() switches back. canLoginAs()
guards it: an admin may only impersonate users at or below their own level.
use Framework\Auth\Auth;
Auth::loginAs($userCredentialID); // act as the user
Auth::logoutAs(); // return to the admin
Configuration
Authentication is toggled and tuned through environment values:
| Key | Controls |
|---|---|
AUTH_ACTIVE | Enables the auth module. |
AUTH_KEY | Secret used to sign the JWT tokens. |
AUTH_HOURS | Access-token lifetime, in hours. |
AUTH_DAYS | Refresh-token lifetime, in days. |
AUTH_API_TOKEN | Static token for server-to-server API access. |
AUTH_FIELDS | Extra credential fields to expose. |