visin / laravel-entra-auth
Shared Microsoft Entra ID client-credentials JWT validation and token acquisition for Laravel services.
Requires
- php: ^8.3
- ext-json: *
- ext-openssl: *
- firebase/php-jwt: ^6.10|^7.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.5|^12.0
This package is not auto-updated.
Last update: 2026-07-14 13:31:33 UTC
README
Microsoft Entra ID client-credentials auth for Laravel services — both halves in one tested implementation, so every service authenticates the same way:
- Resource server: validate incoming Entra JWTs — signature via tenant
JWKS, expiry, audience (both
api://<guid>and bare guid forms), tenant pinning, issuer, and app roles. Discovery and JWKS responses are cached, so validation does not calllogin.microsoftonline.comper request. - Client: acquire app-only tokens with the client credentials flow, cached per scope until shortly before expiry.
Installation
composer require visin/laravel-entra-auth
The service provider is auto-discovered. Publish the config to override defaults:
php artisan vendor:publish --tag=entra-auth-config
Configuration
# Resource server (incoming token validation) ENTRA_AUTH_TENANT_ID=<entra-tenant-guid> ENTRA_AUTH_AUDIENCE=api://<this-api-app-registration-guid> # Client (outgoing token acquisition), only needed when calling other APIs ENTRA_AUTH_CLIENT_ID=<this-service-app-registration-guid> ENTRA_AUTH_CLIENT_SECRET=<client-secret> ENTRA_AUTH_SCOPE=api://<target-api-app-registration-guid>/.default
The protected API's app registration must issue v2 access tokens
("api": { "requestedAccessTokenVersion": 2 } in the app manifest).
Protecting routes
entra.auth validates the bearer token; entra.role:<role> requires an app
role from the token's roles claim:
Route::middleware(['entra.auth', 'entra.role:invoices.write']) ->post('/v1/invoices', [InvoiceController::class, 'store']);
The validated token is available on the request:
use Visin\EntraAuth\AccessToken; use Visin\EntraAuth\Http\Middleware\AuthenticateEntraToken; /** @var AccessToken $token */ $token = $request->attributes->get(AuthenticateEntraToken::REQUEST_ATTRIBUTE); $token->tenantId; // issuing tenant guid $token->clientId; // calling app registration guid $token->roles; // app roles from the token $token->claims; // full decoded claims (stdClass)
Services with their own caller mapping (client registries, audit logging) can skip the middleware and use the validator directly:
use Visin\EntraAuth\Contracts\AccessTokenValidator; $accessToken = app(AccessTokenValidator::class)->validate($bearerToken);
To restrict which roles are passed through, set allowed_roles in
config/entra-auth.php.
Calling another Entra-protected API
use Illuminate\Support\Facades\Http; use Visin\EntraAuth\Client\ClientCredentialsTokenProvider; $token = app(ClientCredentialsTokenProvider::class)->token(); $response = Http::withToken($token)->post('https://api.example.com/v1/invoices', [...]);
Pass a scope to target a different API: ->token('api://other-api/.default').
Testing your app
Bind a fake validator instead of faking HTTP:
use Visin\EntraAuth\AccessToken; use Visin\EntraAuth\Contracts\AccessTokenValidator; $this->app->singleton(AccessTokenValidator::class, fn () => new class implements AccessTokenValidator { public function validate(string $token): AccessToken { return new AccessToken('tenant', 'client-a', ['invoices.read'], new \stdClass); } });
Requirements
PHP 8.3+ · Laravel 12 or 13 · firebase/php-jwt 6.10+ or 7.x
License
MIT — see LICENSE.md. Contributions welcome, see CONTRIBUTING.md.