Search by

bundle / identity-laravel

rakibulalam

Laravel scope authorization SDK for Bundle Identity

Package info

github.com/rakibulalam/bundle-identity-sdk

pkg:composer/bundle/identity-laravel

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-10-28 20:15 UTC

This package is auto-updated.

Last update: 2026-10-28 20:18:38 UTC


README

bundle/identity-laravel adds fail-closed, server-backed scope and role authorization to Laravel applications. It uses Bundle Identity's existing bundle.identity.authorization.v1.AuthorizationService contract over mutually authenticated TLS. The package does not parse JWTs, reproduce RBAC rules, or require Protobuf generation in consuming applications.

Quick start

  1. Install the PHP gRPC extension and this package.
  2. Publish the package configuration.
  3. Set the Bundle Identity endpoint, region, and mTLS file paths in .env.
  4. Register a trusted subject resolver in App\Providers\AppServiceProvider.
  5. Put Laravel authentication before bundle.scope or bundle.role on protected routes.
  6. Mark routes intentionally excluded from Bundle Identity authorization with bundle.public when default-deny is enabled.

Installation

The verified production baseline is PHP 8.2+ and Laravel 12. Laravel 10/11 are intentionally not declared because the currently resolvable framework/Testbench releases are blocked by Composer security advisories. The consuming runtime needs the PHP grpc extension. The Protobuf runtime and generated messages are installed through Composer.

Install and enable the PHP gRPC extension for both CLI and the PHP runtime serving Laravel. The exact package command depends on the deployment image; a typical PECL installation is:

pecl install grpc
php --ri grpc

Install from a private Composer registry:

composer config repositories.bundle-identity composer https://composer.example.internal
composer require bundle/identity-laravel:^1.0

Or install directly from a private Git repository:

composer config repositories.bundle-identity vcs git@github.com:your-org/bundle-identity-laravel.git
composer require bundle/identity-laravel:^1.0

Laravel discovers BundleIdentityServiceProvider through Composer. Publish the optional configuration file:

php artisan vendor:publish \
  --provider="Bundle\Identity\BundleIdentityServiceProvider" \
  --tag=bundle-identity-config

Verify the runtime before deployment:

php --ri grpc
composer show bundle/identity-laravel
php artisan config:cache

Configuration

Set the endpoint, region routing metadata, and certificate paths in the consuming application's environment:

BUNDLE_IDENTITY_SCOPE_ENDPOINT=identity.internal:50051
BUNDLE_IDENTITY_REGION_ID=00000000-0000-0000-0000-000000000001
BUNDLE_IDENTITY_ROOT_CA=/run/secrets/bundle/root-ca.pem
BUNDLE_IDENTITY_CLIENT_KEY=/run/secrets/bundle/client.key
BUNDLE_IDENTITY_CLIENT_CRT=/run/secrets/bundle/client-chain.pem
BUNDLE_IDENTITY_SCOPE_DEADLINE_MS=1000
BUNDLE_IDENTITY_SCOPE_POLICY=all
BUNDLE_IDENTITY_DEFAULT_DENY=true

The endpoint must be host:port, without http:// or https://. gRPC receives root trust, the client private key, and the client certificate chain through ChannelCredentials::createSsl; there is no insecure transport fallback and no hostname override. Consequently, the endpoint hostname must match the server certificate. Keep the private key outside the repository, readable only by the application process, and never put certificate contents in Laravel configuration.

The checked-out Bundle Identity server requires x-region-id metadata on every gRPC request. BUNDLE_IDENTITY_REGION_ID supplies that routing value. It is distinct from the authenticated user subject and does not replace mTLS application authentication.

All values in config/bundle-identity.php are scalar, so Laravel configuration caching is supported. Register the request-time callback in the consuming application's service provider rather than its config file:

<?php

namespace App\Providers;

use Bundle\Identity\BundleIdentityManager;
use Illuminate\Http\Request;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function boot(BundleIdentityManager $identity): void
    {
        $identity->userSub(
            fn (Request $request) => $request->user()?->jwtSubject(),
        );
    }
}

If the application's JWT middleware stores verified claims on the request, resolve only that verified claim:

$identity->userSub(
    fn (Request $request) => $request->attributes->get('verified_jwt')?->sub,
);

Do not place the callback inside config/bundle-identity.php; closures cannot be serialized by php artisan config:cache.

Run authentication middleware before Bundle Identity authorization. The service provider places scope enforcement after Laravel's standard Authenticate middleware in the middleware priority list. Custom authentication middleware must likewise be ordered before ScopeMiddleware. The callback must use identity already verified by the application. Never return an unverified JWT payload, query parameter, header-provided user ID, or request body field. If no callback is registered, the package requires an authenticated Laravel user and uses a verified sub request attribute when present, otherwise the user's authentication identifier.

Route authorization

Require one scope:

Route::get('/orders', [OrderController::class, 'index'])
    ->middleware(['auth', 'bundle.scope:orders.read']);

The configured default policy applies to multiple scopes:

Route::get('/orders', [OrderController::class, 'index'])
    ->middleware(['auth', 'bundle.scope:orders.read,orders.write']);

Override the policy explicitly by putting all or any first:

->middleware(['auth', 'bundle.scope:any,orders.read,orders.write']);

Each scope is sent independently to the existing unary server RPC. all stops at the first denial; any stops at the first allow. No RBAC decision is made locally.

A complete protected route group can be configured as follows:

use Illuminate\Support\Facades\Route;

Route::middleware(['auth:sanctum'])->group(function (): void {
    Route::get('/orders', [OrderController::class, 'index'])
        ->middleware('bundle.scope:orders.read');

    Route::post('/orders', [OrderController::class, 'store'])
        ->middleware('bundle.scope:orders.create');

    Route::delete('/orders/{order}', [OrderController::class, 'destroy'])
        ->middleware('bundle.role:any,code:orders-admin,id:role-supervisor');
});

Controller and method attributes

use Bundle\Identity\Attributes\RequireScopes;

#[RequireScopes('orders.read')]
final class OrderController
{
    public function index() {}

    #[RequireScopes(['orders.create', 'orders.admin'], policy: 'any')]
    public function store() {}
}

Attribute discovery is automatically enforced for routes in Laravel's web and api groups and is cached in process. Precedence is deterministic:

  1. Explicit bundle.scope route middleware overrides attributes.
  2. A method attribute overrides its controller attribute.
  3. A controller attribute applies when the method has none.

Role authorization

Roles must be identified explicitly as code:<value> or id:<value>. This prevents a value from being interpreted as the wrong server field.

Route::get('/admin', AdminController::class)
    ->middleware(['auth', 'bundle.role:code:administrator']);

Route::get('/review', ReviewController::class)
    ->middleware(['auth', 'bundle.role:any,code:reviewer,id:role-admin']);

Controller and method attributes are also supported:

use Bundle\Identity\Attributes\RequireRoles;

#[RequireRoles('code:viewer')]
final class ReportController
{
    #[RequireRoles(['code:editor', 'id:role-admin'], policy: 'any')]
    public function update() {}
}

For one role and for any, the SDK calls VerifyRole and short-circuits on the first assignment. Multiple all requirements use the server's VerifyRoles batch RPC, whose contract allows only when every selector is assigned. Route declarations override role attributes; method role attributes override controller role attributes. When both scope and role attributes are present at the effective level, both are enforced.

With default_deny=true, a grouped route without scope or role metadata returns 403. Mark an intentionally public route explicitly:

Route::get('/health', HealthController::class)->middleware('bundle.public');

If an application uses custom route groups, add both authorization middleware classes:

use Bundle\Identity\Middleware\RoleMiddleware;
use Bundle\Identity\Middleware\ScopeMiddleware;
use Illuminate\Foundation\Configuration\Middleware;

// bootstrap/app.php, inside withMiddleware(...)
->withMiddleware(function (Middleware $middleware): void {
    $middleware->appendToGroup('internal', [
        ScopeMiddleware::class,
        RoleMiddleware::class,
    ]);
})

The package already adds these middleware classes to Laravel's standard web and api groups.

Configuration reference

Environment variable Required Default Purpose
BUNDLE_IDENTITY_SCOPE_ENDPOINT Yes — TLS gRPC endpoint in host:port format. It serves scope and role verification.
BUNDLE_IDENTITY_REGION_ID Yes — Region sent as x-region-id gRPC metadata.
BUNDLE_IDENTITY_ROOT_CA Yes — Readable PEM root CA or CA chain path.
BUNDLE_IDENTITY_CLIENT_KEY Yes — Readable PEM client private-key path.
BUNDLE_IDENTITY_CLIENT_CRT Yes — Readable PEM client certificate/chain path.
BUNDLE_IDENTITY_SCOPE_DEADLINE_MS No 1000 Maximum duration of each gRPC authorization request.
BUNDLE_IDENTITY_SCOPE_POLICY No all Default matching policy for multi-scope and multi-role middleware.
BUNDLE_IDENTITY_DEFAULT_DENY No true Denies standard grouped routes that have no authorization declaration.

After changing environment configuration in a deployed application, rebuild Laravel's cached configuration:

php artisan config:clear
php artisan config:cache

Request and decision flow

  1. The consuming application authenticates and verifies its JWT or session.
  2. The configured callback resolves the verified user sub for that request.
  3. The SDK opens/reuses an mTLS gRPC channel. The client certificate authenticates the calling application; the subject identifies the authenticated user.
  4. Bundle Identity evaluates the requested scope or role assignment against centralized RBAC and returns allowed.
  5. The SDK returns 401 for a missing/invalid subject, 403 for denial or missing metadata, and 503 when a decision cannot be verified.

Timeouts, connection failures, TLS failures, malformed responses, and non-success gRPC statuses fail closed. PERMISSION_DENIED is treated as a normal authorization denial. Certificate private keys are never included in SDK log messages.

Testing

composer install
composer test

Unit and Testbench feature tests mock the scope client. Optional integration testing should use a dedicated Bundle Identity deployment with test CA/client material and a known region; do not use production private keys in CI. The package ships proto/authorization.proto and generated PHP messages/stub, so production installation does not run protoc.

Troubleshooting

  • 401: authentication ran too late, or the subject callback returned an empty/non-scalar value.
  • 403: Bundle Identity denied a requested scope/role, or default-deny found no authorization declaration.
  • 503: inspect service reachability, deadline, x-region-id, certificate chain, client-key pairing, and server hostname/SAN. The response intentionally does not expose internal TLS or gRPC details.
  • Class Grpc\\ChannelCredentials not found: install and enable ext-grpc for the same PHP SAPI that serves Laravel.
  • TLS hostname failure: use an endpoint hostname present in the server certificate SAN; do not disable verification or add an override.

Release process

Tag releases with semantic versions and publish the complete repository, including src/Grpc/Generated and proto/authorization.proto. Regenerate and review the PHP artifacts whenever the owning Bundle Identity proto changes. No database migration or runtime code-generation step is required.