blueids / mak-jwt-middleware
JWT stateless authentication middleware for MAK ERP Hospitalier
0.1.0
2026-03-25 15:14 UTC
Requires
- php: ^8.3
- firebase/php-jwt: *
- illuminate/cache: ^13
- illuminate/contracts: ^13
- illuminate/http: ^13
- illuminate/support: ^13
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^11
- phpunit/phpunit: ^11
README
Package Composer partagé pour l'authentification JWT stateless dans MAK ERP Hospitalier.
Architecture
JWT Stateless Authentication
├── Validation locale (pas d'appel HTTP vers Core)
├── Payload : { user_id, hospital_id, roles[], permissions[], jti, exp }
├── Blacklist Redis avec TTL
├── Clé partagée APP_JWT_SECRET (HS256)
└── Isolation automatique par hospital_id
Installation
Via Composer (recommandé)
# Dans chaque service Laravel (mak-core, mak-patient, mak-finance)
composer require mak/jwt-middleware
Configuration
- Publier la configuration :
php artisan vendor:publish --provider="MAK\JwtMiddleware\JwtMiddlewareServiceProvider" --tag="jwt-middleware-config"
- Variables d'environnement (dans
.env) :
APP_JWT_SECRET=votre-clé-256-bit-minimum-64-caractères-pour-HS256 APP_JWT_TTL=480 # 8 heures (journée de travail) APP_JWT_REFRESH_TTL=43200 # 30 jours
Utilisation
1. Protection des routes
// routes/api.php use Illuminate\Support\Facades\Route; Route::middleware(['jwt.auth'])->group(function () { Route::get('/profile', [UserController::class, 'profile']); // Avec vérification de permission Route::middleware(['jwt.permission:patient.create'])->group(function () { Route::post('/patients', [PatientController::class, 'store']); Route::put('/patients/{id}', [PatientController::class, 'update']); }); Route::middleware(['jwt.permission:finance.facture.annuler'])->group(function () { Route::delete('/factures/{id}', [FactureController::class, 'annuler']); }); });
2. Accès aux données utilisateur
// Dans un Controller public function profile(Request $request) { $user = $request->user(); return response()->json([ 'user_id' => $user->id, 'hospital_id' => $user->hospital_id, 'roles' => $user->roles, 'permissions' => $user->permissions, ]); } // Vérification de permission dans le code public function createPatient(Request $request) { if (!$request->user()->can('patient.create')) { abort(403, 'Permission insuffisante'); } // Logique métier... }
3. Login/Logout (dans mak-core)
use MAK\JwtMiddleware\Services\JwtService; class AuthController extends Controller { public function login(LoginRequest $request, JwtService $jwtService) { // Validation des credentials... $user = User::findByEmail($request->email); $tokenData = [ 'user_id' => $user->id, 'hospital_id' => $user->hospital_id, 'roles' => $user->roles->pluck('name')->toArray(), 'permissions' => $user->getAllPermissions()->pluck('name')->toArray(), ]; $accessToken = $jwtService->generateToken($tokenData); $refreshToken = $jwtService->generateRefreshToken($tokenData); return response()->json([ 'access_token' => $accessToken, 'refresh_token' => $refreshToken, 'token_type' => 'Bearer', 'expires_in' => config('jwt-middleware.ttl') * 60, ]); } public function logout(Request $request, JwtService $jwtService) { $token = $request->bearerToken(); $jti = $jwtService->extractJti($token); if ($jti) { $jwtService->blacklistToken($jti); } return response()->json(['message' => 'Déconnexion réussie']); } public function refresh(Request $request, JwtService $jwtService) { $refreshToken = $request->input('refresh_token'); // Recharger les données utilisateur depuis la DB $user = User::find($request->user()->id); $userData = [ 'user_id' => $user->id, 'hospital_id' => $user->hospital_id, 'roles' => $user->roles->pluck('name')->toArray(), 'permissions' => $user->getAllPermissions()->pluck('name')->toArray(), ]; $tokens = $jwtService->refreshToken($refreshToken, $userData); return response()->json($tokens); } }
Isolation par hôpital
Le middleware applique automatiquement l'isolation par hospital_id :
// Dans un Model (ex: Patient) protected static function booted(): void { static::addGlobalScope(new HospitalScope()); } // HospitalScope utilise automatiquement hospital_id du JWT class HospitalScope implements Scope { public function apply(Builder $builder, Model $model): void { $hospitalId = app(HospitalContext::class)->getId(); $builder->where('hospital_id', $hospitalId); } }
Gestion des erreurs
Le middleware retourne des réponses JSON structurées :
// Token manquant { "message": "Token manquant", "code": "TOKEN_MISSING", "status": 401 } // Token expiré { "message": "Token expiré", "code": "TOKEN_EXPIRED", "status": 401 } // Permission insuffisante { "message": "Permission insuffisante", "code": "INSUFFICIENT_PERMISSIONS", "status": 403, "required_permission": "patient.create" }
Tests
# Exécuter les tests du package vendor/bin/phpunit # Tests spécifiques vendor/bin/phpunit tests/JwtServiceTest.php
Sécurité
Clé secrète
- Minimum 64 caractères pour HS256
- Identique dans tous les services
- Stockée dans les variables d'environnement
- Changée régulièrement en production
Blacklist Redis
- TTL automatique basé sur la durée restante du token
- Fail-safe : en cas d'erreur Redis, les tokens sont considérés valides
- Nettoyage automatique à l'expiration
Payload JWT
- JTI unique (JWT ID) pour chaque token
- Expiration stricte (pas de grâce période)
- Claims obligatoires : user_id, hospital_id, roles, permissions
Développement
Structure du package
mak-jwt-middleware/
├── src/
│ ├── JwtMiddlewareServiceProvider.php
│ ├── Services/
│ │ └── JwtService.php
│ ├── Middleware/
│ │ ├── JwtAuthenticate.php
│ │ └── CheckPermission.php
│ ├── Exceptions/
│ │ └── JwtExceptions.php
│ └── Context/
│ └── HospitalContext.php
├── config/
│ └── jwt-middleware.php
└── tests/
└── JwtServiceTest.php
Commandes de développement
# Installation des dépendances composer install # Tests composer test # Analyse statique (si configuré) composer analyse # Formatage du code composer format
Dépannage
Token rejeté avec "Signature invalide"
Cause : Clé secrète différente entre services
Solution : Vérifier APP_JWT_SECRET identique partout
Token blacklisté après logout
Cause : Token révoqué dans Redis Solution : Se reconnecter pour obtenir un nouveau token
Erreur "Hospital ID non défini"
Cause : Contexte CLI sans hospital_id configuré
Solution : Définir HOSPITAL_ID=1 dans .env
Performance Redis
Cause : Latence réseau élevée vers Redis Solution : Vérifier connexion Redis et monitoring
MAK ERP Hospitalier © 2026# mak-jwt-middleware