diff --git a/.env.example b/.env.example
index 235b66e..1433095 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/app/Console/Commands/RefreshUserJwts.php b/app/Console/Commands/RefreshUserJwts.php
new file mode 100644
index 0000000..38dfad8
--- /dev/null
+++ b/app/Console/Commands/RefreshUserJwts.php
@@ -0,0 +1,35 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Controllers/Auth/EveLoginController.php b/app/Http/Controllers/Auth/EveLoginController.php
index 6486fa0..716d577 100644
--- a/app/Http/Controllers/Auth/EveLoginController.php
+++ b/app/Http/Controllers/Auth/EveLoginController.php
@@ -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.');
}
}
-}
\ No newline at end of file
+}
diff --git a/app/Http/Middleware/RefreshUserJwt.php b/app/Http/Middleware/RefreshUserJwt.php
new file mode 100644
index 0000000..8da463a
--- /dev/null
+++ b/app/Http/Middleware/RefreshUserJwt.php
@@ -0,0 +1,20 @@
+user()) {
+ $jwtService->refreshIfNeeded($request->user());
+ }
+
+ return $next($request);
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php
index 539cc19..e81f19f 100644
--- a/app/Models/Auth/User.php
+++ b/app/Models/Auth/User.php
@@ -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');
+ }
}
diff --git a/app/Services/JwtService.php b/app/Services/JwtService.php
index 70008c7..c09d080 100644
--- a/app/Services/JwtService.php
+++ b/app/Services/JwtService.php
@@ -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');
diff --git a/app/Services/UserPrivilegesService.php b/app/Services/UserPrivilegesService.php
new file mode 100644
index 0000000..8936ece
--- /dev/null
+++ b/app/Services/UserPrivilegesService.php
@@ -0,0 +1,25 @@
+increment('privileges_version');
+ $user->refresh();
+
+ app(JwtService::class)->forceRefresh($user);
+
+ return $user;
+ });
+ }
+}
\ No newline at end of file
diff --git a/bootstrap/app.php b/bootstrap/app.php
index c183276..550dc7d 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -1,5 +1,6 @@
withMiddleware(function (Middleware $middleware): void {
- //
+ $middleware->web(append: [
+ RefreshUserJwt::class,
+ ]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
diff --git a/config/jwt.php b/config/jwt.php
index d22d6e3..535d79d 100644
--- a/config/jwt.php
+++ b/config/jwt.php
@@ -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),
];
\ No newline at end of file
diff --git a/database/migrations/2026_03_08_003737_add_jwt_lifecycle_columns_to_users_table.php b/database/migrations/2026_03_08_003737_add_jwt_lifecycle_columns_to_users_table.php
new file mode 100644
index 0000000..fcca342
--- /dev/null
+++ b/database/migrations/2026_03_08_003737_add_jwt_lifecycle_columns_to_users_table.php
@@ -0,0 +1,30 @@
+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',
+ ]);
+ });
+ }
+};
\ No newline at end of file
diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php
index 3b5a547..76e32f1 100644
--- a/resources/views/dashboard.blade.php
+++ b/resources/views/dashboard.blade.php
@@ -1,8 +1,28 @@
-@extends('layouts.user.dashb4')
-@section('content')
-
-@if(auth()->user()->hasRole('User') || auth()->user()->hasRole('Admin'))
-Welcome to the dashboard where you are now logged into.
-@endif
+
+
+
Logged in as: {{ auth()->user()->character_name }} ({{ auth()->user()->character_id }})
+Privileges version: {{ auth()->user()->privileges_version }}
+JWT issued at: {{ optional(auth()->user()->user_jwt_issued_at)?->toDateTimeString() }}
+JWT expires at: {{ optional(auth()->user()->user_jwt_expires_at)?->toDateTimeString() }}
+ +Logged in as: {{ auth()->user()->character_name }} ({{ auth()->user()->character_id }})
+Privileges version: {{ auth()->user()->privileges_version }}
+JWT issued at: {{ optional(auth()->user()->user_jwt_issued_at)?->toDateTimeString() }}
+JWT expires at: {{ optional(auth()->user()->user_jwt_expires_at)?->toDateTimeString() }}