@rashidee/co2 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- package/static/index.html +24 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# Security Patterns — Keycloak OAuth2 Client with Laravel Socialite
|
|
2
|
+
|
|
3
|
+
This reference describes the complete security architecture for the spec. Include all of
|
|
4
|
+
this content in Section 6 of the generated specification.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Architecture Overview
|
|
9
|
+
|
|
10
|
+
The application operates as an OAuth2 Client using Authorization Code flow with OIDC.
|
|
11
|
+
Keycloak is the external identity provider that handles login UI, user registration,
|
|
12
|
+
and role management. The application's responsibilities are:
|
|
13
|
+
|
|
14
|
+
1. Redirect unauthenticated users to Keycloak login page
|
|
15
|
+
2. Handle the OAuth2 callback and establish an HTTP session
|
|
16
|
+
3. Extract roles from OIDC ID token claims
|
|
17
|
+
4. Enforce role-based access control on routes
|
|
18
|
+
|
|
19
|
+
The application uses server-side sessions (Laravel's default). After successful OAuth2
|
|
20
|
+
login, the authenticated user is stored in the session. User identity and roles are
|
|
21
|
+
read from the OIDC ID token claims.
|
|
22
|
+
|
|
23
|
+
The flow:
|
|
24
|
+
```
|
|
25
|
+
Browser → GET /protected-page → Auth Middleware (not authenticated)
|
|
26
|
+
→ 302 Redirect to Keycloak /auth?response_type=code&client_id=...
|
|
27
|
+
→ User logs in at Keycloak
|
|
28
|
+
→ Keycloak redirects back with authorization code
|
|
29
|
+
→ Laravel exchanges code for tokens (ID token + access token) via Socialite
|
|
30
|
+
→ KeycloakRoleMapper extracts roles from ID token
|
|
31
|
+
→ Session created, User stored as authenticated principal
|
|
32
|
+
→ 302 Redirect to original page
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Configuration in .env
|
|
36
|
+
|
|
37
|
+
```env
|
|
38
|
+
KEYCLOAK_CLIENT_ID={{KEYCLOAK_CLIENT_ID}}
|
|
39
|
+
KEYCLOAK_CLIENT_SECRET={{KEYCLOAK_CLIENT_SECRET}}
|
|
40
|
+
KEYCLOAK_REALM={{KEYCLOAK_REALM}}
|
|
41
|
+
KEYCLOAK_BASE_URL={{KEYCLOAK_BASE_URL}}
|
|
42
|
+
KEYCLOAK_REDIRECT_URI=${APP_URL}/auth/callback
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Configuration in config/services.php
|
|
46
|
+
|
|
47
|
+
```php
|
|
48
|
+
'keycloak' => [
|
|
49
|
+
'client_id' => env('KEYCLOAK_CLIENT_ID'),
|
|
50
|
+
'client_secret' => env('KEYCLOAK_CLIENT_SECRET'),
|
|
51
|
+
'redirect' => env('KEYCLOAK_REDIRECT_URI'),
|
|
52
|
+
'base_url' => env('KEYCLOAK_BASE_URL'),
|
|
53
|
+
'realm' => env('KEYCLOAK_REALM'),
|
|
54
|
+
],
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Socialite Keycloak Provider Setup
|
|
60
|
+
|
|
61
|
+
Install and configure the Keycloak Socialite provider:
|
|
62
|
+
|
|
63
|
+
```php
|
|
64
|
+
// config/app.php or AppServiceProvider boot()
|
|
65
|
+
// The socialiteproviders/keycloak package auto-registers via EventServiceProvider
|
|
66
|
+
|
|
67
|
+
// In app/Providers/EventServiceProvider.php
|
|
68
|
+
protected $listen = [
|
|
69
|
+
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
|
|
70
|
+
\SocialiteProviders\Keycloak\KeycloakExtendSocialite::class . '@handle',
|
|
71
|
+
],
|
|
72
|
+
];
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Auth Controller
|
|
78
|
+
|
|
79
|
+
Complete controller handling OAuth2 login flow with Keycloak:
|
|
80
|
+
|
|
81
|
+
```php
|
|
82
|
+
namespace App\Http\Controllers\Auth;
|
|
83
|
+
|
|
84
|
+
use App\Http\Controllers\Controller;
|
|
85
|
+
use App\Models\User;
|
|
86
|
+
use App\Services\KeycloakRoleMapper;
|
|
87
|
+
use Illuminate\Http\RedirectResponse;
|
|
88
|
+
use Illuminate\Support\Facades\Auth;
|
|
89
|
+
use Laravel\Socialite\Facades\Socialite;
|
|
90
|
+
|
|
91
|
+
class KeycloakAuthController extends Controller
|
|
92
|
+
{
|
|
93
|
+
public function __construct(
|
|
94
|
+
private readonly KeycloakRoleMapper $roleMapper,
|
|
95
|
+
) {}
|
|
96
|
+
|
|
97
|
+
public function redirect(): RedirectResponse
|
|
98
|
+
{
|
|
99
|
+
return Socialite::driver('keycloak')
|
|
100
|
+
->scopes(['openid', 'profile', 'email'])
|
|
101
|
+
->redirect();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public function callback(): RedirectResponse
|
|
105
|
+
{
|
|
106
|
+
$keycloakUser = Socialite::driver('keycloak')->user();
|
|
107
|
+
|
|
108
|
+
$user = User::updateOrCreate(
|
|
109
|
+
['keycloak_id' => $keycloakUser->getId()],
|
|
110
|
+
[
|
|
111
|
+
'name' => $keycloakUser->getName(),
|
|
112
|
+
'email' => $keycloakUser->getEmail(),
|
|
113
|
+
'username' => $keycloakUser->getNickname(),
|
|
114
|
+
]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Sync roles from Keycloak token
|
|
118
|
+
$roles = $this->roleMapper->extractRoles($keycloakUser->accessTokenResponseBody);
|
|
119
|
+
$user->syncRoles($roles);
|
|
120
|
+
|
|
121
|
+
Auth::login($user, remember: true);
|
|
122
|
+
|
|
123
|
+
return redirect()->intended('/');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public function logout(): RedirectResponse
|
|
127
|
+
{
|
|
128
|
+
Auth::logout();
|
|
129
|
+
request()->session()->invalidate();
|
|
130
|
+
request()->session()->regenerateToken();
|
|
131
|
+
|
|
132
|
+
// Redirect to Keycloak logout endpoint
|
|
133
|
+
$keycloakLogoutUrl = config('services.keycloak.base_url')
|
|
134
|
+
. '/realms/' . config('services.keycloak.realm')
|
|
135
|
+
. '/protocol/openid-connect/logout'
|
|
136
|
+
. '?redirect_uri=' . urlencode(config('app.url'));
|
|
137
|
+
|
|
138
|
+
return redirect($keycloakLogoutUrl);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## KeycloakRoleMapper
|
|
146
|
+
|
|
147
|
+
This class bridges Keycloak's OIDC token claim structure to spatie/laravel-permission
|
|
148
|
+
roles. It extracts roles from both `realm_access` and `resource_access` claims in the
|
|
149
|
+
token response.
|
|
150
|
+
|
|
151
|
+
```php
|
|
152
|
+
namespace App\Services;
|
|
153
|
+
|
|
154
|
+
use Illuminate\Support\Arr;
|
|
155
|
+
|
|
156
|
+
class KeycloakRoleMapper
|
|
157
|
+
{
|
|
158
|
+
public function __construct(
|
|
159
|
+
private readonly string $clientId,
|
|
160
|
+
) {}
|
|
161
|
+
|
|
162
|
+
public static function make(): self
|
|
163
|
+
{
|
|
164
|
+
return new self(config('services.keycloak.client_id'));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Extract roles from the Keycloak token response body.
|
|
169
|
+
*
|
|
170
|
+
* @param array $tokenResponse The full token response including 'id_token' claims
|
|
171
|
+
* @return array<string> List of role names (e.g., ['HUB_ADMINISTRATOR', 'USER'])
|
|
172
|
+
*/
|
|
173
|
+
public function extractRoles(array $tokenResponse): array
|
|
174
|
+
{
|
|
175
|
+
$roles = [];
|
|
176
|
+
|
|
177
|
+
// Decode the ID token to get claims
|
|
178
|
+
$idToken = $tokenResponse['id_token'] ?? null;
|
|
179
|
+
if ($idToken) {
|
|
180
|
+
$claims = json_decode(
|
|
181
|
+
base64_decode(explode('.', $idToken)[1]),
|
|
182
|
+
associative: true
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Extract realm roles
|
|
186
|
+
$realmRoles = Arr::get($claims, 'realm_access.roles', []);
|
|
187
|
+
$roles = array_merge($roles, $realmRoles);
|
|
188
|
+
|
|
189
|
+
// Extract client-specific roles
|
|
190
|
+
$clientRoles = Arr::get(
|
|
191
|
+
$claims,
|
|
192
|
+
"resource_access.{$this->clientId}.roles",
|
|
193
|
+
[]
|
|
194
|
+
);
|
|
195
|
+
$roles = array_merge($roles, $clientRoles);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Normalize: uppercase, unique
|
|
199
|
+
return array_unique(array_map('strtoupper', $roles));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Route Configuration
|
|
207
|
+
|
|
208
|
+
```php
|
|
209
|
+
// routes/web.php
|
|
210
|
+
|
|
211
|
+
use App\Http\Controllers\Auth\KeycloakAuthController;
|
|
212
|
+
|
|
213
|
+
// Public routes
|
|
214
|
+
Route::get('/auth/redirect', [KeycloakAuthController::class, 'redirect'])
|
|
215
|
+
->name('auth.redirect');
|
|
216
|
+
Route::get('/auth/callback', [KeycloakAuthController::class, 'callback'])
|
|
217
|
+
->name('auth.callback');
|
|
218
|
+
|
|
219
|
+
// Authenticated routes
|
|
220
|
+
Route::middleware(['auth', 'verified'])->group(function () {
|
|
221
|
+
Route::post('/logout', [KeycloakAuthController::class, 'logout'])
|
|
222
|
+
->name('logout');
|
|
223
|
+
|
|
224
|
+
// Module routes loaded from modules...
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Role-Based Access Control
|
|
231
|
+
|
|
232
|
+
### Using spatie/laravel-permission middleware
|
|
233
|
+
|
|
234
|
+
```php
|
|
235
|
+
// In module route files
|
|
236
|
+
Route::middleware(['role:HUB_ADMINISTRATOR'])->group(function () {
|
|
237
|
+
Route::get('/corridor', [CorridorPageController::class, 'index']);
|
|
238
|
+
Route::get('/corridor/{id}', [CorridorPageController::class, 'show']);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
Route::middleware(['role:HUB_ADMINISTRATOR|HUB_OPERATION_SUPPORT'])->group(function () {
|
|
242
|
+
Route::get('/employer', [EmployerPageController::class, 'index']);
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### In Blade templates
|
|
247
|
+
|
|
248
|
+
```blade
|
|
249
|
+
@role('HUB_ADMINISTRATOR')
|
|
250
|
+
<a href="/corridor">Corridor Management</a>
|
|
251
|
+
@endrole
|
|
252
|
+
|
|
253
|
+
@hasanyrole('HUB_ADMINISTRATOR|HUB_OPERATION_SUPPORT')
|
|
254
|
+
<a href="/employer">Employers</a>
|
|
255
|
+
@endhasanyrole
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### In controllers (Gate authorization)
|
|
259
|
+
|
|
260
|
+
```php
|
|
261
|
+
use Illuminate\Support\Facades\Gate;
|
|
262
|
+
|
|
263
|
+
public function destroy(string $id)
|
|
264
|
+
{
|
|
265
|
+
Gate::authorize('delete-order');
|
|
266
|
+
$this->orderService->delete($id);
|
|
267
|
+
return redirect()->route('order.index');
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Role constants
|
|
272
|
+
|
|
273
|
+
Define roles as a constants class to avoid string duplication:
|
|
274
|
+
|
|
275
|
+
```php
|
|
276
|
+
namespace App\Constants;
|
|
277
|
+
|
|
278
|
+
final class Roles
|
|
279
|
+
{
|
|
280
|
+
public const HUB_ADMINISTRATOR = 'HUB_ADMINISTRATOR';
|
|
281
|
+
public const HUB_OPERATION_SUPPORT = 'HUB_OPERATION_SUPPORT';
|
|
282
|
+
|
|
283
|
+
private function __construct() {}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## SecurityContext Helper
|
|
290
|
+
|
|
291
|
+
Utility to extract user information from the authenticated session. Wraps Laravel's
|
|
292
|
+
`auth()` helper for consistent access across services.
|
|
293
|
+
|
|
294
|
+
```php
|
|
295
|
+
namespace App\Services;
|
|
296
|
+
|
|
297
|
+
class SecurityContext
|
|
298
|
+
{
|
|
299
|
+
public function getCurrentUserId(): ?string
|
|
300
|
+
{
|
|
301
|
+
return auth()->id();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
public function getCurrentUsername(): ?string
|
|
305
|
+
{
|
|
306
|
+
return auth()->user()?->username;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
public function getCurrentUserEmail(): ?string
|
|
310
|
+
{
|
|
311
|
+
return auth()->user()?->email;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
public function getCurrentRoles(): array
|
|
315
|
+
{
|
|
316
|
+
$user = auth()->user();
|
|
317
|
+
return $user ? $user->getRoleNames()->toArray() : [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public function hasRole(string $role): bool
|
|
321
|
+
{
|
|
322
|
+
return auth()->user()?->hasRole($role) ?? false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
This class is injected via constructor into:
|
|
328
|
+
- `BlameableTrait` — for `created_by` / `updated_by` fields
|
|
329
|
+
- Service classes — when business logic depends on the current user
|
|
330
|
+
- Correlation ID middleware — for logging
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## BlameableTrait
|
|
335
|
+
|
|
336
|
+
Trait for Eloquent models to automatically populate `created_by` and `updated_by` fields.
|
|
337
|
+
|
|
338
|
+
```php
|
|
339
|
+
namespace App\Traits;
|
|
340
|
+
|
|
341
|
+
trait Blameable
|
|
342
|
+
{
|
|
343
|
+
public static function bootBlameable(): void
|
|
344
|
+
{
|
|
345
|
+
static::creating(function ($model) {
|
|
346
|
+
if (auth()->check()) {
|
|
347
|
+
$model->created_by = auth()->user()->username ?? auth()->id();
|
|
348
|
+
$model->updated_by = auth()->user()->username ?? auth()->id();
|
|
349
|
+
} else {
|
|
350
|
+
$model->created_by = 'system';
|
|
351
|
+
$model->updated_by = 'system';
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
static::updating(function ($model) {
|
|
356
|
+
$model->updated_by = auth()->check()
|
|
357
|
+
? (auth()->user()->username ?? auth()->id())
|
|
358
|
+
: 'system';
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Usage in models:
|
|
365
|
+
```php
|
|
366
|
+
class Order extends Model
|
|
367
|
+
{
|
|
368
|
+
use Blameable;
|
|
369
|
+
|
|
370
|
+
// created_by and updated_by are auto-populated
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## OIDC ID Token Structure Reference
|
|
377
|
+
|
|
378
|
+
For the spec, include a sample decoded ID token payload showing where Keycloak places
|
|
379
|
+
roles, so developers understand the claim structure:
|
|
380
|
+
|
|
381
|
+
```json
|
|
382
|
+
{
|
|
383
|
+
"sub": "f1234-abcd-5678",
|
|
384
|
+
"preferred_username": "john.doe",
|
|
385
|
+
"email": "john.doe@example.com",
|
|
386
|
+
"realm_access": {
|
|
387
|
+
"roles": ["USER", "offline_access"]
|
|
388
|
+
},
|
|
389
|
+
"resource_access": {
|
|
390
|
+
"{{KEYCLOAK_CLIENT_ID}}": {
|
|
391
|
+
"roles": ["HUB_ADMINISTRATOR"]
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"scope": "openid profile email"
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
This helps developers understand exactly what `KeycloakRoleMapper` is parsing.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## CSP Nonce Middleware
|
|
403
|
+
|
|
404
|
+
Content Security Policy nonce generated per request to allow inline scripts while
|
|
405
|
+
maintaining CSP security.
|
|
406
|
+
|
|
407
|
+
```php
|
|
408
|
+
namespace App\Http\Middleware;
|
|
409
|
+
|
|
410
|
+
use Closure;
|
|
411
|
+
use Illuminate\Http\Request;
|
|
412
|
+
use Illuminate\Support\Facades\Vite;
|
|
413
|
+
use Illuminate\Support\Str;
|
|
414
|
+
use Symfony\Component\HttpFoundation\Response;
|
|
415
|
+
|
|
416
|
+
class CspNonceMiddleware
|
|
417
|
+
{
|
|
418
|
+
public function handle(Request $request, Closure $next): Response
|
|
419
|
+
{
|
|
420
|
+
$nonce = Str::random(32);
|
|
421
|
+
|
|
422
|
+
// Share nonce with Vite for script tags
|
|
423
|
+
Vite::useCspNonce($nonce);
|
|
424
|
+
|
|
425
|
+
// Store nonce for Blade templates
|
|
426
|
+
view()->share('cspNonce', $nonce);
|
|
427
|
+
|
|
428
|
+
$response = $next($request);
|
|
429
|
+
|
|
430
|
+
$response->headers->set('Content-Security-Policy',
|
|
431
|
+
"default-src 'self'; " .
|
|
432
|
+
"script-src 'self' 'nonce-{$nonce}'; " .
|
|
433
|
+
"style-src 'self' 'unsafe-inline';"
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
return $response;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Register in `bootstrap/app.php`:
|
|
442
|
+
```php
|
|
443
|
+
->withMiddleware(function (Middleware $middleware) {
|
|
444
|
+
$middleware->web(append: [
|
|
445
|
+
\App\Http\Middleware\CspNonceMiddleware::class,
|
|
446
|
+
]);
|
|
447
|
+
})
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## ThemeMiddleware
|
|
453
|
+
|
|
454
|
+
Middleware to read the theme cookie and share with all views.
|
|
455
|
+
|
|
456
|
+
```php
|
|
457
|
+
namespace App\Http\Middleware;
|
|
458
|
+
|
|
459
|
+
use Closure;
|
|
460
|
+
use Illuminate\Http\Request;
|
|
461
|
+
use Symfony\Component\HttpFoundation\Response;
|
|
462
|
+
|
|
463
|
+
class ThemeMiddleware
|
|
464
|
+
{
|
|
465
|
+
public function handle(Request $request, Closure $next): Response
|
|
466
|
+
{
|
|
467
|
+
$theme = $request->cookie('theme', 'light');
|
|
468
|
+
view()->share('theme', $theme);
|
|
469
|
+
|
|
470
|
+
return $next($request);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## User Model for Keycloak
|
|
478
|
+
|
|
479
|
+
```php
|
|
480
|
+
namespace App\Models;
|
|
481
|
+
|
|
482
|
+
use App\Traits\Blameable;
|
|
483
|
+
use Illuminate\Foundation\Auth\User as Authenticatable;
|
|
484
|
+
use Spatie\Permission\Traits\HasRoles;
|
|
485
|
+
|
|
486
|
+
class User extends Authenticatable
|
|
487
|
+
{
|
|
488
|
+
use HasRoles, Blameable;
|
|
489
|
+
|
|
490
|
+
protected $fillable = [
|
|
491
|
+
'keycloak_id',
|
|
492
|
+
'name',
|
|
493
|
+
'email',
|
|
494
|
+
'username',
|
|
495
|
+
];
|
|
496
|
+
|
|
497
|
+
protected $hidden = [
|
|
498
|
+
'remember_token',
|
|
499
|
+
];
|
|
500
|
+
}
|
|
501
|
+
```
|