Step 06 · Connect a client
Goal: reach the exact decision from step 05 — but now from application code, not tinker. You’ll use
laravel-iam-client to check a permission with the Iam facade, then protect an HTTP route so Alice gets
in (200) and Bob is refused (403).
Step 6 of 8. The PDP already decides correctly (step 05). Now a consuming app asks it and enforces the
answer.
Server vs client — and why local mode here
A consuming app never re-implements the rules — it delegates every authorization decision to the PDP through
laravel-iam-client. The client has two transports:
local— the server lives in the same app, so the client resolves the server’sAuthorizationEngine
from the container and calls the PDP in-process (no network). This is our tutorial’s single-app setup.http— the server is remote; the clientPOSTs each query to
{base}/api/iam/v1/decisions/checkwith a Bearer token. This is the production topology.
We use local because server and client are in one app. Switching to http later is one env var. See
Choose a transport.
1. Configure the client
You installed laravel-iam-client in step 01. Publish its config and point it at local mode:
php artisan vendor:publish --tag=laravel-iam-client-config
In .env:
IAM_CLIENT_MODE=local
IAM_CLIENT_APP=warehouse
IAM_CLIENT_APP=warehouse becomes the default application on every query, so you don’t repeat it at each
call site. Clear the config cache:
php artisan config:clear
2. Check a decision from code — the Iam facade
The quickest way to ask IAM from application code is the facade. Smoke-test it in tinker:
php artisan tinker
>>> use App\Models\User;
>>> use Padosoft\Iam\Client\Facades\Iam;
>>> $alice = User::find(1);
>>> $bob = User::find(2);
>>> Iam::can($alice, 'warehouse:stock.adjust'); // Alice holds the role granted in step 05
=> true
>>> Iam::can($bob, 'warehouse:stock.adjust'); // Bob has nothing
=> false
// Need the full decision (explanation, step-up)?
>>> Iam::check($alice, 'warehouse:stock.adjust', ['explain' => true])->granted();
=> true
Iam::can($alice, …) is true and Iam::can($bob, …) is false — the same decision as step 05, now
reachable from any controller, job or Blade view. Iam::can() resolves the subject id from the user’s
primary key, so $alice (id 1) matches the grant on user:1. Type exit.
3. Protect a route — real 200 / 403
Now enforce it at the edge. Two things to know for this single-app setup:
The server already registers the iam.can alias (for its own Admin API), so the client does not
overwrite it. In this combined app, reference the client’s middleware class directly:
Padosoft\Iam\Client\Http\Middleware\IamCan. In a dedicated client app (no server), the tidy
iam.can:warehouse:stock.adjust alias works out of the box.
A stand-in login (tutorial only)
iam.can needs an authenticated user ($request->user()). In step 07 that comes from real OIDC login; for
now, add a tiny insecure, tutorial-only login route so you can switch identity by id. In
routes/web.php:
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Padosoft\Iam\Client\Http\Middleware\IamCan;
// ⚠️ TUTORIAL ONLY — never ship a login-by-id route. Replace with real login (step 07).
Route::get('/dev-login/{id}', function (int $id) {
Auth::loginUsingId($id);
return redirect('/warehouse/adjust');
});
// The protected route: only subjects the PDP permits for warehouse:stock.adjust may enter.
Route::get('/warehouse/adjust', fn () => 'You may adjust stock. ✅')
->middleware(IamCan::class.':warehouse:stock.adjust');
Try it
Serve the app and visit the routes in a browser (the session cookie carries the login):
php artisan serve
- Log in as Alice, then hit the protected route
Visithttp://localhost:8000/dev-login/1— it logs Alice in and redirects to/warehouse/adjust.
✅ You see “You may adjust stock.” (HTTP 200) — the PDP permitted her. - Switch to Bob
Visithttp://localhost:8000/dev-login/2, which redirects to/warehouse/adjust.
🚫 You get HTTP 403 — Bob has no grant, soiam.canrefuses him. Fail-closed, at the edge. - Confirm it’s the PDP deciding
Grant Bob the role in tinker
(Grant::create([... 'subject_id'=>'2', 'privilege_type'=>'role', 'privilege_key'=>'warehouse:stock_operator' ...])),
reload/warehouse/adjustas Bob → now 200. Access follows the grants, live.
Alice reaches the route, Bob is blocked with 403, and granting Bob the role lets him in — all decided
centrally by the PDP and enforced by the client middleware. That is a working, tested IAM.
4. Keep your existing Laravel code working
The client also registers a Gate adapter, so your existing @can and $this->authorize() calls consult
IAM automatically for namespaced abilities (those containing :):
@can('warehouse:stock.adjust')
<button>Adjust stock</button>
@endcan
No rewrite — the same central decision, through Laravel’s own Gate. See
Gate adapter.
- Every decision is
false/ every route 403 → inlocalmode the client needs the server in the same
app (it is, here) andIAM_CLIENT_APP=warehouseset; runphp artisan config:clear. Remember the client
is fail-closed — a misconfiguration denies, never allows. Target class [iam.can] does not existor the wrong middleware runs → you used theiam.canalias;
in this single app useIamCan::classas shown above.- 403 for Alice too → did you delete the leftover deny grant from step 05? A deny overrides her
permit.
More in Troubleshooting.
What you just did
- Configured
laravel-iam-clientinlocalmode against the in-app PDP. - Checked a decision from code with
Iam::can()/Iam::check(). - Protected a route and saw a real 200 for Alice, 403 for Bob, changing live with grants.
- Kept your
@can/authorize()calls working via the Gate adapter.
Next: replace the stand-in login with a real OIDC / OAuth login against the server’s IdP.
→ Step 07 · OIDC / OAuth login
Deeper references: laravel-iam-client docs ·
Protect routes ·
Ask the PDP