{"id":47910799,"url":"https://github.com/sumer5020/laravel-keycloak-guard","last_synced_at":"2026-04-04T05:17:57.789Z","repository":{"id":346436428,"uuid":"1189886999","full_name":"sumer5020/laravel-keycloak-guard","owner":"sumer5020","description":"A Keycloak Guard for Laravel — robsontenorio/laravel-keycloak-guard fork","archived":false,"fork":false,"pushed_at":"2026-03-23T21:23:57.000Z","size":40,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-24T19:54:23.428Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sumer5020.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-23T19:07:00.000Z","updated_at":"2026-03-23T21:24:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sumer5020/laravel-keycloak-guard","commit_stats":null,"previous_names":["sumer5020/laravel-keycloak-guard"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/sumer5020/laravel-keycloak-guard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sumer5020%2Flaravel-keycloak-guard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sumer5020%2Flaravel-keycloak-guard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sumer5020%2Flaravel-keycloak-guard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sumer5020%2Flaravel-keycloak-guard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sumer5020","download_url":"https://codeload.github.com/sumer5020/laravel-keycloak-guard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sumer5020%2Flaravel-keycloak-guard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31388505,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T04:26:24.776Z","status":"ssl_error","status_checked_at":"2026-04-04T04:23:34.147Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-04T05:17:55.251Z","updated_at":"2026-04-04T05:17:57.781Z","avatar_url":"https://github.com/sumer5020.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Laravel Keycloak Guard\n\n**Note: This Version is not production ready yet, still in active development**\n\n\u003e **robsontenorio/laravel-keycloak-guard fork** — Full support for **Laravel 13**, **Keycloak 26+**, and the **Keycloak 26 Organizations** feature (`--features=organization`).\n\n[![Laravel](https://img.shields.io/badge/Laravel-13.x-red.svg)](https://laravel.com)\n[![Keycloak](https://img.shields.io/badge/Keycloak-26%2B-blue.svg)](https://www.keycloak.org)\n[![PHP](https://img.shields.io/badge/PHP-8.3%2B-purple.svg)](https://php.net)\n[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)\n\n---\n\n## What's New VS the Original Package:\n\n| Feature                            | Original | This Version |\n|------------------------------------|----------|--------------|\n| Laravel 13 support                 | ❌        | ✅            |\n| Keycloak 26 JWKS auto-discovery    | ❌        | ✅            |\n| Automatic key rotation support     | ❌        | ✅            |\n| Keycloak 26 Organizations          | ❌        | ✅            |\n| `KeycloakOrg` facade               | ❌        | ✅            |\n| `keycloak.org` middleware          | ❌        | ✅            |\n| `keycloak.org.role` middleware     | ❌        | ✅            |\n| `HasOrganizations` trait           | ❌        | ✅            |\n| `TokenUser` (no-DB mode)           | Partial  | ✅            |\n| `Auth::payload()`                  | ❌        | ✅            |\n| `Auth::roles()`                    | ❌        | ✅            |\n| `RealmResource` (API Resource)     | ❌        | ✅            |\n| `keycloak:doctor` command          | ❌        | ✅            |\n| `ActingAsKeycloak` (Testing Trait) | ❌        | ✅            |\n| PHP 8.3 constructor promotion      | ❌        | ✅            |\n\n---\n\n## Requirements\n\n- PHP **8.3+**\n- Laravel **12.x / 13.x**\n- Keycloak **21+** (Organizations require **26+**)\n\n---\n\n## Installation\n\n```bash\ncomposer require sumer5020/laravel-keycloak-guard\n```\n\nPublish the config:\n\n```bash\nphp artisan vendor:publish --tag=keycloak-config\n```\n\nOptionally publish migrations (only if using `organizations.sync_to_database`):\n\n```bash\nphp artisan vendor:publish --tag=keycloak-migrations\nphp artisan migrate\n```\n\n---\n\n## Health \u0026 Diagnostics\n\nThe package includes a \"Doctor\" command to help you troubleshoot connectivity and configuration issues.\n\n```bash\nphp artisan keycloak:doctor\n```\n\nIt performs the following checks:\n- **Configuration**: Verifies that required environment variables are set.\n- **Connectivity**: Attempts to fetch the OIDC discovery document (`.well-known/openid-configuration`) from Keycloak.\n- **Organizations**: Checks if the organization feature is correctly configured.\n\n---\n\n## API Resources\n\nIf 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`.\n\n```php\nuse KeycloakGuard\\Http\\Resources\\RealmResource;\n\nRoute::get('/keycloak/config', function () {\n    return new RealmResource([]);\n});\n```\n\nThis returns a standardized JSON response:\n```json\n{\n  \"realm\": \"my-realm\",\n  \"base_url\": \"https://keycloak.example.com\",\n  \"jwks_uri\": \"https://keycloak.example.com/realms/my-realm/protocol/openid-connect/certs\",\n  \"issuer\": \"https://keycloak.example.com/realms/my-realm\"\n}\n```\n\n---\n\n## Basic Setup\n\n### 1. Configure the Guard\n\n**`config/auth.php`**:\n\n```php\n'guards' =\u003e [\n    'api' =\u003e [\n        'driver'   =\u003e 'keycloak',\n        'provider' =\u003e 'users',\n    ],\n],\n\n'providers' =\u003e [\n    'users' =\u003e [\n        'driver' =\u003e 'eloquent',\n        'model'  =\u003e App\\Models\\User::class,\n    ],\n],\n```\n\n### 2. Environment Variables\n\n```env\n# ── Required ────────────────────────────────────────────────\nKEYCLOAK_BASE_URL=https://keycloak.example.com\nKEYCLOAK_REALM=my-realm\nKEYCLOAK_ALLOWED_RESOURCES=my-laravel-api\n\n# ── Key / Token Verification ────────────────────────────────\n# Option A: Static public key (from Realm Settings → Keys → RS256 → Public Key)\nKEYCLOAK_REALM_PUBLIC_KEY=MIIBIjANBgkq...\n\n# Option B: JWKS auto-discovery (recommended for Keycloak 26+, supports key rotation)\n# Leave KEYCLOAK_REALM_PUBLIC_KEY empty — the package constructs the JWKS URI automatically\n# Or set it explicitly:\nKEYCLOAK_JWKS_URI=https://keycloak.example.com/realms/my-realm/protocol/openid-connect/certs\nKEYCLOAK_JWKS_CACHE_TTL=3600\n\n# ── User Matching ────────────────────────────────────────────\nKEYCLOAK_TOKEN_PRINCIPAL_ATTRIBUTE=sub        # JWT claim used as identifier\nKEYCLOAK_USER_PROVIDER_CREDENTIAL=keycloak_id # DB column to match against\n\n# ── Optional ─────────────────────────────────────────────────\nKEYCLOAK_APPEND_DECODED_TOKEN=true            # Attach $user-\u003etoken (stdClass)\nKEYCLOAK_LEEWAY=30                            # Clock skew tolerance (seconds)\nKEYCLOAK_LOAD_USER_FROM_DATABASE=true\nKEYCLOAK_TOKEN_ENCRYPTION_ALGORITHM=RS256\n\n# ── Organizations (Keycloak 26+) ─────────────────────────────\nKEYCLOAK_ORGANIZATIONS_ENABLED=true\nKEYCLOAK_ORGANIZATION_HEADER=X-Organization   # Header for selecting active org\nKEYCLOAK_ORGANIZATION_REQUIRE=false           # 403 if no org in token?\nKEYCLOAK_ORGANIZATION_SYNC_DB=false           # Persist orgs to database?\n```\n\n### 3. Protect Routes\n\n```php\n// routes/api.php\n\n// Basic auth\nRoute::middleware('auth:api')-\u003egroup(function () {\n    Route::get('/me', fn(Request $r) =\u003e $r-\u003euser());\n});\n\n// Role-based access\nRoute::middleware(['auth:api', 'keycloak.role:admin'])-\u003egroup(function () {\n    Route::get('/admin', AdminController::class);\n});\n\n// Organization-scoped routes (Keycloak 26+)\nRoute::middleware(['auth:api', 'keycloak.org'])-\u003egroup(function () {\n    Route::apiResource('projects', ProjectController::class);\n\n    // Org admin only\n    Route::middleware('keycloak.org.role:admin')-\u003egroup(function () {\n        Route::apiResource('members', MemberController::class);\n    });\n});\n```\n\n---\n\n## Keycloak 26 Organizations\n\n### How It Works\n\nWhen Keycloak is started with `--features=organization`, it injects an `organization` claim into the JWT:\n\n```json\n{\n  \"sub\": \"f7a8b9c0-...\",\n  \"preferred_username\": \"john@acme.com\",\n  \"organization\": {\n    \"acme-corp\": {\n      \"id\": \"3fa85f64-...\",\n      \"name\": \"Acme Corporation\",\n      \"roles\": [\"admin\", \"member\"]\n    }\n  }\n}\n```\n\nFor users in **multiple organizations**, the client sends an `X-Organization` header to select the active one.\n\n### The `KeycloakOrg` Facade\n\n```php\nuse KeycloakGuard\\Facades\\KeycloakOrg;\n\n// Get the active organization for this request\n$org = KeycloakOrg::current();\n// =\u003e ['alias' =\u003e 'acme-corp', 'id' =\u003e '3fa85f64-...', 'name' =\u003e 'Acme Corporation', 'roles' =\u003e ['admin']]\n\n// Get all organizations the user belongs to (from the token)\n$orgs = KeycloakOrg::all();\n\n// Check membership\nKeycloakOrg::belongsTo('acme-corp');        // bool\nKeycloakOrg::hasOrganizations();             // bool\n\n// Role checks within the active organization\nKeycloakOrg::hasRole('admin');               // bool\nKeycloakOrg::hasRole('admin', 'acme-corp'); // bool (explicit org)\nKeycloakOrg::hasAnyRole(['admin', 'billing-manager']); // bool\n\n// Sync to DB (requires sync_to_database = true)\nKeycloakOrg::syncToDatabase(auth()-\u003euser());\n```\n\n### `HasOrganizations` Trait\n\nAdd to your `User` model for database relationships and convenient helpers:\n\n```php\nuse KeycloakGuard\\Traits\\HasOrganizations;\n\nclass User extends Authenticatable\n{\n    use HasOrganizations;\n}\n```\n\nThen use:\n\n```php\n// From JWT (no DB query)\n$user-\u003ecurrentOrganization();        // array|null\n$user-\u003ehasOrgRole('admin');          // bool\n$user-\u003ebelongsToOrg('acme-corp');    // bool\n\n// From database (requires sync_to_database)\n$user-\u003eorganizations()-\u003eget();\n$user-\u003eorganizations()-\u003ewhere('alias', 'acme-corp')-\u003efirst();\n```\n\n### Using Organization Context in Controllers\n\n```php\nclass ProjectController extends Controller\n{\n    public function index(): JsonResponse\n    {\n        // Array from JWT — no DB query\n        $org = KeycloakOrg::current();\n\n        // Eloquent model — only if sync_to_database = true\n        $orgModel = app('current_organization_model');\n\n        return response()-\u003ejson(\n            Project::where('organization_id', $orgModel-\u003eid)-\u003epaginate()\n        );\n    }\n\n    public function store(StoreProjectRequest $request): JsonResponse\n    {\n        $org = app('current_organization_model');\n\n        $project = Project::create([\n            ...$request-\u003evalidated(),\n            'organization_id' =\u003e $org-\u003eid,\n        ]);\n\n        return response()-\u003ejson($project, 201);\n    }\n}\n```\n\n---\n\n## Auth Guard Methods\n\n```php\nuse Illuminate\\Support\\Facades\\Auth;\n\n// Standard Laravel\nAuth::user();           // Authenticatable|null\nAuth::check();          // bool\nAuth::id();             // mixed\n\n// Keycloak-specific\nAuth::token();          // string|null  — full decoded token as JSON\nAuth::payload();        // stdClass|null — decoded token object\nAuth::roles();          // array — all realm + resource roles (with inheritance)\nAuth::roles('my-api'); // array         — resource-specific roles (includes realm roles)\nAuth::hasRole('admin'); // bool\nAuth::hasRole('editor', 'my-api'); // bool — resource-scoped\n\n// Organizations\nAuth::organizations();  // OrganizationService\n```\n\n---\n\n## Middleware Reference\n\n| Middleware                     | Description                           | Example                                      |\n|--------------------------------|---------------------------------------|----------------------------------------------|\n| `keycloak.role:admin`          | Requires realm or any resource role   | `middleware('keycloak.role:admin,editor')`   |\n| `keycloak.role:editor\\|my-api` | Requires role in specific resource    | `middleware('keycloak.role:editor\\|my-api')` |\n| `keycloak.org`                 | Resolves active org from JWT + header | `middleware('keycloak.org')`                 |\n| `keycloak.org.role:admin`      | Requires org-level role               | `middleware('keycloak.org.role:admin')`      |\n\n\u003e **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.\n\n---\n\n## No-Database Mode\n\nIf your API doesn't have a `users` table, set:\n\n```env\nKEYCLOAK_LOAD_USER_FROM_DATABASE=false\nKEYCLOAK_APPEND_DECODED_TOKEN=true\n```\n\n`Auth::user()` returns a `TokenUser` built from JWT claims:\n\n```php\n$user = Auth::user();\n$user-\u003esub;                 // UUID\n$user-\u003epreferred_username;  // username\n$user-\u003eemail;               // email\n$user-\u003etoken;               // full stdClass of JWT claims\n```\n\n---\n\n## Custom User Provider\n\n```php\n// app/Providers/CustomUserProvider.php\nclass CustomUserProvider implements UserProvider\n{\n    public function customRetrieveUser(stdClass $token, array $credentials): ?User\n    {\n        return User::updateOrCreate(\n            ['keycloak_id' =\u003e $token-\u003esub],\n            [\n                'name'  =\u003e $token-\u003ename ?? $token-\u003epreferred_username,\n                'email' =\u003e $token-\u003eemail,\n            ]\n        );\n    }\n    // ... other required methods\n}\n```\n\n```env\nKEYCLOAK_USER_PROVIDER_CUSTOM_RETRIEVE_METHOD=customRetrieveUser\n```\n\n---\n\n## Keycloak 26 — Admin Configuration Checklist\n\nIn your Keycloak 26 admin console:\n\n**Realm Settings → Keys → RS256**: Copy the public key for `KEYCLOAK_REALM_PUBLIC_KEY`, or leave blank to use JWKS auto-discovery.\n\n**Client Settings** (for your Laravel API client):\n- Client protocol: `openid-connect`\n- Access type: `confidential`\n- Standard flow: **OFF** (API only)\n- Direct access grants: **OFF**\n- Service accounts enabled: **ON** (for M2M)\n\n**Organizations** (Keycloak 26+):\n- Realm Settings → General → Enable Organizations: **ON**\n- OR start Keycloak with `--features=organization`\n- Add the `organization` mapper to your client's token scope\n\n**Token Mappers** — ensure your client has:\n- `organization` claim mapper (type: Organization Membership)\n- `realm roles` mapper\n- `client roles` mapper\n\n---\n\n## Testing\n\nThe package provides a powerful `ActingAsKeycloak` trait to mock authenticated states in your feature tests without needing a live Keycloak server.\n\n```php\nnamespace Tests\\Feature;\n\nuse KeycloakGuard\\Testing\\ActingAsKeycloak;\nuse Tests\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    use ActingAsKeycloak;\n\n    public function test_api_is_protected(): void\n    {\n        // Simple authentication\n        $this-\u003ewithKeycloakToken(['name' =\u003e 'John Doe'])\n             -\u003egetJson('/api/me')\n             -\u003eassertOk();\n             \n        // Authentication with specific roles\n        $this-\u003ewithKeycloakToken([\n            'realm_access' =\u003e ['roles' =\u003e ['admin']]\n        ])-\u003egetJson('/api/admin')-\u003eassertOk();\n    }\n\n    public function test_organization_access(): void\n    {\n        // Authenticate with organization membership\n        $this-\u003ewithKeycloakOrganization([\n            'acme' =\u003e ['id' =\u003e 'org-1', 'name' =\u003e 'Acme Corp', 'roles' =\u003e ['member']]\n        ])\n        -\u003ewithOrganization('acme') // Select active org via header\n        -\u003egetJson('/api/projects')\n        -\u003eassertOk();\n    }\n}\n```\n\n---\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsumer5020%2Flaravel-keycloak-guard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsumer5020%2Flaravel-keycloak-guard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsumer5020%2Flaravel-keycloak-guard/lists"}