testing
This commit is contained in:
@@ -71,4 +71,5 @@ EVEONLINE_REDIRECT_URL=replace_this_with_eve_developer_redirect_uri
|
||||
JWT_SECRET=replace_this_with_a_long_random_secret
|
||||
JWT_TTL=3600
|
||||
JWT_ISSUER="${APP_NAME}"
|
||||
JWT_REFRESH_INTERVAL=3600
|
||||
|
||||
|
||||
35
app/Console/Commands/RefreshUserJwts.php
Normal file
35
app/Console/Commands/RefreshUserJwts.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\JwtService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RefreshUserJwts extends Command
|
||||
{
|
||||
protected $signature = 'app:refresh-user-jwts';
|
||||
protected $description = 'Refresh user JWTs that are older than the configured refresh interval.';
|
||||
|
||||
public function handle(JwtService $jwtService): int
|
||||
{
|
||||
$refreshInterval = (int) config('jwt.refresh_interval', 3600);
|
||||
$threshold = now()->subSeconds($refreshInterval);
|
||||
|
||||
User::query()
|
||||
->where(function ($query) use ($threshold) {
|
||||
$query->whereNull('user_jwt')
|
||||
->orWhereNull('user_jwt_issued_at')
|
||||
->orWhere('user_jwt_issued_at', '<=', $threshold);
|
||||
})
|
||||
->chunkById(100, function ($users) use ($jwtService) {
|
||||
foreach ($users as $user) {
|
||||
$jwtService->forceRefresh($user);
|
||||
}
|
||||
});
|
||||
|
||||
$this->info('Stale user JWTs refreshed successfully.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -29,33 +29,25 @@ class EveLoginController extends Controller
|
||||
return Socialite::driver('eveonline')->scopes(['publicData'])->redirect();
|
||||
}
|
||||
|
||||
public function handleProviderCallback(Request $request): RedirectResponse
|
||||
public function handleProviderCallback(Request $request, JwtService $jwtService): RedirectResponse
|
||||
{
|
||||
try {
|
||||
// Stateless can help in some deployments, but keep stateful by default.
|
||||
// If you run into "Invalid state" issues behind proxies, switch to ->stateless()
|
||||
$ssoUser = Socialite::driver('eveonline')->user();
|
||||
Debugbar::warning($ssoUser);
|
||||
|
||||
// Socialite user basics
|
||||
$characterId = (int) $ssoUser->getId();
|
||||
Debugbar::info($characterId);
|
||||
$characterName = $ssoUser->getName() ?: ($ssoUser->getNickname() ?? 'Unknown');
|
||||
Debugbar::info($characterName);
|
||||
|
||||
// Provider-specific extra payload sometimes appears in user array / token response.
|
||||
// We’ll defensively extract what we can.
|
||||
$raw = $ssoUser->user ?? [];
|
||||
|
||||
$characterOwnerHash =
|
||||
$raw['CharacterOwnerHash'] ?? $raw['character_owner_hash'] ?? $raw['owner_hash'] ?? '';
|
||||
Debugbar::info($characterOwnerHash);
|
||||
$raw['CharacterOwnerHash']
|
||||
?? $raw['character_owner_hash']
|
||||
?? $raw['owner_hash']
|
||||
?? '';
|
||||
|
||||
$token = $ssoUser->token;
|
||||
Debugbar::info($token);
|
||||
$refreshToken = $ssoUser->refreshToken ?? null;
|
||||
Debugbar::info($refreshTokeen);
|
||||
$expiresIn = $ssoUser->expiresIn ?? null;
|
||||
Debugbar::info($expiresIn);
|
||||
|
||||
$user = User::updateOrCreate(
|
||||
['character_id' => $characterId],
|
||||
@@ -65,26 +57,22 @@ class EveLoginController extends Controller
|
||||
'token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expiresIn' => $expiresIn,
|
||||
// "user" holds jwt - you can set it later when you add JWT issuance.
|
||||
'user_jwt' => null,
|
||||
]
|
||||
);
|
||||
Debugbar::info($user);
|
||||
|
||||
//Issue JWT and store in the "user" column per your spec
|
||||
$jwt = $jwtService->make($user);
|
||||
$user->user_jwt = $jwt;
|
||||
$user->save();
|
||||
// Always regenerate JWT on successful login
|
||||
$jwtService->forceRefresh($user);
|
||||
|
||||
Auth::login($user, true);
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
} catch (Throwable $e) {
|
||||
// For now: fail back to login with a generic error.
|
||||
// Later you can add logging/telemetry.
|
||||
Debugbar::addThrowable($e);
|
||||
return redirect()->route('login')->with('error', $e);
|
||||
report($e);
|
||||
|
||||
return redirect()
|
||||
->route('login')
|
||||
->with('error', 'SSO login failed. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
app/Http/Middleware/RefreshUserJwt.php
Normal file
20
app/Http/Middleware/RefreshUserJwt.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\JwtService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RefreshUserJwt
|
||||
{
|
||||
public function handle(Request $request, Closure $next, JwtService $jwtService): Response
|
||||
{
|
||||
if ($request->user()) {
|
||||
$jwtService->refreshIfNeeded($request->user());
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ class User extends Authenticatable
|
||||
'refresh_token',
|
||||
'expiresIn',
|
||||
'user_jwt', // holds jwt (per spec)
|
||||
'user_jwt_issued-at',
|
||||
'user_jwt_expires_at',
|
||||
'privleges_version',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@@ -29,6 +32,14 @@ class User extends Authenticatable
|
||||
{
|
||||
return [
|
||||
'expiresIn' => 'integer',
|
||||
'privileges_version' => 'integer',
|
||||
'user_jwt_issued_at' => 'datetime',
|
||||
'user_jwt_expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function markPrivilegesChanged(): void
|
||||
{
|
||||
$this->increment('privileges_version');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,57 @@ use RuntimeException;
|
||||
|
||||
class JwtService
|
||||
{
|
||||
public function issue(User $user, bool $save = true): string
|
||||
{
|
||||
$secret = config('jwt.secret');
|
||||
|
||||
if (! $secret) {
|
||||
throw new RuntimeException('JWT secret is not configured.');
|
||||
}
|
||||
|
||||
$issuedAt = Carbon::now();
|
||||
$ttl = (int) config('jwt.ttl', 3600);
|
||||
$expiresAt = $issuedAt->copy()->addSeconds($ttl);
|
||||
|
||||
$payload = [
|
||||
'iss' => config('jwt.issuer', config('app.name')),
|
||||
'sub' => (string) $user->id,
|
||||
'iat' => $issuedAt->timestamp,
|
||||
'nbf' => $issuedAt->timestamp,
|
||||
'exp' => $expiresAt->timestamp,
|
||||
|
||||
'character_id' => $user->character_id,
|
||||
'character_name' => $user->character_name,
|
||||
'character_owner_hash' => $user->character_owner_hash,
|
||||
|
||||
//Critical for privileg-aware JWT invalidation/regeneration
|
||||
'privileges_version' => (int) $user->privileges_version,
|
||||
];
|
||||
|
||||
$jwt = JWT::encode($payload, $secret, 'HS256');
|
||||
|
||||
if ($save) {
|
||||
$user->forceFill([
|
||||
'user_jwt' => $jwt,
|
||||
'user_jwt_issued_at' => $issuedAt,
|
||||
'user_jwt_expires_at' => $expiresAt,
|
||||
])->save();
|
||||
}
|
||||
|
||||
return $jwt;
|
||||
}
|
||||
|
||||
public function refreshIfNeeded(User $user): string
|
||||
{
|
||||
$refreshInterval = (int) config('jwt.refresh_interval', 3600);
|
||||
|
||||
if ($user->jwtNeedsRefresh($refreshInterval)) {
|
||||
return $this->issue($user);
|
||||
}
|
||||
|
||||
return $user->user_jwt;
|
||||
}
|
||||
|
||||
public function make(User $user): string
|
||||
{
|
||||
$secret = config('jwt.secret');
|
||||
|
||||
25
app/Services/UserPrivilegesService.php
Normal file
25
app/Services/UserPrivilegesService.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UserPrivilegeService
|
||||
{
|
||||
public function privilegesChanged(User $user, ?callable $mutator = null): User
|
||||
{
|
||||
return DB::transaction(function () use ($user, $mutator) {
|
||||
if ($mutator) {
|
||||
$mutator($user);
|
||||
}
|
||||
|
||||
$user->increment('privileges_version');
|
||||
$user->refresh();
|
||||
|
||||
app(JwtService::class)->forceRefresh($user);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\RefreshUserJwt;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
@@ -11,7 +12,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->web(append: [
|
||||
RefreshUserJwt::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
@@ -4,4 +4,7 @@ return [
|
||||
'secret' => env('JWT_SECRET'),
|
||||
'ttl' => env('JWT_TTL', 3600),
|
||||
'issuer' => env('JWT_ISSUER', env('APP_NAME', 'Framework')),
|
||||
|
||||
//Regenerate token if older than this many seconds
|
||||
'refresh_interval' => (int) env('JWT_REFRESH_INTERVAL', 3600),
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->timestamp('user_jwt_issued_at')->nullable()->after('user_jwt');
|
||||
$table->timestamp('user_jwt_expires_at')->nullable()->after('user_jwt_issued_at');
|
||||
|
||||
// Tracks privilege changes. Increment this whenever privileges/roles change.
|
||||
$table->unsignedBigInteger('privileges_version')->default(1)->after('expiresIn');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'user_jwt_issued_at',
|
||||
'user_jwt_expires_at',
|
||||
'privileges_version',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,28 @@
|
||||
@extends('layouts.user.dashb4')
|
||||
@section('content')
|
||||
<br>
|
||||
@if(auth()->user()->hasRole('User') || auth()->user()->hasRole('Admin'))
|
||||
Welcome to the dashboard where you are now logged into.
|
||||
@endif
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dashboard</title>
|
||||
</head>
|
||||
<body style="font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;">
|
||||
<div style="max-width: 900px; margin: 40px auto; padding: 0 16px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h1>Hello World</h1>
|
||||
|
||||
@endsection
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p>Logged in as: <strong>{{ auth()->user()->character_name }}</strong> ({{ auth()->user()->character_id }})</p>
|
||||
<p>Privileges version: <strong>{{ auth()->user()->privileges_version }}</strong></p>
|
||||
<p>JWT issued at: <strong>{{ optional(auth()->user()->user_jwt_issued_at)?->toDateTimeString() }}</strong></p>
|
||||
<p>JWT expires at: <strong>{{ optional(auth()->user()->user_jwt_expires_at)?->toDateTimeString() }}</strong></p>
|
||||
|
||||
<h2>JWT</h2>
|
||||
<textarea readonly style="width:100%; min-height:180px;">{{ auth()->user()->user_jwt }}</textarea>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,6 +17,9 @@
|
||||
</div>
|
||||
|
||||
<p>Logged in as: <strong>{{ auth()->user()->character_name }}</strong> ({{ auth()->user()->character_id }})</p>
|
||||
<p>Privileges version: <strong>{{ auth()->user()->privileges_version }}</strong></p>
|
||||
<p>JWT issued at: <strong>{{ optional(auth()->user()->user_jwt_issued_at)?->toDateTimeString() }}</strong></p>
|
||||
<p>JWT expires at: <strong>{{ optional(auth()->user()->user_jwt_expires_at)?->toDateTimeString() }}</strong></p>
|
||||
|
||||
<h2>JWT</h2>
|
||||
<textarea readonly style="width:100%; min-height:180px;">{{ auth()->user()->user_jwt }}</textarea>
|
||||
|
||||
@@ -6,3 +6,5 @@ use Illuminate\Support\Facades\Artisan;
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::command('app:refresh-user-jwts')->everyMinute();
|
||||
Reference in New Issue
Block a user