PHPackages                             sumer5020/laravel-keycloak-guard - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. sumer5020/laravel-keycloak-guard

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

sumer5020/laravel-keycloak-guard
================================

A Keycloak Guard for Laravel — robsontenorio/laravel-keycloak-guard fork

v1.0.0(3mo ago)05MITPHPPHP ^8.3CI passing

Since Mar 23Pushed 3mo agoCompare

[ Source](https://github.com/sumer5020/laravel-keycloak-guard)[ Packagist](https://packagist.org/packages/sumer5020/laravel-keycloak-guard)[ RSS](/packages/sumer5020-laravel-keycloak-guard/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (1)Dependencies (11)Versions (3)Used By (0)

Laravel Keycloak Guard
======================

[](#laravel-keycloak-guard)

**Note: This Version is not production ready yet, still in active development**

> **robsontenorio/laravel-keycloak-guard fork** — Full support for **Laravel 13**, **Keycloak 26+**, and the **Keycloak 26 Organizations** feature (`--features=organization`).

[![Laravel](https://camo.githubusercontent.com/379ac210c7e2644180ddc1fb0ce39d8af7c51fdb8715c59570c4cf0da3403aa5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d31332e782d7265642e737667)](https://laravel.com)[![Keycloak](https://camo.githubusercontent.com/7e53e0e4dc19cb8b955e11a50274ef67ecbfc9a875107b0f9f513a005e9ee327/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4b6579636c6f616b2d32362532422d626c75652e737667)](https://www.keycloak.org)[![PHP](https://camo.githubusercontent.com/4c2df0f02cdad1271b09dfd1a4fa97c9020769ca5df612d8e5ea75d67908f15e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e332532422d707572706c652e737667)](https://php.net)[![License: MIT](https://camo.githubusercontent.com/784362b26e4b3546254f1893e778ba64616e362bd6ac791991d2c9e880a3a64e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e2e737667)](LICENSE)

---

What's New VS the Original Package:
-----------------------------------

[](#whats-new-vs-the-original-package)

FeatureOriginalThis VersionLaravel 13 support❌✅Keycloak 26 JWKS auto-discovery❌✅Automatic key rotation support❌✅Keycloak 26 Organizations❌✅`KeycloakOrg` facade❌✅`keycloak.org` middleware❌✅`keycloak.org.role` middleware❌✅`HasOrganizations` trait❌✅`TokenUser` (no-DB mode)Partial✅`Auth::payload()`❌✅`Auth::roles()`❌✅`RealmResource` (API Resource)❌✅`keycloak:doctor` command❌✅`ActingAsKeycloak` (Testing Trait)❌✅PHP 8.3 constructor promotion❌✅---

Requirements
------------

[](#requirements)

- PHP **8.3+**
- Laravel **12.x / 13.x**
- Keycloak **21+** (Organizations require **26+**)

---

Installation
------------

[](#installation)

```
composer require sumer5020/laravel-keycloak-guard
```

Publish the config:

```
php artisan vendor:publish --tag=keycloak-config
```

Optionally publish migrations (only if using `organizations.sync_to_database`):

```
php artisan vendor:publish --tag=keycloak-migrations
php artisan migrate
```

---

Health &amp; Diagnostics
------------------------

[](#health--diagnostics)

The package includes a "Doctor" command to help you troubleshoot connectivity and configuration issues.

```
php artisan keycloak:doctor
```

It performs the following checks:

- **Configuration**: Verifies that required environment variables are set.
- **Connectivity**: Attempts to fetch the OIDC discovery document (`.well-known/openid-configuration`) from Keycloak.
- **Organizations**: Checks if the organization feature is correctly configured.

---

API Resources
-------------

[](#api-resources)

If you need to expose your Keycloak configuration (realm name, base URL, issuer, JWKS URI) to your frontend (e.g., for `keycloak-js`), you can use the built-in `RealmResource`.

```
use KeycloakGuard\Http\Resources\RealmResource;

Route::get('/keycloak/config', function () {
    return new RealmResource([]);
});
```

This returns a standardized JSON response:

```
{
  "realm": "my-realm",
  "base_url": "https://keycloak.example.com",
  "jwks_uri": "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/certs",
  "issuer": "https://keycloak.example.com/realms/my-realm"
}
```

---

Basic Setup
-----------

[](#basic-setup)

### 1. Configure the Guard

[](#1-configure-the-guard)

**`config/auth.php`**:

```
'guards' => [
    'api' => [
        'driver'   => 'keycloak',
        'provider' => 'users',
    ],
],

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model'  => App\Models\User::class,
    ],
],
```

### 2. Environment Variables

[](#2-environment-variables)

```
# ── Required ────────────────────────────────────────────────
KEYCLOAK_BASE_URL=https://keycloak.example.com
KEYCLOAK_REALM=my-realm
KEYCLOAK_ALLOWED_RESOURCES=my-laravel-api

# ── Key / Token Verification ────────────────────────────────
# Option A: Static public key (from Realm Settings → Keys → RS256 → Public Key)
KEYCLOAK_REALM_PUBLIC_KEY=MIIBIjANBgkq...

# Option B: JWKS auto-discovery (recommended for Keycloak 26+, supports key rotation)
# Leave KEYCLOAK_REALM_PUBLIC_KEY empty — the package constructs the JWKS URI automatically
# Or set it explicitly:
KEYCLOAK_JWKS_URI=https://keycloak.example.com/realms/my-realm/protocol/openid-connect/certs
KEYCLOAK_JWKS_CACHE_TTL=3600

# ── User Matching ────────────────────────────────────────────
KEYCLOAK_TOKEN_PRINCIPAL_ATTRIBUTE=sub        # JWT claim used as identifier
KEYCLOAK_USER_PROVIDER_CREDENTIAL=keycloak_id # DB column to match against

# ── Optional ─────────────────────────────────────────────────
KEYCLOAK_APPEND_DECODED_TOKEN=true            # Attach $user->token (stdClass)
KEYCLOAK_LEEWAY=30                            # Clock skew tolerance (seconds)
KEYCLOAK_LOAD_USER_FROM_DATABASE=true
KEYCLOAK_TOKEN_ENCRYPTION_ALGORITHM=RS256

# ── Organizations (Keycloak 26+) ─────────────────────────────
KEYCLOAK_ORGANIZATIONS_ENABLED=true
KEYCLOAK_ORGANIZATION_HEADER=X-Organization   # Header for selecting active org
KEYCLOAK_ORGANIZATION_REQUIRE=false           # 403 if no org in token?
KEYCLOAK_ORGANIZATION_SYNC_DB=false           # Persist orgs to database?
```

### 3. Protect Routes

[](#3-protect-routes)

```
// routes/api.php

// Basic auth
Route::middleware('auth:api')->group(function () {
    Route::get('/me', fn(Request $r) => $r->user());
});

// Role-based access
Route::middleware(['auth:api', 'keycloak.role:admin'])->group(function () {
    Route::get('/admin', AdminController::class);
});

// Organization-scoped routes (Keycloak 26+)
Route::middleware(['auth:api', 'keycloak.org'])->group(function () {
    Route::apiResource('projects', ProjectController::class);

    // Org admin only
    Route::middleware('keycloak.org.role:admin')->group(function () {
        Route::apiResource('members', MemberController::class);
    });
});
```

---

Keycloak 26 Organizations
-------------------------

[](#keycloak-26-organizations)

### How It Works

[](#how-it-works)

When Keycloak is started with `--features=organization`, it injects an `organization` claim into the JWT:

```
{
  "sub": "f7a8b9c0-...",
  "preferred_username": "john@acme.com",
  "organization": {
    "acme-corp": {
      "id": "3fa85f64-...",
      "name": "Acme Corporation",
      "roles": ["admin", "member"]
    }
  }
}
```

For users in **multiple organizations**, the client sends an `X-Organization` header to select the active one.

### The `KeycloakOrg` Facade

[](#the-keycloakorg-facade)

```
use KeycloakGuard\Facades\KeycloakOrg;

// Get the active organization for this request
$org = KeycloakOrg::current();
// => ['alias' => 'acme-corp', 'id' => '3fa85f64-...', 'name' => 'Acme Corporation', 'roles' => ['admin']]

// Get all organizations the user belongs to (from the token)
$orgs = KeycloakOrg::all();

// Check membership
KeycloakOrg::belongsTo('acme-corp');        // bool
KeycloakOrg::hasOrganizations();             // bool

// Role checks within the active organization
KeycloakOrg::hasRole('admin');               // bool
KeycloakOrg::hasRole('admin', 'acme-corp'); // bool (explicit org)
KeycloakOrg::hasAnyRole(['admin', 'billing-manager']); // bool

// Sync to DB (requires sync_to_database = true)
KeycloakOrg::syncToDatabase(auth()->user());
```

### `HasOrganizations` Trait

[](#hasorganizations-trait)

Add to your `User` model for database relationships and convenient helpers:

```
use KeycloakGuard\Traits\HasOrganizations;

class User extends Authenticatable
{
    use HasOrganizations;
}
```

Then use:

```
// From JWT (no DB query)
$user->currentOrganization();        // array|null
$user->hasOrgRole('admin');          // bool
$user->belongsToOrg('acme-corp');    // bool

// From database (requires sync_to_database)
$user->organizations()->get();
$user->organizations()->where('alias', 'acme-corp')->first();
```

### Using Organization Context in Controllers

[](#using-organization-context-in-controllers)

```
class ProjectController extends Controller
{
    public function index(): JsonResponse
    {
        // Array from JWT — no DB query
        $org = KeycloakOrg::current();

        // Eloquent model — only if sync_to_database = true
        $orgModel = app('current_organization_model');

        return response()->json(
            Project::where('organization_id', $orgModel->id)->paginate()
        );
    }

    public function store(StoreProjectRequest $request): JsonResponse
    {
        $org = app('current_organization_model');

        $project = Project::create([
            ...$request->validated(),
            'organization_id' => $org->id,
        ]);

        return response()->json($project, 201);
    }
}
```

---

Auth Guard Methods
------------------

[](#auth-guard-methods)

```
use Illuminate\Support\Facades\Auth;

// Standard Laravel
Auth::user();           // Authenticatable|null
Auth::check();          // bool
Auth::id();             // mixed

// Keycloak-specific
Auth::token();          // string|null  — full decoded token as JSON
Auth::payload();        // stdClass|null — decoded token object
Auth::roles();          // array — all realm + resource roles (with inheritance)
Auth::roles('my-api'); // array         — resource-specific roles (includes realm roles)
Auth::hasRole('admin'); // bool
Auth::hasRole('editor', 'my-api'); // bool — resource-scoped

// Organizations
Auth::organizations();  // OrganizationService
```

---

Middleware Reference
--------------------

[](#middleware-reference)

MiddlewareDescriptionExample`keycloak.role:admin`Requires realm or any resource role`middleware('keycloak.role:admin,editor')``keycloak.role:editor|my-api`Requires role in specific resource`middleware('keycloak.role:editor|my-api')``keycloak.org`Resolves active org from JWT + header`middleware('keycloak.org')``keycloak.org.role:admin`Requires org-level role`middleware('keycloak.org.role:admin')`> **Note**: The `keycloak.role` middleware supports role inheritance. If a user has a realm-level role (e.g., `admin`), they will pass checks for any resource-specific role requirements.

---

No-Database Mode
----------------

[](#no-database-mode)

If your API doesn't have a `users` table, set:

```
KEYCLOAK_LOAD_USER_FROM_DATABASE=false
KEYCLOAK_APPEND_DECODED_TOKEN=true
```

`Auth::user()` returns a `TokenUser` built from JWT claims:

```
$user = Auth::user();
$user->sub;                 // UUID
$user->preferred_username;  // username
$user->email;               // email
$user->token;               // full stdClass of JWT claims
```

---

Custom User Provider
--------------------

[](#custom-user-provider)

```
// app/Providers/CustomUserProvider.php
class CustomUserProvider implements UserProvider
{
    public function customRetrieveUser(stdClass $token, array $credentials): ?User
    {
        return User::updateOrCreate(
            ['keycloak_id' => $token->sub],
            [
                'name'  => $token->name ?? $token->preferred_username,
                'email' => $token->email,
            ]
        );
    }
    // ... other required methods
}
```

```
KEYCLOAK_USER_PROVIDER_CUSTOM_RETRIEVE_METHOD=customRetrieveUser
```

---

Keycloak 26 — Admin Configuration Checklist
-------------------------------------------

[](#keycloak-26--admin-configuration-checklist)

In your Keycloak 26 admin console:

**Realm Settings → Keys → RS256**: Copy the public key for `KEYCLOAK_REALM_PUBLIC_KEY`, or leave blank to use JWKS auto-discovery.

**Client Settings** (for your Laravel API client):

- Client protocol: `openid-connect`
- Access type: `confidential`
- Standard flow: **OFF** (API only)
- Direct access grants: **OFF**
- Service accounts enabled: **ON** (for M2M)

**Organizations** (Keycloak 26+):

- Realm Settings → General → Enable Organizations: **ON**
- OR start Keycloak with `--features=organization`
- Add the `organization` mapper to your client's token scope

**Token Mappers** — ensure your client has:

- `organization` claim mapper (type: Organization Membership)
- `realm roles` mapper
- `client roles` mapper

---

Testing
-------

[](#testing)

The package provides a powerful `ActingAsKeycloak` trait to mock authenticated states in your feature tests without needing a live Keycloak server.

```
namespace Tests\Feature;

use KeycloakGuard\Testing\ActingAsKeycloak;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    use ActingAsKeycloak;

    public function test_api_is_protected(): void
    {
        // Simple authentication
        $this->withKeycloakToken(['name' => 'John Doe'])
             ->getJson('/api/me')
             ->assertOk();

        // Authentication with specific roles
        $this->withKeycloakToken([
            'realm_access' => ['roles' => ['admin']]
        ])->getJson('/api/admin')->assertOk();
    }

    public function test_organization_access(): void
    {
        // Authenticate with organization membership
        $this->withKeycloakOrganization([
            'acme' => ['id' => 'org-1', 'name' => 'Acme Corp', 'roles' => ['member']]
        ])
        ->withOrganization('acme') // Select active org via header
        ->getJson('/api/projects')
        ->assertOk();
    }
}
```

---

License
-------

[](#license)

[MIT](LICENSE)

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance82

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

95d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/24268489?v=4)[Sumer Ahmed](/maintainers/sumer5020)[@sumer5020](https://github.com/sumer5020)

---

Top Contributors

[![sumer5020](https://avatars.githubusercontent.com/u/24268489?v=4)](https://github.com/sumer5020 "sumer5020 (6 commits)")

---

Tags

jwtlaravelauthoauth2saasmultitenancykeycloakguardorganizations

###  Code Quality

TestsPHPUnit

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/sumer5020-laravel-keycloak-guard/health.svg)

```
[![Health](https://phpackages.com/badges/sumer5020-laravel-keycloak-guard/health.svg)](https://phpackages.com/packages/sumer5020-laravel-keycloak-guard)
```

###  Alternatives

[laravel/socialite

Laravel wrapper around OAuth 1 &amp; OAuth 2 libraries.

5.7k104.3M831](/packages/laravel-socialite)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9742.3M121](/packages/roots-acorn)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

245.2k](/packages/aedart-athenaeum)[simplestats-io/laravel-client

Analytics for Laravel. Track visitors, registrations, and payments. Discover which channels actually drive revenue, not just traffic. Server-side, GDPR compliant, ad-blocker proof.

5019.3k](/packages/simplestats-io-laravel-client)[alajusticia/laravel-logins

Session management in Laravel apps, user notifications on new access, support for multiple separate remember tokens, IP geolocation, User-Agent parser

2013.2k](/packages/alajusticia-laravel-logins)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
