signifly/laravel-janitor
Laravel Janitor is a lightweight Laravel package for automatically cleaning up stale Eloquent records. Define which models and records should be pruned, then let scheduled cleanup keep your database tidy without custom commands.
Install the package
composer require signifly/laravel-janitor
php artisan vendor:publish --provider="Signifly\Janitor\JanitorServiceProvider"
Publishing gives you config/janitor.php — this is the first file to read. It holds the proxy endpoint (/oauth/token), the OAuth client_id / client_secret (or the config keys that resolve them), the grant type, the cookie name and its lifetime/domain/SameSite settings, and the route prefix/middleware used for the package's routes.
Make sure an OAuth server exists. The package is a proxy: it does not issue tokens itself. You either run Laravel Passport in the same app (php artisan passport:install, password grant client created) or point the config at a remote token endpoint. Without a password-grant client you will get a 400 invalid_client on the very first request — that is the most common "it doesn't work" report.
Check the routes. Run php artisan route:list | grep -i auth (or janitor). You should see the login, refresh and logout endpoints registered by the package under whatever prefix/middleware you configured. If they are missing, the service provider isn't registered or the config has the routes disabled.
First use case — SPA login without shipping a client secret. Your frontend posts { email, password } to the package's login route. The package calls the OAuth token endpoint server-side using the confidential client credentials, returns the short-lived access_token in the JSON body, and sets the long-lived refresh_token as an HttpOnly cookie. The browser never sees the client secret and JavaScript never touches the refresh token.
const { access_token } = await fetch('/api/auth/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
}).then(r => r.json());
Before you invest: the repository is archived (last release 2023-08-03). Read the source once — it is small, essentially one controller plus config — and be prepared to fork it into app/ or packages/, or to replace it with Passport's own first-party cookie guard / Sanctum if your Laravel version supports them.
Token lifecycle in an HTTP client. The canonical loop is: request → 401 → POST to the refresh route (with credentials: 'include') → retry original request with the new access token. Guard against concurrent refreshes with a single in-flight promise, otherwise a burst of parallel 401s triggers several refresh calls and, if you ever enable refresh-token rotation, invalidates the token chain.
Guard your API routes with auth:api. Janitor issues tokens; it does not protect routes. Keep auth:api (or your configured guard) on the resource routes and treat the package purely as the credential-exchange surface. Set the guard's driver to passport in config/auth.php so $request->user() resolves from the bearer token.
Separate the login surface from the rest. Put the janitor routes in their own middleware group with throttle (e.g. throttle:5,1 on login) and no session/CSRF middleware, since they are stateless JSON endpoints. Registering them inside web is a classic misconfiguration that produces 419s.
Multi-client / multi-tenant setups. If you have several frontends or tenants, resolve the OAuth client dynamically rather than hardcoding one. The clean approach is a small subclass or a container binding that returns client credentials based on a request header or host, then rebind the package's service in a service provider. Keep per-client refresh cookies on distinct names or paths so they don't collide.
Customising responses and errors. Point the config at your own controller (or extend the package's) when you need to add user data, flags like must_change_password, or a consistent { message, errors } error envelope. Normalising the OAuth error payloads (invalid_grant vs invalid_credentials) here saves every consumer from mapping them.
Logout properly. Remember to revoke tokens, not just clear the cookie — delete the user's access tokens ($user->tokens()->delete()) and/or revoke the refresh token so a stolen cookie cannot be replayed. The package clears the cookie; server-side revocation is your job.
Testing. Feature tests should not round-trip through a real OAuth server. Use Passport::actingAs($user) for authenticated endpoints and only cover the proxy endpoints themselves with a mocked HTTP client (Http::fake() on the token URL) asserting the request payload, the JSON shape, and the Set-Cookie headers.
composer.json and check its Laravel illuminate/* constraint against your framework version. On newer Laravel releases the auto-discovery and middleware signatures may drift; a local fork is the safest route if you plan to keep it long-term.SameSite=None; Secure and a Domain covering both hosts. Locally, localhost and 127.0.0.1 are different sites for cookie purposes — testing on http://localhost:3000 against http://localhost:8000 with SameSite=Lax silently drops the cookie on cross-site refreshes.Secure cookies are required for cross-site use, so a plain-HTTP staging environment will appear to "lose" the refresh token.token_expired from invalid_token in the frontend interceptor, or you'll loop refreshes for revoked users.Passport::tokensExpireIn() and refreshTokensExpireIn() explicitly, and keep the cookie's max-age in sync with the refresh TTL or the cookie outlives the token.oauth_refresh_tokens. Schedule passport:purge (or the package's equivalent) in Kernel::schedule.passport:keys must run in deployment; without it token issuance throws an obscure encryption error that surfaces as a 500 from the login route. If you proxy to a remote auth server, that key never lives in this app at all.--personal instead of --password.How can I help you explore Laravel packages today?