Framework
Subsystems

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:

MethodReturns
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:

KeySets
AUTH_KEYThe secret the JWTs are signed with — keep it private; changing it invalidates every issued token.
AUTH_HOURSAccess-token lifetime, in hours (shorter is safer, but refreshes more often).
AUTH_DAYSRefresh-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.

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):

config/Access.config.php
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.

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:

KeyControls
AUTH_ACTIVEEnables the auth module.
AUTH_KEYSecret used to sign the JWT tokens.
AUTH_HOURSAccess-token lifetime, in hours.
AUTH_DAYSRefresh-token lifetime, in days.
AUTH_API_TOKENStatic token for server-to-server API access.
AUTH_FIELDSExtra credential fields to expose.