skills/implementing-scalekit-laravel-auth/SKILL.md
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.
npx skillsauth add scalekit-inc/skills implementing-scalekit-laravel-authInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference repo: scalekit-inc/scalekit-laravel-auth-example
app/
├── Services/
│ └── ScalekitClient.php # Raw HTTP OAuth client (no PHP SDK)
├── Http/
│ ├── Controllers/
│ │ └── AuthController.php
│ └── Middleware/
│ ├── ScalekitAuth.php # Session auth gate
│ ├── ScalekitPermission.php # Per-route permission check
│ └── ScalekitTokenRefresh.php # Auto token refresh on every request
config/
└── scalekit.php # Reads from env via config('scalekit.*')
routes/
└── web.php # Named routes + middleware groups
SCALEKIT_ENV_URL=https://your-env.scalekit.io
SCALEKIT_CLIENT_ID=your-client-id
SCALEKIT_CLIENT_SECRET=your-client-secret
SCALEKIT_REDIRECT_URI=http://localhost:8000/auth/callback
Scopes are hardcoded in config/scalekit.php, not from env:
'scopes' => 'openid profile email offline_access',
// offline_access is required to receive a refresh token
ScalekitClient service (app/Services/ScalekitClient.php)⚠️ No official Scalekit PHP SDK exists. This app uses Laravel's
Httpfacade with raw HTTP calls. Always useconfig('scalekit.*')— do not readenv()directly:
use App\Services\ScalekitClient;
// Injected via Laravel's service container — never `new ScalekitClient()`
| Method | HTTP call | Auth |
|---|---|---|
| getAuthorizationUrl($state) | Builds {env_url}/oauth/authorize?response_type=code&... | None |
| exchangeCodeForTokens($code) | POST {env_url}/oauth/token with grant_type=authorization_code | Basic Auth |
| refreshAccessToken($refreshToken) | POST {env_url}/oauth/token with grant_type=refresh_token | Basic Auth |
| getUserInfo($accessToken) | Delegates to validateTokenAndGetClaims() | — |
| validateTokenAndGetClaims($token) | Manual base64 JWT decode — no signature verification | — |
| hasPermission($token, $permission) | Decodes JWT, checks permission claim chain | — |
| logout($accessToken) | Builds {env_url}/oidc/logout?post_logout_redirect_uri=... | None |
| isTokenExpired($expiresAt) | now()->addMinutes(5)->gt(Carbon::parse($expiresAt)) | — |
Token exchange and refresh use Http::asForm()->withBasicAuth(clientId, clientSecret). Both fall back to expires_in = 3600 if the field is missing.
$parts = explode('.', $token);
$payload = $parts[1];
$payload .= str_repeat('=', (4 - strlen($payload) % 4) % 4); // padding
$decoded = base64_decode(strtr($payload, '-_', '+/')); // URL-safe base64
$claims = json_decode($decoded, true);
$permissions = $claims['permissions']
?? $claims['https://scalekit.com/permissions']
?? $claims['scalekit:permissions']
?? [];
// Also falls back to scope string if all are empty
if (empty($permissions)) {
$permissions = explode(' ', $claims['scope'] ?? '');
}
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 8601 string 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').
GET /login → AuthController::login)$state = Str::random(32); // Illuminate\Support\Str
session(['oauth_state' => $state]);
$authUrl = $this->scalekitClient->getAuthorizationUrl($state);
return view('auth.login', ['auth_url' => $authUrl]);
// Template renders a link/button to $auth_url
GET /auth/callback → AuthController::callback)$request->query('state') vs session('oauth_state') → response()->view('auth.error', [...], 400) on mismatchsession()->forget('oauth_state')$tokenResponse = $this->scalekitClient->exchangeCodeForTokens($code)$idTokenClaims$userInfo = $this->scalekitClient->getUserInfo($accessToken) → access token claims$mergedClaims = array_merge($userInfo, $idTokenClaims) — ID token wins (overlaid last)$expiresAt = now()->addSeconds($expiresIn)return redirect()->route('auth.dashboard')GET|POST /logout → AuthController::logout)$logoutUrl = $this->scalekitClient->logout($accessToken);
// → {env_url}/oidc/logout?post_logout_redirect_uri={base_url}
// post_logout_redirect_uri is derived from SCALEKIT_REDIRECT_URI, stripping /auth/callback
session()->flush(); // Full session wipe
return redirect($logoutUrl); // Server-side redirect to Scalekit
POST /sessions/refresh-token)On invalid_grant error: session()->flush() + return 401 with 'requiresReauth' => true.
bootstrap/app.php (Laravel 11) or Kernel.php (Laravel ≤10)// Laravel 11 — bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'scalekit.auth' => \App\Http\Middleware\ScalekitAuth::class,
'scalekit.permission' => \App\Http\Middleware\ScalekitPermission::class,
]);
$middleware->append(\App\Http\Middleware\ScalekitTokenRefresh::class);
})
ScalekitAuth — session gateRedirects to auth.login with ->with('next', $request->path()) if scalekit_user session key is missing.
ScalekitPermission — parameterised permission checkValidates 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 requestSkipped paths: login, auth/callback, logout, sessions/refresh-token.
Buffer: 5 minutes (via isTokenExpired()). On invalid_grant during auto-refresh: session()->flush() (user gets redirected on next request).
routes/web.php)// Public
Route::get('/', [AuthController::class, 'home'])->name('auth.home');
Route::get('/login', [AuthController::class, 'login'])->name('auth.login');
Route::get('/auth/callback', [AuthController::class, 'callback'])->name('auth.callback');
// Protected group
Route::middleware(['scalekit.auth'])->group(function () {
Route::get('/dashboard', [AuthController::class, 'dashboard'])->name('auth.dashboard');
Route::match(['get', 'post'], '/logout', [AuthController::class, 'logout'])->name('auth.logout');
Route::get('/sessions', [AuthController::class, 'sessions'])->name('auth.sessions');
Route::post('/sessions/validate-token', [AuthController::class, 'validateToken'])->name('auth.validate_token');
Route::post('/sessions/refresh-token', [AuthController::class, 'refreshToken'])->name('auth.refresh_token');
// Permission-gated — note colon syntax for middleware parameter
Route::get('/organization/settings', [AuthController::class, 'organizationSettings'])
->middleware('scalekit.permission:organization:settings')
->name('auth.organization_settings');
});
Key notes:
GET and POST (Route::match)auth. prefix throughout — use route('auth.dashboard') in Blade| URL | Middleware | Auth |
|---|---|---|
| GET / | — | No |
| GET /login | — | No |
| GET /auth/callback | — | No |
| GET /dashboard | scalekit.auth | Yes |
| GET\|POST /logout | scalekit.auth | Yes |
| GET /sessions | scalekit.auth | Yes |
| POST /sessions/validate-token | scalekit.auth | Yes |
| POST /sessions/refresh-token | scalekit.auth | Yes |
| GET /organization/settings | scalekit.auth + scalekit.permission:organization:settings | Yes + permission |
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);
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.
Verify your session cookie config in config/session.php:
'same_site' => 'lax', // Required — 'strict' breaks OAuth callbacks
'secure' => env('SESSION_SECURE_COOKIE', false), // true in production
'http_only' => true,
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.
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
]);
})
// In AuthController::login
$next = $request->query('next', route('auth.dashboard'));
// Validate: only relative paths
if (!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');
}
return redirect($next);
ScalekitAuth middleware passes ->with('next', $request->path()) when redirecting to login — read it back in login() with session('next') or $request->query('next').
return response()
->view('auth.dashboard', ['user' => session('scalekit_user', [])])
->header('Cache-Control', 'no-store');
Prevents the browser back button from serving a cached authenticated page after logout.
Update ScalekitAuth middleware to return 401 for JSON requests:
public function handle(Request $request, Closure $next): Response
{
if (!session()->has('scalekit_user')) {
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated'], 401);
}
return redirect()->route('auth.login')->with('next', $request->path());
}
return $next($request);
}
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 withsupports_credentials => true.
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();
return redirect($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.
tools
Create or review Scalekit custom providers/connectors for proxy-only usage, including MCP providers. Use this skill when the task is to gather API docs, infer whether a connector is OAuth, Basic, Bearer, or API Key, determine if it is an MCP provider, determine required tracked fields like domain or version, generate provider JSON, check for existing custom providers, show update diffs, run approved create or update curls, and print resolved delete curls.
tools
Use when a developer is new to Scalekit and needs guidance on where to start, doesn't know which auth plugin or skill to choose, wants to connect an AI agent or agentic workflow to third-party services (Gmail, Slack, Notion, Google Calendar), needs OAuth or tool-calling auth for agents, wants to add authentication to a project but hasn't chosen an approach yet, or needs to install the Scalekit plugin for their AI coding tool (Claude Code, Codex, Copilot CLI, Cursor, or other agents).
tools
Use when a user asks to generate, review, validate, or fix any code snippet that uses Scalekit APIs or SDKs. This skill is the single source of truth for Scalekit code correctness — it can generate illustration-quality snippets from scratch (for docs, websites, or integration guides) and review existing code to catch wrong method names, missing parameters, security anti-patterns, and broken auth flows. Covers all four SDKs (Node, Python, Go, Java), raw REST API calls, and both Scalekit product suites — SaaSKit (SSO, login, sessions, RBAC, SCIM) and AgentKit (connections, tool calling, MCP auth). Use when the user says review my Scalekit code, generate a Scalekit example, validate this auth flow, check my SDK usage, fix my Scalekit integration, write a code sample for docs, or anything involving Scalekit code quality.
development
Walks through a structured production readiness checklist for Scalekit SSO implementations. Use when the user says they are going live, launching to production, doing a pre-launch review, hardening their SSO setup, or wants to verify their Scalekit implementation is production-ready.