Tutorial troubleshooting
The errors a newcomer hits most, with cause → fix. Grouped by the step where they usually surface.
Install & migrate (steps 01–02)
Cause: migrations didn’t run, or the app is pointed at the wrong database.
Fix: run php artisan migrate. On SQLite confirm database/database.sqlite exists and .env has
DB_CONNECTION=sqlite. Re-check with php artisan tinker --execute="var_dump(Schema::hasTable('iam_grants'));".
Cause: the service provider wasn’t auto-discovered.
Fix: composer dump-autoload then php artisan package:discover --ansi. Confirm
composer require padosoft/laravel-iam-server completed without errors. The provider is
Padosoft\Iam\IamServiceProvider.
Cause: config not published, or .env/config edited without clearing the cache.
Fix: php artisan vendor:publish --tag="laravel-iam-server-config", then always
php artisan config:clear after editing config or .env.
Cause: EC key generation needs an OpenSSL config file that’s missing on some Windows/Herd setups.
Fix: create a file whose first line is [req], and set crypto.openssl_config in config/iam.php to its
path (e.g. via an env var). Then php artisan config:clear. Linux/macOS usually don’t need this.
Decisions deny when you expect allow (steps 05–06)
Work through the fail-closed reasons in order:
- No matching grant — the subject holds no
permitfor thatfull_key(directly or via a role). Add
one (step 05). - A deny grant exists — deny-overrides. Any applicable
effect => 'deny'beats a permit. Remove it. - Outside the time window —
valid_fromis in the future orvalid_untilhas passed. Check the dates. - Wrong scope — the grant has an
application_key/organization_idthat doesn’t match the check’s
application/organization. - Wrong subject id —
Iam::can($user, …)uses the user’s primary key. A grant onuser:1only matches
user #1. Confirm with$user->getKey(). - Deprecated permission — the catalog row has a
deprecated_at; re-declare it in the manifest.
Cause: the client is fail-closed and something is misconfigured — a misconfiguration always denies,
never allows.
Fix: in local mode confirm the server lives in the same app and IAM_CLIENT_MODE=local,
IAM_CLIENT_APP=warehouse are set; php artisan config:clear. In http mode confirm
IAM_CLIENT_BASE_URL reaches the server and IAM_CLIENT_TOKEN is a valid bearer.
Route protection (step 06)
Cause: the server owns the iam.can alias in a combined server+client app, so the client does not
register it.
Fix: reference the client’s middleware class directly:
Padosoft\Iam\Client\Http\Middleware\IamCan::class.':warehouse:stock.adjust'. In a dedicated client app (no
server), the iam.can:… alias works as-is.
Cause: iam.auth / iam.can require an already-authenticated user ($request->user()); they never log
anyone in.
Fix: log in first (the tutorial’s /dev-login/{id} stand-in, or real OIDC login in step 07). Put
Laravel’s auth guard before the IAM middleware.
OIDC / OAuth (step 07)
Cause: the app isn’t serving, or OIDC route registration was disabled.
Fix: start php artisan serve; check php artisan route:list --path=well-known. Discovery lives at the
application root, not under /oauth.
Cause: signing keys are created lazily; none has been generated yet.
Fix: this is expected before the first token is signed. Issue one token through the flow and re-fetch —
the EC P-256 verification key (with its kid) then appears.
Cause: the client_id or redirect_uri doesn’t match the registered client.
Fix: use client_id=cli_warehouse and a redirect_uri that exactly matches the one in your step-04
manifest (http://localhost:8000/callback). Re-apply the manifest if you changed it.
Cause: no login backend is installed, so the IdP has nothing to render for authentication.
Fix: install one — composer require laravel/fortify (or Socialite / passkeys). They are suggest
dependencies, not bundled.
Admin API over HTTP (advanced)
Cause: the Admin API requires a bearer token (iam.admin_auth) — an IAM-issued token. The unauthenticated
routes are only /health and /ready.
Fix: for learning, prefer the in-process path this tutorial uses (tinker + CLI + local client). To call
the HTTP API, obtain a token via an OAuth flow and pin IAM_ADMIN_AUDIENCE. See
Securing the Admin API.
Still stuck?
- Re-read the step’s “If it fails” box — most issues are covered inline.
- Check the Configuration keys you touched.
- Compare against the runnable demo app — it wires the whole
ecosystem in one app and its feature tests encode the exact working API.
When in doubt, the system denies. A surprising 403 almost always means a missing/expired/mis-scoped grant or
a misconfiguration — never a silent allow. That is the safety contract working as designed.