Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Passport Introspection Laravel Package

xcoorp/laravel-passport-introspection

Laravel Passport Introspection adds an OAuth2 token introspection endpoint to Laravel Passport, letting resource servers validate access tokens and retrieve token metadata such as scopes, expiry, and client details.

View on GitHub
Deep Wiki
Context7

Getting Started

xcoorp/laravel-passport-introspection adds OAuth 2.0 Token Introspection (RFC 7662) to a Laravel app — either as a consumer (your API validates opaque tokens against a remote authorization server) or as an exposer (your Passport server publishes an /introspect endpoint for other services). The v2.0.1 release is a dependency-only change adding Laravel 13 support; there are no breaking changes, but if you are running Laravel 13 you need v2.0.1 or later, so widen your constraint to ^2.0.1 rather than pinning ^2.0.0.

Minimal setup:

composer require xcoorp/laravel-passport-introspection:^2.0.1
php artisan vendor:publish --tag=passport-introspection-config

Package auto-discovery registers the service provider, so there is no manual config/app.php entry. Confirm the config file landed in config/passport-introspection.php (or whatever the published filename is for your version) — that file is the first place to look, since it controls the introspection endpoint URL, client credentials, route prefix, middleware stack, and cache settings.

Set the credentials in .env:

PASSPORT_INTROSPECTION_URL=https://auth.example.com/oauth/introspect
PASSPORT_INTROSPECTION_CLIENT_ID=my-resource-server
PASSPORT_INTROSPECTION_CLIENT_SECRET=secret

First use case: you have a central auth server issuing opaque tokens to several downstream Laravel APIs. Those APIs cannot verify the token locally (it is not a JWT and they do not hold Passport's signing keys), so each request calls the introspection endpoint. Drop the package's middleware on the route group you want to protect and you get 401/403 handling for free:

Route::middleware('introspect')->group(function () {
    Route::get('/orders', OrderController::class);
});

Verify the middleware alias in the provider or config before assuming the name — aliases have shifted between major versions of similar packages.

Implementation Patterns

Route-group protection. The dominant pattern is applying the middleware to an api group rather than per-route. Keep a separate group for genuinely public endpoints so you are not introspecting health checks and webhooks.

Caching introspection results. Introspection is a network round-trip per request; without caching you have added an HTTP call to every API hit and a hard dependency on the auth server's latency. Cache by token hash (never the raw token as a key — it ends up in logs and in Redis keyspace dumps) with a TTL clamped to the token's remaining lifetime and a short ceiling (30–60s) so revocation propagates in bounded time:

$key = 'introspect:'.hash('sha256', $bearer);
$result = Cache::remember($key, now()->addSeconds(30), fn () => $introspector->introspect($bearer));

Resolving a user from the response. RFC 7662 returns active, scope, client_id, sub, exp, and anything the server chose to add. If you want Auth::user() populated in the downstream API, map sub to your local User model in a small listener/middleware that runs after introspection — do not assume the package does this for you. Alternatively, Auth::loginUsingId() or a custom guard that reads the cached introspection payload.

Scope-based authorization. The scope claim is a space-delimited string, not an array. Wrap the split in a helper and use it in a Gate::before or a dedicated scope:read middleware rather than sprinkling str_contains checks. Combine with route-model binding so authorisation and scoping are declared at the route, not in controllers.

Multi-issuer / multi-tenant. If you accept tokens from more than one authorization server, do not try to multiplex a single client-credentials pair. Register one named configuration per issuer (URL + credentials) and select it based on the iss claim or the route/host. Since the token is opaque you often cannot read iss before introspecting, so route- or host-based selection is the reliable approach.

Testing. Never hit the real endpoint in tests. Use Http::fake() to return canned introspection responses and assert both the happy path and {"active": false}. Add a test for the auth-server-down case, which is the one everybody forgets:

Http::fake(['*/introspect' => Http::response(['active' => true, 'sub' => '1', 'scope' => 'read'])]);

Integration with Passport on the same app. If the app both issues and validates tokens, do not introspect its own tokens — local Passport::token() verification is cheaper and does not fail when the app is under load. Reserve introspection middleware for the paths where tokens come from elsewhere.

Failure semantics. Decide deliberately between fail-closed (401 when the auth server is unreachable — secure, but an auth-server blip becomes a full outage) and fail-open (serve the request — never do this for sensitive routes). The pragmatic middle ground is serving from cache with a longer TTL when the upstream errors, and alerting on the error path.

Gotchas and Tips

  • Laravel 13 support is the whole of v2.0.1. Nothing else changed, so an upgrade from v2.0.0 is a composer update. If you are on Laravel 13 and hit an illuminate/support constraint conflict, that is the fix. Re-run vendor:publish with --force only after diffing — config files can change shape between majors even when the changelog only mentions dependencies.
  • The token must be sent as-is. Pass the bearer token without the Bearer prefix to the introspection call; stripping it wrong is the most common cause of a spurious active: false.
  • Client authentication is usually HTTP Basic. Per RFC 7662 the client id and secret are form-encoded before being base64'd into the Authorization header. Secrets containing +, /, = or : break naive implementations — check whether the package handles the encoding or whether you need to pre-encode.
  • active: false is a valid 200 response, not an error. Treat a non-2xx from the endpoint as an infrastructure failure (log loudly, consider 503) and {"active": false} as an authentication decision (401). Conflating these makes outages look like a wave of bad credentials.
  • A missing active key is not false. Strict comparison; if the auth server returns a malformed body, reject it rather than treating absence as truthy.
  • Check exp yourself. A cached active: true can outlive the token. Always compare exp (with a little clock skew tolerance, ~30–60s) before honouring a cached result.
  • HTTPS and timeouts. Set an explicit connect/read timeout in the config or via Http::timeout(). The default can be long enough to exhaust PHP-FPM workers under an auth-server slowdown — a cascading failure that looks like your API is broken.
  • Debugging. Log::debug() the raw response body and status, never the token. For one-off checks, Http::fake() in tinker or a throwaway test is faster than adding logging. If the endpoint returns 401 to you, the problem is your client credentials, not the user's token.
  • Cache keys and revocation. Short TTLs are the only revocation mechanism you get with an opaque token. If you need instant revocation, the auth server must push it to you — introspection alone cannot deliver that.
  • Route protection is not authorisation. Introspection tells you the token is valid and what it is scoped for; it does not tell you this user may edit this order. Keep policies in place.

Conclusion

v2.0.1 is a low-risk compatibility release — bump to ^2.0.1 if you are testing against Laravel 13, otherwise there is nothing to act on. The package itself is straightforward: the engineering effort sits in caching the introspection call correctly and in distinguishing "token is bad" from "auth server is down," which are the two places implementations of this pattern usually go wrong.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console