Token management, session handling, and security utilities for authentication and authorization

Maintainers

Package info

github.com/jardisSupport/auth

pkg:composer/jardissupport/auth

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-04-05 11:28 UTC

This package is auto-updated.

Last update: 2026-04-05 11:34:41 UTC


README

Build Status License: PolyForm Shield PHP Version PHPStan Level PSR-12

Part of the Jardis Business Platform — Enterprise-grade PHP components for Domain-Driven Design

Authentication and authorization without framework coupling. Opaque tokens, session management, password hashing, and role-based access control — designed for DDD applications. No HTTP layer, no JWT, no external dependencies. Pure support package.

Why This Package?

  • Four classes to learnSessionManager, PasswordHasher, Guard, PasswordAuthenticator. 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
  • Zero external dependencies — uses PHP built-ins: password_hash, random_bytes, hash_hmac, hash_equals

Installation

composer require jardissupport/auth

Quick Start

Create a Session

use JardisSupport\Auth\SessionManager;
use JardisSupport\Auth\Data\Subject;

$sessionManager = new SessionManager($tokenStore);
$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\Handler\Token\VerifyToken;
use JardisSupport\Auth\Data\TokenType;

// Verify an access token
$verifier = new VerifyToken();
$hash = hash('sha256', $accessToken);
$stored = $tokenStore->find($hash);
$verifier($accessToken, $stored, TokenType::Access);
// throws TokenExpiredException or TokenRevokedException

// 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);
}

Invalidate Sessions

// Single session (logout) — returns SessionInvalidated event
$event = $sessionManager->invalidate($session);

// All sessions for a subject (logout everywhere) — returns AllSessionsInvalidated event
$event = $sessionManager->invalidateAll('user:user-42');

Token Store

The package defines TokenStoreInterface — you implement it in your infrastructure layer:

use JardisSupport\Contract\Auth\TokenStoreInterface;
use JardisSupport\Auth\Data\HashedToken;

class DatabaseTokenStore implements TokenStoreInterface
{
    public function __construct(private PDO $pdo) {}

    public function store(HashedToken $token): void { /* INSERT */ }
    public function find(string $hash): ?HashedToken { /* SELECT */ }
    public function revoke(string $hash): void { /* UPDATE revoked = true */ }
    public function revokeAllForSubject(string $subject): void { /* UPDATE WHERE subject = ? */ }
    public function deleteExpired(): int { /* DELETE WHERE expires_at < NOW() */ }
}

An InMemoryTokenStore is included in tests/Support/ for testing.

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 four orchestrators. Internally, each delegates to invokable handlers:

SessionManager (Orchestrator)
  ├── Handler/Session/CreateSession         create session + token pair + SessionCreated event
  ├── Handler/Session/RefreshSession        rotate tokens + SessionRefreshed event
  ├── Handler/Session/InvalidateSession     revoke single session + SessionInvalidated event
  └── Handler/Session/InvalidateAllSessions revoke all + AllSessionsInvalidated event

PasswordAuthenticator (Orchestrator)
  ├── Handler/Authentication/LookupUser      resolve user via $userLookup closure
  ├── Handler/Authentication/VerifyCredential verify password against hash
  └── Handler/Authentication/BuildAuthResult  assemble AuthenticationResult + events

PasswordHasher (Orchestrator)
  ├── Handler/Password/HashPassword         hash via password_hash()
  ├── Handler/Password/VerifyPassword       verify via password_verify()
  └── Handler/Password/CheckRehash          check via password_needs_rehash()

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, 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
├── 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
├── Handler/Token/
│   └── VerifyTokenTest.php                ← src/Handler/Token/VerifyToken.php
└── Support/
    └── InMemoryTokenStoreTest.php         ← tests/Support/InMemoryTokenStore.php

Contracts

Defined in jardissupport/contract — implement these in your infrastructure:

Interface Purpose
TokenStoreInterface Token persistence: store, find, revoke, deleteExpired
PasswordHasherInterface Hash, verify, needsRehash
GuardInterface Permission check + authorize
AuthenticatorInterface Authenticate credentials, return AuthResult

Foundation Integration

Auth is a support package — no service hook in DomainApp. Integration happens in your bounded context:

  • TokenStore: Implement in infrastructure (database, Redis)
  • Policy: Define as value object in application layer
  • Guard: Instantiate in application layer, inject Policy

ENV Variables (optional)

# 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=3600
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 token persistence — only the interface. You implement the store
  • 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
make phpstan             # Static analysis (Level 8)
make phpcs               # Coding standards (PSR-12)

License

PolyForm Shield License 1.0.0 — free for all use including commercial. Only restriction: don't build a competing framework.