alvinfadli / access-lock
A lightweight password-protected access gate middleware for Laravel applications.
Requires
- php: ^8.2
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/hashing: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^10.0|^11.0|^12.0|^13.0
- illuminate/session: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.0|^11.0
README
A lightweight Laravel package that password-protects your application (or specific routes) using a middleware, a JavaScript prompt(), and Laravel session storage.
Not intended for production-grade authentication. Use this as a simple access gate — e.g. for staging environments, internal tools, or early-access previews.
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.2 |
| Laravel | ^10.0 or ^11.0 |
Installation
1. Install via Composer
composer require alvinfadli/access-lock
The service provider is auto-discovered; no manual registration is needed.
2. Set a Password
Run the Artisan command to set the access password:
php artisan access-lock:set-password
You will be prompted to enter and confirm a password. The bcrypt hash is automatically written to your .env file as:
ACCESS_LOCK_PASSWORD_HASH="$2y$12$..."
If you cache your configuration, clear it afterwards:
php artisan config:clear
Usage
Web (Monolith / Blade)
Important: always add this middleware inside the
webgroup, never in the global middleware stack. The global stack runs beforeStartSession, so$request->session()would not be available yet and you would get a "Session store not set on request" error.
Laravel 10 — app/Http/Kernel.php
Add it to the web group (after StartSession):
protected $middlewareGroups = [ 'web' => [ // ... existing entries ... \AlvinFadli\AccessLock\Http\Middleware\AccessLockMiddleware::class, ], ];
Laravel 11 / 12 / 13 — bootstrap/app.php
Use appendToGroup('web', ...) — not append():
->withMiddleware(function (Middleware $middleware) { $middleware->appendToGroup('web', \AlvinFadli\AccessLock\Http\Middleware\AccessLockMiddleware::class); })
API (Decoupled / SPA / Mobile)
For decoupled setups (e.g. Angular, React, Vue, or mobile apps talking to a Laravel API), use the access.lock.api middleware instead. It is token-based and returns JSON responses rather than redirecting to a Blade view.
1. Protect your API routes
// routes/api.php Route::middleware('access.lock.api')->group(function () { Route::apiResource('/users', UserController::class); // ... other protected routes });
2. Obtain a token
POST the staging password to the built-in unlock endpoint. No authentication is required for this endpoint — it is the entry point.
POST /api/access-lock/unlock
Content-Type: application/json
{ "password": "your-staging-password" }
Success (200):
{ "token": "your-staging-token", "expires_in": 120 }
Wrong password (401):
{ "message": "Invalid password." }
3. Use the token on subsequent requests
Include the token on every protected API request using one of two headers:
Authorization: Bearer your-staging-password
or
X-Access-Lock-Token: your-staging-password
The middleware verifies the token against the configured bcrypt hash on every request — no session or cache storage is needed.
How it works
- The client POSTs the password to
/api/access-lock/unlock. - The package verifies it using
access_lock_verify(). - On success, the plain-text password is returned as a token.
- The client stores the token (e.g.
localStorage) and sends it with every subsequent request. AccessLockApiMiddlewarecallsaccess_lock_verify(token)on each request — if it matches the configured hash, the request passes through; otherwise it returns403.
Protect a Route Group (web)
Route::middleware('access.lock')->group(function () { Route::get('/dashboard', [DashboardController::class, 'index']); Route::get('/reports', [ReportController::class, 'index']); });
Protect a Single Route (web)
Route::get('/secret', [SecretController::class, 'index'])->middleware('access.lock');
Protect a Route Group (api)
Route::middleware('access.lock.api')->group(function () { Route::get('/dashboard', [DashboardController::class, 'index']); Route::get('/reports', [ReportController::class, 'index']); });
Protect a Single Route (api)
Route::get('/secret', [SecretController::class, 'index'])->middleware('access.lock.api');
How It Works (Web)
- A visitor hits a protected route.
AccessLockMiddlewarechecks the Laravel session foraccess_lock_unlocked = true.- If not unlocked, the visitor is redirected to
/access-lock. - The unlock page loads and a
window.prompt()dialog appears automatically. - The visitor enters the password and it is submitted via
POST. - If correct, the session flag is set and the visitor is redirected back to the original URL.
- If incorrect, the unlock page reloads with an error message.
How It Works (API)
- A client (SPA, mobile app, etc.) hits a protected API route.
AccessLockApiMiddlewarechecks for a valid token in theAuthorization: BearerorX-Access-Lock-Tokenheader.- If no valid token is found, the request is rejected with a
401or403response. - The client obtains a token by POSTing the password to the
/api/access-lock/unlockendpoint. - On success, the server returns the plain-text password as a token (for convenience in decoupled setups).
- The client stores the token and includes it on every subsequent request.
AccessLockApiMiddlewarecallsaccess_lock_verify(token)on each request — if it matches the configured hash, the request passes through; otherwise it returns403.
Publishing Assets
Publish Config
php artisan vendor:publish --tag=access-lock-config
This copies config/access-lock.php to your application's config/ directory so you can customise it.
Publish Views
php artisan vendor:publish --tag=access-lock-views
This copies the unlock Blade view to resources/views/vendor/access-lock/ for customisation.
Publish Everything
php artisan vendor:publish --tag=access-lock
Configuration
After publishing, edit config/access-lock.php:
return [ // Bcrypt hash of the access password (set via Artisan command). 'password_hash' => env('ACCESS_LOCK_PASSWORD_HASH', null), // Session key used to track unlocked state. 'session_key' => 'access_lock_unlocked', // URL prefix for the unlock page routes (/access-lock by default). 'route_prefix' => 'access-lock', // Bypass conditions — see "Bypass Conditions" section below. 'bypass' => [ 'query' => [], 'headers' => [], ], // Setup API token TTL (in seconds) 'api' => [ 'token_ttl' => env('ACCESS_LOCK_API_TOKEN_TTL', 120), ], ];
Bypass Conditions
You can configure query string parameters or request headers that automatically and permanently unlock the session for a visitor — no password prompt is shown.
This is useful for automated tools, CI checks, SSO redirects, or any trusted caller that should never see the lock screen.
Setup
Publish the config and list the query keys / header names you want to act as bypass signals:
php artisan vendor:publish --tag=access-lock-config
// config/access-lock.php 'bypass' => [ // All listed query keys must be present and non-empty to bypass. // e.g. visiting /?ssoKey=anything&userId=123 will unlock the session. 'query' => [ 'ssoKey', 'userId', ], // All listed header names must be present and non-empty to bypass. // e.g. sending X-SSO-Key: anything will unlock the session. 'headers' => [ 'X-SSO-Key', ], ],
Helper Functions
The package provides three global helpers:
// Returns true if a password hash has been configured. access_lock_active(): bool // Returns true if the current visitor has already unlocked access. access_lock_unlocked(): bool // Verifies a plain-text password against the configured hash. access_lock_verify(string $password): bool
Setting Password Programmatically
use AlvinFadli\AccessLock\Support\PasswordManager; PasswordManager::setPassword('my-plain-text-password');
Middleware Reference
| Alias | Class | Use case |
|---|---|---|
access.lock |
AccessLockMiddleware |
Monolith / Blade apps — session-based, redirects to prompt page |
access.lock.api |
AccessLockApiMiddleware |
Decoupled / API apps — token-based, returns JSON |
License
MIT — see LICENSE.