anastosios / firauth
JWT + Cookie Auth Package
Installs: 367
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/anastosios/firauth
Requires
- php: ^7.3|^8.0
- laravel/framework: ^8|^9|^10|^11
README
FirAuth provides a clean, extensible authentication layer for multi‑service Laravel setups:
- JWT (RS256 recommended) — sign on the main service, verify on consumer services
- Cookie or Bearer transport — HttpOnly cookie across subdomains, or
Authorization: Bearer - Single‑session via Redis —
session_idclaim validated across services - Pre / Post checkers — plug custom gates (integration credentials, password reset, MFA, timezone, …)
- Drop‑in routes —
/firauth/login,/firauth/refresh,/firauth/logout - Stateless on consumers —
request()->user()is built from JWT claims (no DB hit)
Package namespace used below:
cs/auth(PSR‑4 root:Firauth\auth\).
Adjust to your vendor/name if you publish under another vendor on Packagist.
Table of Contents
- Requirements
- Installation
- Configuration
- Quick Start
- Routes \u0026 Middlewares
- Overriding only the Login action (best practice)
- Pre / Post Checkers
- Refresh Best Practice
- Logout Behavior
- Testing Recipes
- Postman Tips
- Troubleshooting
- Security Notes
- Optional Artisan Commands
- License
Requirements
- PHP 8.0+ (Laravel 8/9/10/11 supported)
- Redis (recommended for single‑session)
- tymon/jwt-auth (used internally)
- OpenSSL if using RS256
Installation
composer require anastosios/firauth # Publish config php artisan vendor:publish --tag=firauth-config # Interactive setup (main vs consumer, cookie/jwt, keys, redis, etc.) php artisan firauth:install
The installer will ask you:
- Is this the MAIN service? (issues tokens and exposes /firauth routes)
- Strategy:
cookie(HttpOnly) orjwt - Redis connection TTL (seconds) for sessions
- JWT: RS256 (preferred) or HS256, along with TTLs and key paths
Recommendation: Use RS256 for multi‑service , private key on main only, public key on consumers.
Configuration
Environment variables quick reference
# Core FIRAUTH_MAIN_SERVICE=true|false FIRAUTH_STRATEGY=cookie|jwt # Cookie FIRAUTH_COOKIE_NAME=FIRAUTH_token FIRAUTH_COOKIE_DOMAIN=.your-domain.com FIRAUTH_COOKIE_SECURE=true FIRAUTH_COOKIE_HTTP_ONLY=true FIRAUTH_COOKIE_SAMESITE=Lax # Lax|Strict|None (use None with HTTPS only) FIRAUTH_COOKIE_LIFETIME=60 # minutes # Redis session binding FIRAUTH_SESSION_REDIS=session FIRAUTH_SESSION_KEY_PREFIX=session_ FIRAUTH_SESSION_TTL=0 # 0=no expiry at Redis level # JWT behavior FIRAUTH_REMEMBER_DAYS=90 FIRAUTH_RENEW_WINDOW=600 # refresh only if <= 10m remaining (or expired) FIRAUTH_USE_MODEL_CLAIMS=true # use getJWTCustomClaims() from your User model FIRAUTH_BIND_SESSION=false # bind Redis session at login automatically # Route options FIRAUTH_ROUTES_PREFIX=firauth FIRAUTH_ROUTES_EXPOSE_LOGIN=true FIRAUTH_ROUTES_EXPOSE_REFRESH=true FIRAUTH_ROUTES_EXPOSE_LOGOUT=true # JWT (tymon) JWT_ALGO=RS256 # RS256 recommended (or HS256) JWT_TTL=60 # minutes JWT_REFRESH_TTL=20160 # minutes (14 days) JWT_BLACKLIST_ENABLED=true JWT_BLACKLIST_GRACE_PERIOD=30 JWT_PASSPHRASE= # if your private key is encrypted # For RS256 JWT_PUBLIC_KEY="file:///absolute/path/to/public.pem" JWT_PRIVATE_KEY="file:///absolute/path/to/private.pem" # For HS256 (alternative) JWT_SECRET=change_me_please
Tip: For local dev cookie tests, map a host in
/etc/hosts(e.g.,127.0.0.1 api.your-domain.test) and useFIRAUTH_COOKIE_DOMAIN=.your-domain.testso the browser/Postman attaches cookies correctly.
JWT (RS256 vs HS256)
- RS256 (recommended): sign with private key on the main service; verify with public key on all consumers. Keys are read via
file://URIs. - HS256: shared secret across services (simpler but less ideal for multi‑service separation).
Cookie transport
When FIRAUTH_STRATEGY=cookie the package sets an HttpOnly cookie named firauth_token. Configure domain/samesite/secure flags in config/env. Consumers under subdomains will automatically receive the cookie.
The middleware prefers the cookie in cookie strategy; otherwise it falls back to the
Authorization: Bearerheader.
Redis session binding
Single‑session across services is enforced by a session_id claim bound to Redis:
- On login, a
session_idis included in the JWT (either from yourgetJWTCustomClaims()or generated). - Middleware validates
session_idagainst Redis (FIRAUTH_SESSION_REDISFIRAUTH_SESSION_KEY_PREFIX). - Logout revokes the Redis entry and blacklists the token.
Routes toggles
You can selectively expose package routes:
FIRAUTH_ROUTES_EXPOSE_LOGIN=true|false FIRAUTH_ROUTES_EXPOSE_REFRESH=true|false FIRAUTH_ROUTES_EXPOSE_LOGOUT=true|false
Disable only login to override it in your app while keeping refresh/logout from the package.
Quick Start
Main service
- Run the installer and choose Main service = yes.
- Set RS256 keys using
file://paths in.env(or use HS256 withJWT_SECRET). - If using cookies, configure
FIRAUTH_COOKIE_DOMAINfor your apex/subdomains. - Test login:
curl -i -X POST https://api.your-domain.com/firauth/login \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com","password":"secret"}'
You should see a JSON response and aSet-Cookie: firauth_token=...header (cookie strategy).
Consumer services
- Run the installer and choose Main service = no.
- Configure only the public key for RS256 (
JWT_PUBLIC_KEY). - Protect routes with the middleware:
Route::middleware('firauth')->group(function () { Route::get('/me', fn() => response()->json(request()->user())); // your protected routes… });
The middleware will verify signature/expiry/nbf/blacklist, validate the Redis session_id, and expose a dynamic request()->user() built from claims (no DB hit).
Routes & Middlewares
Routes (package):
POST /firauth/login— issues the JWT (+Set-Cookiein cookie mode)POST /firauth/refresh— refreshes token; middleware:firauth.token(token presence; accepts expired token)POST /firauth/logout— blacklists current token revokes Redis session; middleware:firauth(full check)
Middlewares:
firauth— Full verification (signature, expiry, blacklist) Redissession_idvalidation; bindsrequest()->user()from claims.firauth.token— Helper that injects cookie token into theAuthorizationheader if missing (used for/refresh).
Overriding only the Login action (best practice)
You can replace only the login endpoint and reuse the package’s services:
-
Disable the package login route in
.env:FIRAUTH_ROUTES_EXPOSE_LOGIN=false
-
Create a subclass of the base controller and override
login():// app/Http/Controllers/Auth/CustomAuthController.php namespace App\Http\Controllers\Auth; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Firauth\auth\Http\Controllers\AuthController as BaseAuthController; class CustomAuthController extends BaseAuthController { public function login(Request $request) { $request->validate([ 'email' => 'required|string', 'password' => 'required|string', 'remember' => 'sometimes|boolean', ]); $rememberDays = $request->boolean('remember') ? (int) config('firauth.jwt.remember_me_days') : null; // Reuse token driver (no direct Tymon calls here) $result = $this->tokens->attempt( $request->only('email','password'), $rememberDays ); if (!$result) { throw ValidationException::withMessages(['email' => ['Invalid credentials']])->status(422); } $claims = $result['claims'] ?? []; $user = $result['user']; $sessionId = $claims['session_id'] ?? null; if (config('firauth.jwt.bind_session_in_login', false) && $sessionId) { $this->sessions->bind($user['id'], $sessionId); } // Use the transport to emit JSON + optional Set-Cookie return $this->transport->loginResponse($result['token'], $user); } }
-
Register only your login route (keep refresh/logout from the package):
// routes/api.php use App\Http\Controllers\Auth\CustomAuthController; Route::post('firauth/login', [CustomAuthController::class, 'login']);
Pre / Post Checkers
Use checkers to inject custom logic without touching core auth:
-
Pre checkers (run first) — may short‑circuit or bypass JWT by returning user attributes.
If a pre checker returns an array, we treat the user as authenticated and skip everything else by default.
If that case still needs post checks, include['_run_post' => true]in the returned array. -
Post checkers (run last) — final gate after JWT \u002b Redis session validation (or pre opted‑in).
Perfect for: password‑reset gates (423), tenant disabled, timezone binding, MFA after JWT, etc.
Return aResponseto block, ornullto allow; you may mutate the$userAttrsarray by reference.
Contracts:
// Pre — return Response|array|null interface PreAuthCheckerInterface { public function handle(Request $request): \Symfony\Component\HttpFoundation\Response|array|null; } // Post — return ?Response; can mutate user attrs by reference interface PostAuthCheckerInterface { public function handle(Request $request, array &$userAttrs): ?\Symfony\Component\HttpFoundation\Response; }
Register your checkers in config/firauth.php:
'middleware' => [ 'pre' => [ // \App\FirAuth\Checkers\IntegrationCredentialsPreChecker::class, ], 'post' => [ // \App\FirAuth\Checkers\ForcePasswordResetPostChecker::class, // \App\FirAuth\Checkers\TimezoneBinderPostChecker::class, ], ],
Refresh Best Practice
- Set a window to refresh only when near expiry or expired:
FIRAUTH_RENEW_WINDOW=600 # 10 minutes
- Behavior at
/firauth/refresh:- If token far from expiry →
{ "still_valid": true, "expires_in": <seconds> } - If expired or within window → a new token is issued; the old is blacklisted immediately.
- If token far from expiry →
- To reduce races during refresh (parallel requests):
JWT_BLACKLIST_GRACE_PERIOD=30
Logout Behavior
POST /firauth/logout(middleware:firauthfull check):- Blacklists the current token.
- Revokes the Redis session entry → global single‑session kill.
- Returns 200 (cookie cleared if cookie strategy).
- A second logout with the same token returns 401 (blacklisted \u002b no session).
Testing Recipes
JWT (header)
# Login (get token) TOKEN=$(curl -s -X POST https://api.your-domain.com/firauth/login \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com","password":"secret"}' | jq -r '.data.token') # Protected route curl -H "Authorization: Bearer $TOKEN" https://api.your-domain.com/protected # Refresh curl -X POST -H "Authorization: Bearer $TOKEN" https://api.your-domain.com/firauth/refresh # Logout curl -X POST -H "Authorization: Bearer $TOKEN" https://api.your-domain.com/firauth/logout
Cookie
# Let curl manage cookies automatically (-c to save, -b to send) curl -c cookies.txt -b cookies.txt -i -X POST https://api.your-domain.com/firauth/login \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com","password":"secret"}' # Refresh / protected / logout curl -c cookies.txt -b cookies.txt -i -X POST https://api.your-domain.com/firauth/refresh curl -c cookies.txt -b cookies.txt -i https://api.your-domain.com/protected curl -c cookies.txt -b cookies.txt -i -X POST https://api.your-domain.com/firauth/logout
Feature tests (Laravel)
- Login returns token (or sets cookie) \u002b payload has
exp - Refresh near expiry issues a new token; far from expiry returns
still_valid - Logout revokes session and blacklists token; second logout is 401
Postman Tips
- Do not paste the full
Set‑Cookieheader as yourCookieheader; send onlyCookie: firauth_token=<TOKEN>. - In cookie mode, prefer using a host that matches
FIRAUTH_COOKIE_DOMAIN(e.g.,api.your-domain.test) so Postman attaches cookies automatically. - If both cookie and
Authorizationheader are present, cookie is preferred in cookie strategy (package behavior). Keep yourAuthorizationheader clean to avoid sending stale tokens.
What’s Next (Roadmap)
1. Pluggable Session Stores (Not Only Redis)
Make the session binding layer driver-based so teams can choose from:
- Redis (default)
- Database
- File
- In-memory / Array (for testing)
- Custom-defined stores
Goal: Decouple session handling from Redis to allow for flexible and testable implementations.
2. Multi-Device Sessions & Revocation UX
Support both single-session (default today) and multi-session modes.
Modes
single: One activesession_idper user (current behavior).multi: Track a set of session IDs per user.revoke(user, session_id)→ removes one devicerevoke(user, null)→ removes all sessions for the user
Claims
Add optional claims:
device_idclient metadata(OS, browser, device info)
APIs (optional)
GET /firauth/sessions→ List all active devicesDELETE /firauth/sessions/{session_id}→ Revoke specific device
⚙ Dev Experience
- Artisan command:
php artisan firauth:sessions {userId}
3. Pre-Built SSO Adapters (as Pre-Checkers)
Ship optional pre-auth adapters for popular identity providers:
- OAuth2 / OIDC flows:
- Microsoft
- GitHub.