Implements Scalekit authentication in a Laravel app using the patterns from scalekit-inc/scalekit-laravel-auth-example. Handles login, OAuth callback, Laravel session storage, automatic token refresh via middleware, logout, and permission-based route protection. Uniquely uses Laravel's Http facade with raw HTTP calls instead of a PHP SDK — no official Scalekit PHP SDK exists. Use when adding auth controllers, protecting routes with middleware, managing sessions, or checking permissions in a Laravel + Scalekit codebase.
Implements Scalekit authentication in a Laravel app using the patterns from scalekit-inc/scalekit-laravel-auth-example. Handles login, OAuth callback, Laravel session storage, automatic token refresh via middleware, logout, and permission-based route protection. Uniquely uses Laravel's Http facade with raw HTTP calls instead of a PHP SDK — no official Scalekit PHP SDK exists. Use when adding auth controllers, protecting routes with middleware, managing sessions, or checking permissions in a Laravel + Scalekit codebase.
$permissions = $claims['permissions']
?? $claims['https://scalekit.com/permissions']
?? $claims['scalekit:permissions']
?? [];
// Also falls back to scope string if all are emptyif (empty($permissions)) {
$permissions = explode(' ', $claims['scope'] ?? '');
}
Session storage schema
All auth state lives in Laravel's session — no extra DB tables (uses default database or file driver):
session([
'scalekit_user' => [
'sub', 'email', 'name', 'given_name', 'family_name',
'preferred_username',
'claims' // merged array of ALL claims (ID token overlaid on access token)
],
'scalekit_tokens' => [
'access_token', 'refresh_token', 'id_token',
'expires_at', // Carbon ISO 8601string via ->toIso8601String()
'expires_in', // int seconds
],
'scalekit_roles' => [], // from access token claims
'scalekit_permissions' => [], // from access token claims
]);
Check auth status anywhere: session()->has('scalekit_user').
Validates access token claims via ScalekitClient::hasPermission(). On failure: response()->view('auth.permission_denied', [...], 403). Never returns a JSON 403 — always renders a view.
ScalekitTokenRefresh — auto refresh on every request
ScalekitClient is resolved from Laravel's service container in every controller and middleware constructor. No singleton binding needed — Laravel resolves it fresh per request by default. Register it in AppServiceProvider only if you need to scope it as a singleton:
// Optional — only if you want to share a single instance$this->app->singleton(ScalekitClient::class);
Install
composer require firebase/php-jwt # Only if using JWT signature verification
php artisan key:generate
php artisan migrate # Creates sessions table if using database driver
php artisan serve
Copy .env.example to .env and fill in the four SCALEKIT_* values.
Tactics
SameSite=Lax — required for OAuth callbacks
Verify your session cookie config in config/session.php:
SameSite: strict drops the session cookie on the cross-origin redirect from Scalekit back to /auth/callback, making oauth_state unavailable and causing the state mismatch check to fail every time.
CSRF exclusion for the OAuth callback
The OAuth callback is a GET request and is not subject to Laravel's CSRF middleware. However, if you add any Scalekit webhook endpoints (POST), exclude them explicitly. In Laravel 11 (bootstrap/app.php):
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'webhooks/scalekit', // example — callback is GET, not needed here
]);
})
Deep link preservation
// In AuthController::login$next = $request->query('next', route('auth.dashboard'));
// Validate: only relative pathsif (!str_starts_with($next, '/')) {
$next = route('auth.dashboard');
}
session(['oauth_state' => $state, 'next' => $next]);
// In AuthController::callback — after writing session data$next = session()->pull('next', route('auth.dashboard'));
if (!str_starts_with($next, '/')) {
$next = route('auth.dashboard');
}
returnredirect($next);
ScalekitAuth middleware passes ->with('next', $request->path()) when redirecting to login — read it back in login() with session('next') or $request->query('next').
Prevents the browser back button from serving a cached authenticated page after logout.
AJAX: 401 instead of redirect
Update ScalekitAuth middleware to return 401 for JSON requests:
publicfunctionhandle(Request $request, Closure$next): Response{
if (!session()->has('scalekit_user')) {
if ($request->expectsJson()) {
returnresponse()->json(['error' => 'Unauthenticated'], 401);
}
returnredirect()->route('auth.login')->with('next', $request->path());
}
return$next($request);
}
CORS for JavaScript clients
Laravel ships with CORS support. In config/cors.php:
'paths' => ['api/*', 'auth/*', 'sessions/*'],
'allowed_origins' => ['http://localhost:3000'], // explicit origin required'supports_credentials' => true, // required for session cookies
⚠️ 'allowed_origins' => ['*'] does not work with supports_credentials => true.
Session fixation after login
After writing all session data in callback(), regenerate the session ID to prevent session fixation:
// At the end of AuthController::callback, after writing session data:session()->regenerate();
returnredirect($next);
session()->regenerate() issues a new session ID while preserving the session data — an attacker who set a known session ID before login cannot use it after authentication.