graphene-ict / laravel-cognito-guard
Lean Laravel auth guard that validates AWS Cognito User Pool JWTs. Supports DB-backed users, DB-less mode, cognito:groups → Gates bridge, multi-pool, and Octane.
Package info
github.com/GrapheneICT/laravel-cognito-guard
pkg:composer/graphene-ict/laravel-cognito-guard
Requires
- php: ^8.3
- ext-openssl: *
- firebase/php-jwt: ^6.10|^7.0
- illuminate/auth: ^11.0|^12.0
- illuminate/contracts: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpseclib/phpseclib: ^3.0
Suggests
- laravel/octane: Recommended for high-throughput APIs — this package is Octane-safe and ships with a state-leak test.
- laravel/sanctum: For hybrid auth setups where some routes use Cognito JWTs and others use Sanctum personal-access-tokens.
- 2.x-dev
- dev-main / 2.x-dev
- v2.2.0
- v2.1.0
- v2.0.0
- v1.0.0
- dev-dependabot/github_actions/dependabot/fetch-metadata-3.1.0
- dev-dependabot/github_actions/actions/checkout-6
- dev-dependabot/github_actions/ramsey/composer-install-4
- dev-dependabot/github_actions/codecov/codecov-action-6
- dev-dependabot/github_actions/stefanzweifel/git-auto-commit-action-7
- dev-develop
- dev-feature/additionals
- dev-feature/tests
- dev-feature/fix-namespaces
This package is auto-updated.
Last update: 2026-05-19 16:09:19 UTC
README
A lean Laravel auth guard that validates JSON Web Tokens issued by an AWS Cognito User Pool. Verifies the JWT signature against Cognito's JWKS, enforces standard Cognito claims, and resolves the authenticated user via a UserProvider — or returns a value object in DB-less mode.
Requirements
- PHP 8.3+
- Laravel 11 / 12
- A configured AWS Cognito User Pool
Installation
You can install the package via Composer:
composer require graphene-ict/laravel-cognito-guard
Publish the config file:
php artisan vendor:publish --tag=cognito-guard-config
Set the env vars:
COGNITO_USER_POOL_ID=us-east-1_XXXXXXXXX AWS_REGION=us-east-1 # Optional comma-separated allow-list: COGNITO_CLIENT_IDS=app-client-1,app-client-2
Usage
DB-backed users (default)
-
Add
provider_idto youruserstable:$table->string('provider_id')->unique()->nullable();
-
Add
provider_idto the model's$fillable. -
Register the guard in
config/auth.php:'guards' => [ 'cognito' => [ 'driver' => 'cognito', 'provider' => 'cognito', 'pool' => 'default', ], ], 'providers' => [ 'cognito' => ['driver' => 'cognito'], ],
-
Protect routes:
Route::middleware('auth:cognito')->get('/me', fn () => auth()->user());
A new User record is auto-provisioned on the first authenticated request whose sub is not yet known. Disable by setting cognito-guard.user_provider.auto_provision to false.
DB-less mode
For SPA / service-to-service callers that don't need a local users table:
'guards' => [ 'cognito' => [ 'driver' => 'cognito', 'provider' => 'cognito', 'pool' => 'default', 'db_less' => true, ], ],
auth()->user() returns a GrapheneICT\CognitoGuard\CognitoUser value object:
$user = auth()->user(); $user->username(); // string|null $user->email(); // string|null $user->groups(); // string[] $user->scopes(); // string[] $user->claim('sub'); // any single claim $user->claims(); // raw payload (stdClass)
Per-route scope enforcement
Pool-wide required_scopes is a blunt instrument — every route on the guard demands the same scopes. For finer control, use the cognito.scope middleware:
Route::middleware(['auth:cognito', 'cognito.scope:read:reports'])->get('/reports', ...); Route::middleware(['auth:cognito', 'cognito.scope:read:reports,write:reports'])->post('/reports', ...);
401 if unauthenticated, 403 if any required scope is missing from the token's scope claim.
Groups → Gates bridge
With cognito-guard.bridge_groups_to_gates enabled (default), entries in the cognito:groups claim become Gate abilities for free:
Gate::allows('admins'); // true if 'admins' is in cognito:groups Route::middleware('can:moderators')->...; // works the same
Multi-pool
// config/cognito-guard.php 'pools' => [ 'default' => ['user_pool_id' => env('COGNITO_USER_POOL_ID'), 'region' => 'us-east-1'], 'partners' => ['user_pool_id' => env('PARTNER_POOL_ID'), 'region' => 'us-east-1'], ], // config/auth.php 'guards' => [ 'cognito' => ['driver' => 'cognito', 'provider' => 'cognito', 'pool' => 'default'], 'partners' => ['driver' => 'cognito', 'provider' => 'cognito', 'pool' => 'partners'], ],
Recipes
- End-to-end Cognito Hosted UI → SPA → API (auth code + PKCE, the modern flow):
docs/COGNITO-SETUP.md. - Running Cognito alongside Sanctum:
docs/SANCTUM-HYBRID.md.
Configuration reference
See config/cognito-guard.php. Key knobs:
pools.<name>.allowed_token_use—['access'],['id'], or both.pools.<name>.allowed_client_ids— empty = accept any; populated = strict allow-list againstclient_id(access) /aud(id).pools.<name>.required_scopes— every scope must be present in the token'sscopeclaim.pools.<name>.leeway— clock-skew tolerance forexp/nbf/iat, in seconds.jwks.cache_ttl— JWKS cache TTL (default 6h). Stale entries kept 30d and used on Cognito outages.bridge_groups_to_gates— toggle thecognito:groups→ Gate bridge.user_provider.sub_claim— which JWT claim supplies the stable identifier. Defaultsub. Set tocognito:usernameor a custom attribute when a legacy users table is keyed by something other than the Cognito sub.user_provider.sub_column— the column on the user model that stores the value above (defaultprovider_id).
Events
The guard dispatches two events for observability — listen for them however you wire listeners normally (e.g. in AppServiceProvider::boot()):
GrapheneICT\CognitoGuard\Events\CognitoTokenValidated— fired after a JWT is verified and a user is resolved. Carries$user,$claims(stdClass), and$pool.GrapheneICT\CognitoGuard\Events\CognitoTokenRejected— fired when a token fails verification, just before theInvalidTokenExceptionpropagates. Carries$exceptionand$pool.
Raw tokens are intentionally excluded from event payloads — log claims, not credentials.
Diagnostics
php artisan about # shows the Cognito Guard section php artisan cognito:test-token <jwt> # validates a token + prints a step-by-step diagnosis php artisan cognito:warm-jwks # pre-fetch JWKS at deploy time
The cognito:test-token command accepts the raw JWT or a Bearer <jwt> string and prints which validation step passed or failed (signature, issuer, token_use, client_id/aud, scopes, expiry). Add --pool=<name> to test against a non-default pool, or --verbose-claims to dump the full payload.
cognito:warm-jwks pre-fetches and caches the JWKS for every configured pool so the first authenticated request after a cold cache doesn't pay the round-trip, and so reachability to cognito-idp.<region>.amazonaws.com is verified at deploy time. Use --pool=<name> to warm a single pool.
FAQ
InvalidTokenException: Invalid token_use "id". Allowed: access
Your guard is configured to accept access tokens only, but the client is sending an id token. Either send an access token, or widen cognito-guard.pools.<name>.allowed_token_use to ['access', 'id'] (the default).
InvalidTokenException: Token client_id/aud is not in the allow-list
The token's client_id (access tokens) or aud (id tokens) doesn't match COGNITO_CLIENT_IDS. Either add the App Client ID to that env var (comma-separated), or leave allowed_client_ids empty to accept any client.
InvalidTokenException: Invalid issuer
The token was issued by a different User Pool. Check COGNITO_USER_POOL_ID and AWS_REGION.
InvalidTokenException: Token has expired
Either the token genuinely expired, or your server clock has drifted. Bump cognito-guard.pools.<name>.leeway (seconds) to tolerate minor skew — but fix the underlying NTP issue, don't paper over it long-term.
JwksFetchException: Failed to fetch JWKS from https://cognito-idp.<region>.amazonaws.com/...
Your app can't reach Cognito's JWKS endpoint. Check egress to cognito-idp.<region>.amazonaws.com. The guard will serve from stale cache for up to 30 days if the JWKS was ever fetched successfully (toggle via cognito-guard.jwks.stale_on_error).
The user returned from auth()->user() isn't my Eloquent model
If your guard config has 'db_less' => true, the package returns a CognitoUser value object built from JWT claims instead of looking up a database row. Set db_less => false (the default) and configure cognito-guard.user_provider.model to point at your Eloquent user model.
Auth provider "..." is not configured
You set 'provider' => 'cognito' on the guard but didn't add a cognito entry under auth.providers. Add:
'providers' => ['cognito' => ['driver' => 'cognito']],
Multiple Cognito pools — how do I authenticate against the second one?
Register a second guard with a different pool key and pick which one to apply per route:
'guards' => [ 'cognito' => ['driver' => 'cognito', 'provider' => 'cognito', 'pool' => 'default'], 'partners' => ['driver' => 'cognito', 'provider' => 'cognito', 'pool' => 'partners'], ],
Then Route::middleware('auth:partners')->....
Testing your app
The guard exposes an actingAs() helper so your test suite doesn't need to forge JWTs:
use Illuminate\Support\Facades\Auth; // DB-less: pass JWT claims, get a CognitoUser back. Auth::guard('cognito')->actingAs([ 'sub' => 'user-uuid', 'email' => 'alice@example.com', 'cognito:groups' => ['admins'], ]); // DB-backed: pass your Eloquent user + the claims you want attached // (so the groups → Gates bridge sees them). Auth::guard('cognito')->actingAs($user, ['cognito:groups' => ['editors']]);
After the call, auth()->user(), auth()->id(), Gate::allows('admins'), and routes behind auth:cognito all behave as if a real token had been verified.
Testing the package
composer install
composer test
composer analyse
Upgrading from v1
Breaking changes — see UPGRADING.md.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.