visin/laravel-entra-auth

Shared Microsoft Entra ID client-credentials JWT validation and token acquisition for Laravel services.

Maintainers

Package info

github.com/visin-platform/laravel-entra-auth

pkg:composer/visin/laravel-entra-auth

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-14 10:21 UTC

This package is not auto-updated.

Last update: 2026-07-14 13:31:33 UTC


README

Latest Version on Packagist Tests Total Downloads License

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 call login.microsoftonline.com per 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.