jardissupport / auth
Signed access tokens and opaque refresh tokens with SQL token store and token authentication (subject + tenant), session handling, password hashing, and role-based access control for PHP DDD applications — framework-free, no JWT, no third-party runtime dependencies beyond PHP built-ins and the Jardi
Requires
- php: >=8.3
- ext-sodium: *
- jardissupport/contracts: ^2.16
- jardissupport/dbquery: ^1.1
Requires (Dev)
- jardisadapter/dbconnection: ^1.2
- phpstan/phpstan: 2.2.3
- phpunit/phpunit: 10.5.63
- squizlabs/php_codesniffer: 3.13.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Part of Jardis — the Domain-Driven Design platform for PHP. You model your domain; Jardis generates the production-ready hexagonal code (DTOs, Command/Query handlers, repositories, persistence). This package is part of the open-source foundation that generated code runs on.
Authentication and authorization without framework coupling. A focused RBAC for PHP covering signed access tokens, opaque refresh tokens, session management, password hashing, and role-based access control — designed for DDD applications. No HTTP layer, no JWT, no third-party runtime dependencies beyond PHP built-ins and the Jardis packages contracts and dbquery. Pure support package.
Why This Package?
- Nine classes to learn —
SessionManager,PasswordHasher,Guard,PasswordAuthenticator,SignedTokenAuthenticator,TokenAuthenticator,DbTokenStore,TokenIssuer,TokenRedeemer. Everything else is data - Opaque tokens — server-side state, SHA-256 hashed storage, no JWT complexity
- Token rotation — automatic refresh with old-token revocation
- RBAC as Value Objects — policies are immutable, defined in code, not in a database
- No third-party runtime dependencies — PHP built-ins (
password_hash,random_bytes,hash,hash_equals) plus the Jardis contract
Installation
composer require jardissupport/auth
Quick Start
Create a Session
use JardisSupport\Auth\SessionManager; use JardisSupport\Auth\Data\Subject; $sessionManager = new SessionManager($tokenStore, $accessTokenKey); // 32-byte signing key, see "Session tokens" $subject = Subject::from('user-42', 'user'); $result = $sessionManager->create($subject, ['role' => 'editor']); $accessToken = $result->accessToken; // send to client $refreshToken = $result->refreshToken; // store securely on client $session = $result->session; // use server-side // Dispatch events (optional — use your EventDispatcher) foreach ($result->events as $event) { $dispatcher->dispatch($event); }
Verify & Refresh Tokens
use JardisSupport\Auth\Data\Credential; use JardisSupport\Auth\SignedTokenAuthenticator; // Verify an access token — signature + expiry, no store, no database $result = (new SignedTokenAuthenticator($accessTokenKey))->authenticate(Credential::token($accessToken)); // Refresh — rotates tokens, revokes the old refresh token $newResult = $sessionManager->refresh($refreshToken); // $newResult->events contains SessionCreated + SessionRefreshed
Hash & Verify Passwords
use JardisSupport\Auth\PasswordHasher; $hasher = PasswordHasher::argon2id(); // Registration $hash = $hasher->hash('secret-password'); // Login $hasher->verify('secret-password', $hash); // true // Rehash check on every login if ($hasher->needsRehash($hash)) { $newHash = $hasher->hash('secret-password'); // update stored hash }
Authorize with RBAC
use JardisSupport\Auth\Guard; use JardisSupport\Auth\Data\Policy; $policy = Policy::create() ->role('admin')->allow('*') ->role('editor') ->allow('article:read', 'article:write', 'article:publish') ->deny('article:delete') ->role('viewer')->allow('article:read') ->role('moderator')->includes('editor')->allow('comment:delete') ->build(); $guard = new Guard($policy); $guard->check($session, 'article:publish'); // true/false $guard->authorize($session, 'article:delete'); // throws UnauthorizedException // Multi-role sessions — first matching role wins $session = new Session( subject: 'user:42', tokenHash: $hash, createdAt: new DateTimeImmutable(), expiresAt: null, metadata: ['role' => ['editor', 'moderator']], ); $guard->check($session, 'comment:delete'); // true (moderator has permission)
Authenticate with Password
use JardisSupport\Auth\PasswordAuthenticator; use JardisSupport\Auth\Data\Credential; $authenticator = new PasswordAuthenticator( $passwordHasher, $sessionManager, function (string $identifier): ?array { $user = $userRepository->findByEmail($identifier); if ($user === null) { return null; } return [ 'hash' => $user->passwordHash, 'subject' => Subject::from($user->id, 'user'), 'claims' => ['role' => $user->role], ]; }, ); $credential = Credential::password('john@example.com', 'secret123'); $result = $authenticator->authenticate($credential); if ($result->isSuccess()) { $session = $result->session; $accessToken = $result->accessToken; } // All events in one place: SessionCreated + AuthenticationSucceeded (or AuthenticationFailed) foreach ($result->events as $event) { $dispatcher->dispatch($event); }
Instead of a closure, the third argument may be a UserLookupInterface (jardissupport/contracts >= 2.10, implemented by an account store such as jardissupport/identity). It returns a CredentialRecordInterface (getPasswordHash(), getSubject() as type:id, getClaims()) or null; a subject without type: becomes user:<value>. The claims are copied into the session at login (SessionClaim::TENANT, SessionClaim::ROLE).
$authenticator = new PasswordAuthenticator($passwordHasher, $sessionManager, $accountStore); // UserLookupInterface
Issue and redeem one-time tokens
Invitation, password-reset and verification links use TokenIssuer and TokenRedeemer on the same TokenStoreInterface. Only the hash is stored; the plain value is handed out once.
use JardisSupport\Auth\TokenIssuer; use JardisSupport\Auth\TokenRedeemer; use JardisSupport\Contract\Auth\TokenType; $issuer = new TokenIssuer($tokenStore); // 2nd argument: token length in bytes (default 32) $redeemer = new TokenRedeemer($tokenStore); // 2nd argument: VerifyToken $issued = $issuer->issue(TokenType::PasswordReset, Subject::from('42'), 3600, ['note' => 'x']); $issued->plain; // send this to the user, never store it $issued->hashed; // HashedToken as stored $token = $redeemer->redeem($plainFromLink, TokenType::PasswordReset); // HashedTokenInterface $token->getSubject(); // 'user:42' $token->getClaims(); // Look first, consume later: same checks as redeem(), but the token stays redeemable $token = $redeemer->peek($plainFromLink, TokenType::Invitation);
redeem() revokes the token on success, so a second call throws TokenRevokedException. peek() performs the same checks (unknown/wrong type, expiry, revocation) with the same exceptions but never revokes — use it to show an invitation or validate a reset link, and redeem() as the last step of the flow (TokenType::Invitation is a valid issued type). Unknown token or wrong type throws InvalidCredentialException (the token stays valid for its right type), an expired one TokenExpiredException.
Invalidate Sessions
// Single session (logout) — returns SessionInvalidated event $event = $sessionManager->invalidate($session); // All sessions for a subject (logout everywhere) — returns nothing (SessionManagerInterface) $sessionManager->invalidateAll('user:user-42'); // Exactly the session behind a session reference (see "Authenticate a Token") — other sessions // of the same subject stay valid; idempotent, an unknown reference revokes nothing; returns nothing $sessionManager->invalidateByReference($result->getSessionReference());
Since v1.6 invalidateAll() and invalidateByReference() return void because SessionManagerInterface (contracts 2.11) prescribes it — before, they returned the AllSessionsInvalidated / SessionInvalidated event. No package in the fleet used those return values; invalidate(SessionInterface) is not part of the interface and still returns its event. The handlers Handler/Session/InvalidateAllSessions and InvalidateSessionByReference still return the events if you need them. Also for the interface, the parameters of SessionManager::refresh() and TokenRedeemer::redeem() are now named $refreshToken and $plain/$expected (only relevant for callers using named arguments).
Revocation acts on the refresh token. invalidate(), invalidateByReference() and invalidateAll() revoke refresh tokens in the store — refresh($oldRefreshToken) afterwards throws AuthenticationException. An already issued access token stays valid until its exp (default 15 minutes), see "Session tokens". refresh() rotates the refresh token and issues a new access token with a new session reference; the refreshed session keeps the claims of the original session. Claims with a _ prefix are package-internal: they are never part of Session::metadata and never reach TenantAuthResult.
Session tokens
Breaking since v1.5.0:
SessionManagertakes the signing key as required second constructor argument (new SessionManager($store, $accessTokenKey)), and session access tokens are no longer stored. Authenticate them withSignedTokenAuthenticator, notTokenAuthenticator. Default access TTL is now 900 s (was 3600 s).
| Token | Form | Storage | Checked by |
|---|---|---|---|
| Access | signed: base64url(payload) . '.' . base64url(mac) |
none | SignedTokenAuthenticator — signature and exp, no database access per request |
| Refresh | opaque random value | SHA-256 hash in TokenStoreInterface |
SessionManager::refresh() |
- Signature:
sodium_crypto_auth(HMAC-SHA512-256) over the encoded payload; no header, noalgfield, no JWT library. Payload:{typ:"access", sub, sid, iat, exp, claims:{…}}. sid(the session reference,getSessionReference()) is the hash of the session's refresh token:invalidateByReference($sid)revokes exactly that session's refresh.- Revocation happens at refresh: an issued access token remains valid until
exp(TTL default 900 s, refresh TTL 604800 s). Keep the access TTL short. - Key: an own secret of exactly 32 bytes — in
.envasAUTH_TOKEN_KEY(base64), handed to the package as raw bytes:base64_decode($_ENV['AUTH_TOKEN_KEY'], true). A key of another length throwsInvalidArgumentException. Do not reuseAPP_SECRET_KEY. - Rotation: replacing the key invalidates all issued access tokens at once (clients refresh and get new ones signed with the new key); refresh tokens are unaffected.
Token Store
DbTokenStore implements TokenStoreInterface on a DbConnectionInterface (jardissupport/contracts) using jardissupport/dbquery — no raw SQL, no own PDO. Only the SHA-256 hash of a token is stored, never the plain value.
use JardisSupport\Auth\DbTokenStore; $store = new DbTokenStore($connection); // table: auth_tokens $store = new DbTokenStore($connection, 'my_tokens'); // custom table name (plain identifier) $sessionManager = new SessionManager($store, $accessTokenKey);
The store holds refresh tokens and one-time tokens (API keys, verification, password reset); session access tokens are signed and never stored.
Table DDL ships with the package (one file per dialect, auth_tokens, timestamps in UTC):
| Dialect | File |
|---|---|
| MySQL / MariaDB | vendor/jardissupport/auth/resources/sql/mysql.sql |
| PostgreSQL | vendor/jardissupport/auth/resources/sql/pgsql.sql |
| SQLite | vendor/jardissupport/auth/resources/sql/sqlite.sql |
Apply the file matching $connection->getDriverName() once in your migration/setup (all statements are IF NOT EXISTS). For another table name replace auth_tokens in the file.
Prefer your own store (Redis, ...)? Implement TokenStoreInterface yourself; an InMemoryTokenStore for tests lives in tests/Support/.
Authenticate a Token (request → subject + tenant)
Session access tokens (issued by SessionManager) are authenticated by SignedTokenAuthenticator($accessTokenKey, $tenantClaim = SessionClaim::TENANT): same result type, same reasons (Invalid token, Token expired, Unsupported credential type; never Token revoked — revocation acts at refresh), no store. getSessionReference() is the token's sid.
TokenAuthenticator resolves a stored token (API key, verification, password reset, or any TokenIssuer-issued token; $expectedType selects the type): a valid, non-revoked, non-expired token yields a successful SessionAwareAuthResultInterface with qualified subject, bare subject id, tenant and a session reference; any other token yields a failure with a reason.
use JardisSupport\Auth\Data\Credential; use JardisSupport\Auth\TokenAuthenticator; use JardisSupport\Contract\Auth\RequestAttribute; // The tenant is a claim of the token, set when the session / token is created: $session = $sessionManager->create(Subject::from('42'), ['tenant' => 'acme']); $authenticator = new TokenAuthenticator($store); // tenant claim: 'tenant' $authenticator = new TokenAuthenticator($store, tenantClaim: 'org'); $result = $authenticator->authenticate(Credential::token($bearerToken)); if ($result->isSuccess()) { $request = $request ->withAttribute(RequestAttribute::SUBJECT, $result->getSubject()) // 'user:42' ->withAttribute(RequestAttribute::SUBJECT_ID, $result->getSubjectId()) // '42' ->withAttribute(RequestAttribute::TENANT, $result->getTenant()) // 'acme' | null ->withAttribute(RequestAttribute::SESSION, $result->getSessionReference()); // hash of the access token } else { $result->getReason(); // 'Invalid token' | 'Token expired' | 'Token revoked' | 'Unsupported credential type' }
Only TokenType::Access tokens are accepted by default (4th constructor argument changes that). The attribute names RequestAttribute::SUBJECT (jardisSubject) and RequestAttribute::TENANT (jardisTenant) live in jardissupport/contracts (>= 2.8.0); SUBJECT_ID, SESSION and SessionAwareAuthResultInterface since 2.9.0.
The session reference of a stored-token result is the SHA-256 hash of the presented token; for a signed access token it is the sid (hash of the refresh token); SessionManager::invalidateByReference() revokes exactly that session's refresh token. Any other place that needs the hash of a plain token uses the one function HashedToken::hashOf(string $plainToken): string — never hash('sha256', …) in the consumer. Subject::parse('user:42') is the inverse of Subject::toString().
Password Hashing
// Argon2id (default, recommended) $hasher = PasswordHasher::argon2id(memoryCost: 65536, timeCost: 4, threads: 1); // Bcrypt (fallback) $hasher = PasswordHasher::bcrypt(cost: 12); // Default constructor uses Argon2id $hasher = new PasswordHasher();
Error Handling
| Exception | When |
|---|---|
AuthenticationException |
Authentication failed (base class) |
TokenExpiredException |
Token has expired |
TokenRevokedException |
Token was revoked |
InvalidCredentialException |
Invalid credentials provided |
UnauthorizedException |
Insufficient permissions (RBAC) |
use JardisSupport\Auth\Exception\TokenExpiredException; use JardisSupport\Auth\Exception\UnauthorizedException; try { $verifier($token, $storedToken, TokenType::Access); } catch (TokenExpiredException $e) { // Token expired — client should use refresh token } try { $guard->authorize($session, 'admin:delete'); } catch (UnauthorizedException $e) { // Access denied }
Architecture
The user sees nine orchestrators. Internally, each delegates to invokable handlers:
SessionManager (Orchestrator)
├── Handler/Session/CreateSession store refresh token, sign access token (sid = refresh hash) + SessionCreated event
├── Handler/Session/RefreshSession rotate tokens + SessionRefreshed event
├── Handler/Session/InvalidateSession revoke single session + SessionInvalidated event
├── Handler/Session/InvalidateSessionByReference revoke the session's refresh token (sid)
├── Handler/Token/SignAccessToken payload → signed access token
├── Handler/Token/VerifyAccessToken signed access token → payload (MAC, typ, exp) or null
└── Handler/Session/InvalidateAllSessions revoke all + AllSessionsInvalidated event
PasswordAuthenticator (Orchestrator)
├── Handler/Authentication/LookupUser resolve user via $userLookup closure
├── Handler/Authentication/LookupCredentialRecord resolve user via UserLookupInterface
├── Handler/Authentication/VerifyCredential verify password against hash
└── Handler/Authentication/BuildAuthResult assemble AuthenticationResult + events
SignedTokenAuthenticator (Orchestrator)
└── Handler/Authentication/AuthenticateSignedToken verify signature + exp → subject + tenant claim + sid → TenantAuthResult
TokenAuthenticator (Orchestrator)
└── Handler/Authentication/AuthenticateToken lookup → verify → subject + subject id + tenant claim + session reference → TenantAuthResult
TokenIssuer (Orchestrator)
└── Handler/Token/IssueToken create token, store hash → IssuedToken
TokenRedeemer (Orchestrator)
├── Handler/Token/PeekToken find → verify (type, expiry, revocation) → stored token, no revoke
└── Handler/Token/RedeemToken find → verify (type, expiry, revocation) → revoke → stored token
AuthServices (bundle, no logic)
└── sessions() · tokenIssuer() · tokenRedeemer() · hasher() — fromStore() builds the defaults
DbTokenStore (Orchestrator)
├── Handler/TokenStore/StoreToken · FindToken · RevokeToken · RevokeSubjectTokens · DeleteExpiredTokens
└── Handler/TokenStore/ExecuteQuery dbquery builder → prepared statement on the connection
PasswordHasher (Orchestrator)
├── Handler/Password/HashPassword hash via password_hash()
├── Handler/Password/VerifyPassword verify via password_verify()
└── Handler/Password/CheckRehash check via password_needs_rehash()
Rights check (closure, used without Guard)
└── Handler/Authorization/HasRight right in a list of granted rights (`Permission::matches`)
Guard (Orchestrator)
├── Handler/Authorization/CheckPermission check role(s) against policy
└── Handler/Authorization/AuthorizePermission check + throw on failure
Data (Value Objects, Enums, Builder, Events)
├── Token, HashedToken, IssuedToken, TokenType
├── Session, SessionResult
├── Subject, Credential, CredentialType, AuthResult, AuthenticationResult
├── Permission, Policy, PolicyBuilder
└── Event/ (AuthenticationSucceeded, AuthenticationFailed, SessionCreated,
SessionRefreshed, SessionInvalidated, AllSessionsInvalidated)
Each handler is an invokable object (__invoke) — independently testable, replaceable, composable. The orchestrators contain no business logic, only delegation.
Test Structure
Tests mirror the src/ directory:
tests/Integration/
├── GuardTest.php ← src/Guard.php
├── SessionManagerTest.php ← src/SessionManager.php
├── PasswordHasherTest.php ← src/PasswordHasher.php
├── PasswordAuthenticatorTest.php ← src/PasswordAuthenticator.php
├── TokenIssuerTest.php ← src/TokenIssuer.php
├── TokenRedeemerTest.php ← src/TokenRedeemer.php
├── AuthContractsTest.php ← contracts 2.11 interfaces, AuthServices, peek, role
├── SessionExpiryContractTest.php ← SessionInterface::getExpiresAt() (v1.7)
├── Data/
│ ├── AuthResultTest.php ← src/Data/AuthResult.php
│ ├── CredentialTest.php ← src/Data/Credential.php
│ ├── SubjectTest.php ← src/Data/Subject.php
│ ├── PermissionTest.php ← src/Data/Permission.php
│ ├── PolicyTest.php ← src/Data/Policy.php
│ ├── TokenTest.php ← src/Data/Token.php
│ └── HashedTokenTest.php ← src/Data/HashedToken.php
├── RightsClaimTest.php ← claim `rights` → TenantAuthResult::getRights()
├── Handler/Authorization/
│ └── HasRightTest.php ← src/Handler/Authorization/HasRight.php
├── Handler/Token/
│ └── VerifyTokenTest.php ← src/Handler/Token/VerifyToken.php
└── Support/
└── InMemoryTokenStoreTest.php ← tests/Support/InMemoryTokenStore.php
Contracts
Requires jardissupport/contracts ^2.16 and jardissupport/dbquery ^1.1. Defined in jardissupport/contracts:
| Interface | Purpose |
|---|---|
TokenStoreInterface |
Token persistence: store, find, revoke, deleteExpired |
PasswordHasherInterface |
Hash, verify, needsRehash |
GuardInterface |
Permission check + authorize |
AuthenticatorInterface |
Authenticate credentials, return AuthResult |
TenantAwareAuthResultInterface |
AuthResult plus getTenant() |
SessionAwareAuthResultInterface |
TenantAware plus getSubjectId() and getSessionReference() (returned by SignedTokenAuthenticator and TokenAuthenticator) |
UserLookupInterface |
lookup(string $identifier): ?CredentialRecordInterface — login lookup port, accepted by PasswordAuthenticator |
CredentialRecordInterface |
getPasswordHash(), getSubject() (type:id), getClaims() |
SessionClaim |
Claim names TENANT / ROLE / RIGHTS (default of TokenAuthenticator, key read by Guard) |
RequestAttribute |
Request attribute names SUBJECT / SUBJECT_ID / TENANT / SESSION / ROLE / RIGHTS for the authenticated session |
SessionManagerInterface |
create, refresh, invalidateByReference, invalidateAll — implemented by SessionManager |
TokenIssuerInterface / TokenRedeemerInterface |
issue / peek, redeem — implemented by TokenIssuer / TokenRedeemer |
SessionResultInterface / IssuedTokenInterface |
getters (getAccessToken, getRefreshToken, getSession / getPlain, getHashed) — implemented by SessionResult / IssuedToken |
RoleAwareAuthResultInterface |
getRole(): ?string — implemented by TenantAuthResult |
RightsAwareAuthResultInterface |
getRights(): list<string> — implemented by TenantAuthResult |
AuthServicesInterface |
sessions(), tokenIssuer(), tokenRedeemer(), hasher() — implemented by AuthServices |
Interfaces (contracts 2.11)
The services implement the contract interfaces, so a kernel can wire and swap them without knowing the classes:
- String subjects.
SessionManagerInterface::create()andTokenIssuerInterface::issue()take the qualified subject stringtype:id(Subject::parse; a bare id isuser:<id>). The classes additionally still accept aSubjectobject, so existing callers keep working. - Session expiry (v1.7, contracts 2.12).
SessionInterface::getExpiresAt(): ?DateTimeImmutable(null= does not expire) is implemented byData\Sessionand returns the publicexpiresAtproperty, which stays unchanged. A session fromSessionManager::create()/refresh()carries the expiry of its signed access token (exp). Consumers read the expiry through the contract, never from the class. peek()on the redeemer: see "Issue and redeem one-time tokens".- Role. The claim
SessionClaim::ROLE(role) of a signed token or stored token surfaces asTenantAuthResult::getRole()(nullwhen the claim is absent or not a non-empty string);TenantAuthResultimplementsRoleAwareAuthResultInterface. - Rights (contracts 2.16). The claim
SessionClaim::RIGHTS(rights) of a signed token or stored token surfaces asTenantAuthResult::getRights(); only a list of strings counts, anything else (missing, string, map, mixed list) is[].TenantAuthResult::success(…)takes the rights as its last argument (default[]) and implementsRightsAwareAuthResultInterface. Elements areresource:action,resource:*or*. The check is the closureHandler\Authorization\HasRight:
use JardisSupport\Auth\Handler\Authorization\HasRight; $hasRight = new HasRight(); $hasRight($result->getRights(), 'sales.process:approve'); // bool — '*' grants all, 'sales.process:*' grants the resource // an unparsable right throws InvalidArgumentException (Permission::from)
AuthServicesbundles the four services:
use JardisSupport\Auth\AuthServices; $auth = AuthServices::fromStore($tokenStore, $accessTokenKey); // 3rd argument: ?PasswordHasherInterface (default Argon2id) $auth->sessions()->create('user:42', ['tenant' => 'acme', 'role' => 'admin']); $auth->tokenIssuer()->issue(TokenType::Invitation, 'user:42', 3600); $auth->tokenRedeemer()->peek($plain, TokenType::Invitation); $auth->hasher()->hash($password); // or wire your own services: new AuthServices($sessions, $issuer, $redeemer, $hasher)
fromStore() uses the package defaults and reads no environment — key and store come from the caller. The class-level SessionResult/IssuedToken keep their public properties and add the interface getters; PasswordAuthenticator takes any SessionManagerInterface.
The auth exceptions (InvalidCredentialException, TokenExpiredException, TokenRevokedException) are documented in contracts as prose only.
Kernel Integration
Auth is wired directly — constructor injection, no framework hook. DomainKernel (the Koffer in jardiscore/kernel) exposes 11 accessors for cross-cutting infrastructure (cache, logger, database, HTTP client, mailer, filesystem, event dispatcher/registry), but no auth(): Auth is a support package you instantiate and wire yourself in your bounded context.
(Earlier docs described this as "no service hook in DomainApp" — DomainApp was removed in the Kernel-Entkopplung refactor. The fact is unchanged, only the vocabulary: today's Koffer is DomainKernel, consumed by the generated {Domain}Context.)
- TokenStore:
DbTokenStoreon the DomainKernel database connection, or implementTokenStoreInterfaceyourself (Redis, ...) - Policy: Define as value object in application layer
- Guard: Instantiate in application layer, inject Policy
ENV Variables (optional)
This package does not read the process environment itself — no getenv/$_ENV/DotEnv call exists in src/. The names below are a suggested convention for values you read yourself in the consuming application and pass explicitly into the constructors/methods (e.g. AUTH_HASH_ALGO → the $algorithm argument of PasswordHasher, AUTH_TOKEN_LENGTH → the length argument when generating a Token).
# Password Hashing AUTH_HASH_ALGO=argon2id AUTH_HASH_MEMORY=65536 AUTH_HASH_TIME=4 AUTH_HASH_THREADS=1 # Token Defaults AUTH_TOKEN_LENGTH=32 AUTH_ACCESS_TOKEN_TTL=900 # 32 bytes, base64 (e.g. `openssl rand -base64 32`); decode before handing to SessionManager AUTH_TOKEN_KEY= AUTH_REFRESH_TOKEN_TTL=604800
What This Package Does NOT Do
- No JWT — opaque tokens only. JWT comes in v2 at the earliest
- No OAuth2/OIDC — no authorization server, no PKCE
- No HTTP layer — no cookies, no middleware, no
session_start() - No user management — no user model, no registration flow
- No rate limiting — brute-force protection is infrastructure concern
- No other token persistence than SQL —
DbTokenStorecovers MySQL/MariaDB, PostgreSQL, SQLite; anything else: implementTokenStoreInterface - No event dispatching — events are returned to the caller, not dispatched internally
Development
cp .env.example .env # One-time setup make install # Install dependencies make phpunit # Run tests (starts a MariaDB 11.8 service, reachable inside the Compose network only) make phpstan # Static analysis (Level 8) make phpcs # Coding standards (PSR-12)
Documentation
Full documentation, guides, and API reference:
docs.jardis.io/en/support/auth
License
MIT License — free for any use, including commercial.
AI-Assisted Development
This package ships with a skill for Claude Code, Cursor, Continue, and Aider. Install it in your consuming project:
composer require --dev jardis/dev-skills
More details: https://docs.jardis.io/en/skills