@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.
Files changed (190) hide show
  1. package/dist/index.js +25449 -0
  2. package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
  3. package/drizzle/0001_wise_wolf_cub.sql +13 -0
  4. package/drizzle/0002_secret_spyke.sql +16 -0
  5. package/drizzle/0003_workspace_config_singleton.sql +16 -0
  6. package/drizzle/0004_terminal_claim_v120.sql +11 -0
  7. package/drizzle/0005_brainstorm_v124.sql +10 -0
  8. package/drizzle/0006_project_v140.sql +6 -0
  9. package/drizzle/0007_prd_wizard_v124.sql +14 -0
  10. package/drizzle/0008_workflow_artifact_v126.sql +24 -0
  11. package/drizzle/0009_project_import_v150.sql +7 -0
  12. package/drizzle/0010_quality_v151.sql +3 -0
  13. package/drizzle/meta/0000_snapshot.json +1335 -0
  14. package/drizzle/meta/0001_snapshot.json +1416 -0
  15. package/drizzle/meta/0002_snapshot.json +1510 -0
  16. package/drizzle/meta/_journal.json +83 -0
  17. package/package.json +41 -0
  18. package/plugin/.claude-plugin/marketplace.json +36 -0
  19. package/plugin/.claude-plugin/plugin.json +22 -0
  20. package/plugin/README.md +625 -0
  21. package/plugin/SKILLS.md +350 -0
  22. package/plugin/co2.svg +5 -0
  23. package/plugin/commands/brainstorm-loop.md +129 -0
  24. package/plugin/skills/conductor-defect/SKILL.md +898 -0
  25. package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
  26. package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
  27. package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
  28. package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
  29. package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
  30. package/plugin/skills/depgen-k8s/SKILL.md +468 -0
  31. package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
  32. package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
  33. package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
  34. package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
  35. package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
  36. package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
  37. package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
  38. package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
  39. package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
  40. package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
  41. package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
  42. package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
  43. package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
  44. package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
  45. package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
  46. package/plugin/skills/modelgen-relational/SKILL.md +707 -0
  47. package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
  48. package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
  49. package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
  50. package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
  51. package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
  52. package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
  53. package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
  54. package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
  55. package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
  56. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
  57. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
  58. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
  59. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
  60. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
  61. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
  62. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
  63. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
  64. package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
  65. package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
  66. package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
  67. package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
  68. package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
  69. package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
  70. package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
  71. package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
  72. package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
  73. package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
  74. package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
  75. package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
  76. package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
  77. package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
  78. package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
  79. package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
  80. package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
  81. package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
  82. package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
  83. package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
  84. package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
  85. package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
  86. package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
  87. package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
  88. package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
  89. package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
  90. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
  91. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
  92. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
  93. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
  94. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
  95. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
  96. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
  97. package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
  98. package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
  99. package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
  100. package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
  101. package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
  102. package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
  103. package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
  104. package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
  105. package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
  106. package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
  107. package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
  108. package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
  109. package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
  110. package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
  111. package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
  112. package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
  113. package/plugin/skills/testgen-functional/SKILL.md +1381 -0
  114. package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
  115. package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
  116. package/plugin/skills/util-projectinit/SKILL.md +307 -0
  117. package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
  118. package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
  119. package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
  120. package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
  121. package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
  122. package/plugin/skills/util-projectsync/SKILL.md +501 -0
  123. package/plugin/skills/util-updprd/SKILL.md +324 -0
  124. package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
  125. package/plugin/skills/util-ustagger/SKILL.md +154 -0
  126. package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
  127. package/static/assets/arc-DRKw7sjs.js +1 -0
  128. package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
  129. package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
  130. package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
  131. package/static/assets/channel-C2nVbuwv.js +1 -0
  132. package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
  133. package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
  134. package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
  135. package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
  136. package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
  137. package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
  138. package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
  139. package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
  140. package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
  141. package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
  142. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
  143. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
  144. package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
  145. package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
  146. package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
  147. package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  148. package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
  149. package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  150. package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
  151. package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
  152. package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
  153. package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
  154. package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
  155. package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
  156. package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
  157. package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
  158. package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
  159. package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
  160. package/static/assets/graph-C9eacEi8.js +1 -0
  161. package/static/assets/index-LZSQkLE2.css +41 -0
  162. package/static/assets/index-y7XDSgMG.js +495 -0
  163. package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
  164. package/static/assets/init-Gi6I4Gst.js +1 -0
  165. package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
  166. package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
  167. package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
  168. package/static/assets/katex-C5jXJg4s.js +257 -0
  169. package/static/assets/layout-DEXfKzaS.js +1 -0
  170. package/static/assets/linear-CLO--ryk.js +1 -0
  171. package/static/assets/map-Czzmt4hB.js +1 -0
  172. package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
  173. package/static/assets/ordinal-Cboi1Yqb.js +1 -0
  174. package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
  175. package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
  176. package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
  177. package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
  178. package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
  179. package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
  180. package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
  181. package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
  182. package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
  183. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
  184. package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
  185. package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
  186. package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
  187. package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
  188. package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
  189. package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
  190. package/static/index.html +24 -0
@@ -0,0 +1,551 @@
1
+ # Security Patterns — Keycloak OAuth2 Client with Spring Security
2
+
3
+ This reference describes the complete security architecture for the spec. Include all of
4
+ this content in Section 5 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 endpoints
18
+
19
+ The application uses server-side sessions (Spring's default). After successful OAuth2
20
+ login, the authenticated principal (`OidcUser`) is stored in the session. User identity
21
+ and roles are read from the OIDC ID token claims.
22
+
23
+ The flow:
24
+ ```
25
+ Browser → GET /protected-page → Spring Security (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
+ → Spring exchanges code for tokens (ID token + access token)
30
+ → KeycloakGrantedAuthoritiesMapper extracts roles from ID token
31
+ → Session created, OidcUser stored as principal
32
+ → 302 Redirect to original page
33
+ ```
34
+
35
+ ### Configuration in application.yml
36
+
37
+ ```yaml
38
+ spring:
39
+ security:
40
+ oauth2:
41
+ client:
42
+ registration:
43
+ keycloak:
44
+ client-id: {{KEYCLOAK_CLIENT_ID}}
45
+ client-secret: {{KEYCLOAK_CLIENT_SECRET}}
46
+ scope: openid,profile,email
47
+ provider:
48
+ keycloak:
49
+ issuer-uri: {{KEYCLOAK_ISSUER_URI}}
50
+
51
+ app:
52
+ security:
53
+ keycloak-client-id: {{KEYCLOAK_CLIENT_ID}}
54
+ ```
55
+
56
+ ---
57
+
58
+ ## SecurityConfig.java
59
+
60
+ Complete sample code. The application uses OAuth2 Login with Keycloak as the identity
61
+ provider. Sessions are managed by Spring (default behavior). No custom login page is
62
+ needed — unauthenticated users are automatically redirected to Keycloak.
63
+
64
+ ```java
65
+ package {{BASE_PACKAGE}}.config;
66
+
67
+ import {{BASE_PACKAGE}}.shared.security.CspNonceFilter;
68
+ import lombok.RequiredArgsConstructor;
69
+ import org.springframework.context.annotation.Bean;
70
+ import org.springframework.context.annotation.Configuration;
71
+ import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
72
+ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
73
+ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
74
+ import org.springframework.security.web.SecurityFilterChain;
75
+ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
76
+
77
+ @Configuration
78
+ @EnableWebSecurity
79
+ @EnableMethodSecurity
80
+ @RequiredArgsConstructor
81
+ public class SecurityConfig {
82
+
83
+ private final CspNonceFilter cspNonceFilter;
84
+ private final KeycloakGrantedAuthoritiesMapper keycloakGrantedAuthoritiesMapper;
85
+ private final LoginRedirectEntryPoint loginRedirectEntryPoint;
86
+
87
+ @Bean
88
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
89
+ http
90
+ .authorizeHttpRequests(auth -> auth
91
+ .requestMatchers("/static/**", "/assets/**", "/error").permitAll()
92
+ .anyRequest().authenticated()
93
+ )
94
+ .oauth2Login(oauth2 -> oauth2
95
+ .userInfoEndpoint(userInfo -> userInfo
96
+ .userAuthoritiesMapper(keycloakGrantedAuthoritiesMapper)
97
+ )
98
+ .defaultSuccessUrl("/home", true)
99
+ )
100
+ // Disable Spring's built-in logout to prevent LogoutFilter from intercepting
101
+ // before our custom LogoutController can handle Keycloak end_session
102
+ .logout(logout -> logout.disable())
103
+ .exceptionHandling(ex -> ex
104
+ .authenticationEntryPoint(loginRedirectEntryPoint)
105
+ )
106
+ .addFilterBefore(cspNonceFilter, UsernamePasswordAuthenticationFilter.class);
107
+
108
+ return http.build();
109
+ }
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## KeycloakGrantedAuthoritiesMapper.java
116
+
117
+ This class bridges Keycloak's OIDC ID token claim structure to Spring Security
118
+ `GrantedAuthority` objects. It extracts roles from both `realm_access` and
119
+ `resource_access` claims in the ID token.
120
+
121
+ Complete sample code:
122
+
123
+ ```java
124
+ package {{BASE_PACKAGE}}.config;
125
+
126
+ import org.springframework.beans.factory.annotation.Value;
127
+ import org.springframework.security.core.GrantedAuthority;
128
+ import org.springframework.security.core.authority.SimpleGrantedAuthority;
129
+ import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
130
+ import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
131
+ import org.springframework.stereotype.Component;
132
+ import java.util.Collection;
133
+ import java.util.Collections;
134
+ import java.util.HashSet;
135
+ import java.util.List;
136
+ import java.util.Map;
137
+ import java.util.Set;
138
+
139
+ @Component
140
+ public class KeycloakGrantedAuthoritiesMapper implements GrantedAuthoritiesMapper {
141
+
142
+ @Value("${app.security.keycloak-client-id}")
143
+ private String clientId;
144
+
145
+ @Override
146
+ public Collection<? extends GrantedAuthority> mapAuthorities(
147
+ Collection<? extends GrantedAuthority> authorities) {
148
+ Set<GrantedAuthority> mapped = new HashSet<>();
149
+ for (GrantedAuthority authority : authorities) {
150
+ if (authority instanceof OidcUserAuthority oidc) {
151
+ mapped.addAll(extractRealmRoles(oidc));
152
+ mapped.addAll(extractResourceRoles(oidc));
153
+ }
154
+ mapped.add(authority);
155
+ }
156
+ return mapped;
157
+ }
158
+
159
+ @SuppressWarnings("unchecked")
160
+ private Collection<GrantedAuthority> extractRealmRoles(OidcUserAuthority oidc) {
161
+ Map<String, Object> realmAccess = oidc.getIdToken().getClaim("realm_access");
162
+ if (realmAccess == null) return Collections.emptyList();
163
+ List<String> roles = (List<String>) realmAccess.get("roles");
164
+ if (roles == null) return Collections.emptyList();
165
+ return roles.stream()
166
+ .map(role -> (GrantedAuthority) new SimpleGrantedAuthority("ROLE_" + role))
167
+ .toList();
168
+ }
169
+
170
+ @SuppressWarnings("unchecked")
171
+ private Collection<GrantedAuthority> extractResourceRoles(OidcUserAuthority oidc) {
172
+ Map<String, Object> resourceAccess = oidc.getIdToken().getClaim("resource_access");
173
+ if (resourceAccess == null) return Collections.emptyList();
174
+ Map<String, Object> resource = (Map<String, Object>) resourceAccess.get(clientId);
175
+ if (resource == null) return Collections.emptyList();
176
+ List<String> roles = (List<String>) resource.get("roles");
177
+ if (roles == null) return Collections.emptyList();
178
+ return roles.stream()
179
+ .map(role -> (GrantedAuthority) new SimpleGrantedAuthority("ROLE_" + role))
180
+ .toList();
181
+ }
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Role-Based Access Control
188
+
189
+ ### At the endpoint level (method security)
190
+
191
+ ```java
192
+ @PreAuthorize("hasRole('ADMIN')")
193
+ @DeleteMapping("/{id}")
194
+ public String delete(@PathVariable String id) { ... }
195
+
196
+ @PreAuthorize("hasAnyRole('ADMIN', 'USER')")
197
+ @GetMapping
198
+ public String list(Model model) { ... }
199
+ ```
200
+
201
+ The `ROLE_` prefix is handled by the mapper. In `@PreAuthorize`, use role names
202
+ WITHOUT the prefix (Spring Security strips it automatically in `hasRole()`).
203
+
204
+ ### Role constants
205
+
206
+ Define roles as a constants class to avoid string duplication:
207
+
208
+ ```java
209
+ public final class Roles {
210
+ public static final String ADMIN = "ADMIN";
211
+ public static final String USER = "USER";
212
+ private Roles() {}
213
+ }
214
+ ```
215
+
216
+ ---
217
+
218
+ ## SecurityContextUtil.java
219
+
220
+ Utility to extract user information from the OIDC principal in the current security
221
+ context. Reads from the session-stored `OidcUser` principal.
222
+
223
+ ```java
224
+ package {{BASE_PACKAGE}}.shared.util;
225
+
226
+ import org.springframework.security.core.Authentication;
227
+ import org.springframework.security.core.GrantedAuthority;
228
+ import org.springframework.security.core.context.SecurityContextHolder;
229
+ import org.springframework.security.oauth2.core.oidc.user.OidcUser;
230
+ import org.springframework.stereotype.Component;
231
+ import java.util.Collections;
232
+ import java.util.Set;
233
+ import java.util.stream.Collectors;
234
+
235
+ @Component
236
+ public class SecurityContextUtil {
237
+
238
+ public String getCurrentUserId() {
239
+ OidcUser user = getOidcUser();
240
+ return user != null ? user.getSubject() : null;
241
+ }
242
+
243
+ public String getCurrentUsername() {
244
+ OidcUser user = getOidcUser();
245
+ return user != null ? user.getPreferredUsername() : null;
246
+ }
247
+
248
+ public String getCurrentUserEmail() {
249
+ OidcUser user = getOidcUser();
250
+ return user != null ? user.getEmail() : null;
251
+ }
252
+
253
+ public Set<String> getCurrentRoles() {
254
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
255
+ if (auth == null) return Collections.emptySet();
256
+ return auth.getAuthorities().stream()
257
+ .map(GrantedAuthority::getAuthority)
258
+ .filter(a -> a.startsWith("ROLE_"))
259
+ .map(a -> a.substring(5))
260
+ .collect(Collectors.toSet());
261
+ }
262
+
263
+ public OidcUser getOidcUser() {
264
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
265
+ if (auth != null && auth.getPrincipal() instanceof OidcUser oidcUser) {
266
+ return oidcUser;
267
+ }
268
+ return null;
269
+ }
270
+ }
271
+ ```
272
+
273
+ This class is injected via constructor into:
274
+ - `AuditAwareImpl` — for `@CreatedBy` / `@LastModifiedBy` fields
275
+ - Service classes — when business logic depends on the current user
276
+ - The MDC correlation filter — for logging (see Section 11)
277
+
278
+ ---
279
+
280
+ ## AuditAwareImpl.java
281
+
282
+ Implements `AuditorAware<String>` to populate `@CreatedBy` and `@LastModifiedBy` fields
283
+ on JPA entities or MongoDB documents. Uses constructor injection for `SecurityContextUtil`.
284
+
285
+ ```java
286
+ package {{BASE_PACKAGE}}.shared.audit;
287
+
288
+ import {{BASE_PACKAGE}}.shared.util.SecurityContextUtil;
289
+ import lombok.RequiredArgsConstructor;
290
+ import org.springframework.data.domain.AuditorAware;
291
+ import org.springframework.stereotype.Component;
292
+ import java.util.Optional;
293
+
294
+ @Component
295
+ @RequiredArgsConstructor
296
+ public class AuditAwareImpl implements AuditorAware<String> {
297
+
298
+ private final SecurityContextUtil securityContextUtil;
299
+
300
+ @Override
301
+ public Optional<String> getCurrentAuditor() {
302
+ return Optional.ofNullable(securityContextUtil.getCurrentUsername())
303
+ .or(() -> Optional.of("system"));
304
+ }
305
+ }
306
+ ```
307
+
308
+ ---
309
+
310
+ ## OIDC ID Token Structure Reference
311
+
312
+ For the spec, include a sample decoded ID token payload showing where Keycloak places
313
+ roles, so developers understand the claim structure:
314
+
315
+ ```json
316
+ {
317
+ "sub": "f1234-abcd-5678",
318
+ "preferred_username": "john.doe",
319
+ "email": "john.doe@example.com",
320
+ "realm_access": {
321
+ "roles": ["USER", "offline_access"]
322
+ },
323
+ "resource_access": {
324
+ "{{KEYCLOAK_CLIENT_ID}}": {
325
+ "roles": ["ADMIN"]
326
+ }
327
+ },
328
+ "scope": "openid profile email"
329
+ }
330
+ ```
331
+
332
+ This helps developers understand exactly what `KeycloakGrantedAuthoritiesMapper` is parsing.
333
+
334
+ ---
335
+
336
+ ## CSP Nonce Filter
337
+
338
+ For the web application, a Content Security Policy nonce is generated per request to allow
339
+ inline scripts while maintaining CSP security.
340
+
341
+ ```java
342
+ package {{BASE_PACKAGE}}.shared.security;
343
+
344
+ import jakarta.servlet.FilterChain;
345
+ import jakarta.servlet.ServletException;
346
+ import jakarta.servlet.http.HttpServletRequest;
347
+ import jakarta.servlet.http.HttpServletResponse;
348
+ import org.springframework.stereotype.Component;
349
+ import org.springframework.web.filter.OncePerRequestFilter;
350
+ import java.io.IOException;
351
+ import java.security.SecureRandom;
352
+ import java.util.Base64;
353
+
354
+ @Component
355
+ public class CspNonceFilter extends OncePerRequestFilter {
356
+
357
+ private static final SecureRandom RANDOM = new SecureRandom();
358
+
359
+ @Override
360
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
361
+ FilterChain filterChain) throws ServletException, IOException {
362
+ byte[] nonceBytes = new byte[32];
363
+ RANDOM.nextBytes(nonceBytes);
364
+ String nonce = Base64.getUrlEncoder().withoutPadding().encodeToString(nonceBytes);
365
+
366
+ request.setAttribute("cspNonce", nonce);
367
+ // 'unsafe-eval' required for Alpine.js v3 expression evaluation (new Function())
368
+ // 'data:' in font-src required for Vite-bundled font data URIs
369
+ response.setHeader("Content-Security-Policy",
370
+ "default-src 'self'; " +
371
+ "script-src 'self' 'nonce-" + nonce + "' 'unsafe-eval'; " +
372
+ "style-src 'self' 'unsafe-inline'; " +
373
+ "font-src 'self' data:; " +
374
+ "img-src 'self' data:; " +
375
+ "connect-src 'self';");
376
+
377
+ filterChain.doFilter(request, response);
378
+ }
379
+ }
380
+ ```
381
+
382
+ ---
383
+
384
+ ## AuthAware Mixin
385
+
386
+ Mixin interface for controllers to extract user information from the OIDC principal.
387
+
388
+ ```java
389
+ package {{BASE_PACKAGE}}.shared.security;
390
+
391
+ import {{BASE_PACKAGE}}.shared.fragment.UserProfile;
392
+ import jakarta.servlet.http.HttpServletRequest;
393
+ import org.springframework.security.core.context.SecurityContextHolder;
394
+ import org.springframework.security.oauth2.core.oidc.user.OidcUser;
395
+
396
+ public interface AuthAware {
397
+ default UserProfile currentUser(HttpServletRequest req) {
398
+ var auth = SecurityContextHolder.getContext().getAuthentication();
399
+ if (auth != null && auth.getPrincipal() instanceof OidcUser oidcUser) {
400
+ return new UserProfile(
401
+ oidcUser.getSubject(),
402
+ oidcUser.getPreferredUsername(),
403
+ oidcUser.getEmail()
404
+ );
405
+ }
406
+ return UserProfile.anonymous();
407
+ }
408
+ }
409
+ ```
410
+
411
+ ---
412
+
413
+ ## LoginRedirectEntryPoint
414
+
415
+ Custom `AuthenticationEntryPoint` that redirects to the login page when authentication
416
+ fails (e.g., session expired, no valid session). For HTMX requests, it returns an
417
+ `HX-Redirect` header instead of a 302 redirect so that htmx can handle the full page
418
+ navigation.
419
+
420
+ This prevents the application from showing a raw 401 error page when the user's session
421
+ expires while they are idle.
422
+
423
+ ```java
424
+ package {{BASE_PACKAGE}}.shared.security;
425
+
426
+ import jakarta.servlet.ServletException;
427
+ import jakarta.servlet.http.HttpServletRequest;
428
+ import jakarta.servlet.http.HttpServletResponse;
429
+ import org.springframework.security.core.AuthenticationException;
430
+ import org.springframework.security.web.AuthenticationEntryPoint;
431
+ import org.springframework.stereotype.Component;
432
+
433
+ import java.io.IOException;
434
+
435
+ @Component
436
+ public class LoginRedirectEntryPoint implements AuthenticationEntryPoint {
437
+
438
+ @Override
439
+ public void commence(HttpServletRequest request, HttpServletResponse response,
440
+ AuthenticationException authException) throws IOException, ServletException {
441
+ if ("true".equals(request.getHeader("HX-Request"))) {
442
+ // HTMX request — send HX-Redirect header so htmx triggers full page navigation
443
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
444
+ response.setHeader("HX-Redirect", "/login");
445
+ } else {
446
+ // Regular browser request — standard redirect
447
+ response.sendRedirect("/login");
448
+ }
449
+ }
450
+ }
451
+ ```
452
+
453
+ Wire this into SecurityConfig via:
454
+ ```java
455
+ .exceptionHandling(ex -> ex
456
+ .authenticationEntryPoint(loginRedirectEntryPoint)
457
+ )
458
+ ```
459
+
460
+ ---
461
+
462
+ ## LogoutController
463
+
464
+ Custom logout controller that handles Keycloak end-session. Spring Security's built-in
465
+ `LogoutFilter` must be disabled (`.logout(logout -> logout.disable())`) because it
466
+ intercepts GET `/logout` before the custom controller when CSRF is disabled.
467
+
468
+ The controller:
469
+ 1. Reads the OIDC ID token from the authenticated session
470
+ 2. Builds the Keycloak `end_session_endpoint` URL with `id_token_hint` and
471
+ `post_logout_redirect_uri`
472
+ 3. Invalidates the HTTP session
473
+ 4. Redirects to Keycloak's end_session_endpoint to destroy the SSO session
474
+
475
+ **Prerequisite:** The Keycloak client must have `post_logout_redirect_uris` configured
476
+ (e.g., `http://localhost:8080/*`) via the Keycloak admin console or admin API.
477
+
478
+ ```java
479
+ package {{BASE_PACKAGE}}.config;
480
+
481
+ import jakarta.servlet.http.HttpServletRequest;
482
+ import lombok.RequiredArgsConstructor;
483
+ import org.springframework.beans.factory.annotation.Value;
484
+ import org.springframework.security.core.Authentication;
485
+ import org.springframework.security.core.context.SecurityContextHolder;
486
+ import org.springframework.security.oauth2.core.oidc.user.OidcUser;
487
+ import org.springframework.stereotype.Controller;
488
+ import org.springframework.web.bind.annotation.GetMapping;
489
+ import org.springframework.web.util.UriComponentsBuilder;
490
+
491
+ @Controller
492
+ @RequiredArgsConstructor
493
+ public class LogoutController {
494
+
495
+ @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri}")
496
+ private String issuerUri;
497
+
498
+ @GetMapping("/logout")
499
+ public String logout(HttpServletRequest request) {
500
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
501
+ String idToken = null;
502
+
503
+ if (auth != null && auth.getPrincipal() instanceof OidcUser oidcUser) {
504
+ idToken = oidcUser.getIdToken().getTokenValue();
505
+ }
506
+
507
+ // Invalidate the HTTP session
508
+ request.getSession().invalidate();
509
+ SecurityContextHolder.clearContext();
510
+
511
+ // Build Keycloak end_session_endpoint URL
512
+ String endSessionUrl = UriComponentsBuilder
513
+ .fromUriString(issuerUri + "/protocol/openid-connect/logout")
514
+ .queryParam("post_logout_redirect_uri", getBaseUrl(request))
515
+ .queryParamIfPresent("id_token_hint",
516
+ idToken != null ? java.util.Optional.of(idToken) : java.util.Optional.empty())
517
+ .toUriString();
518
+
519
+ return "redirect:" + endSessionUrl;
520
+ }
521
+
522
+ private String getBaseUrl(HttpServletRequest request) {
523
+ return request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
524
+ }
525
+ }
526
+ ```
527
+
528
+ ---
529
+
530
+ ## ThemeAware Mixin
531
+
532
+ Mixin interface for controllers to read the theme cookie.
533
+
534
+ ```java
535
+ package {{BASE_PACKAGE}}.shared.security;
536
+
537
+ import jakarta.servlet.http.Cookie;
538
+ import jakarta.servlet.http.HttpServletRequest;
539
+ import java.util.Arrays;
540
+
541
+ public interface ThemeAware {
542
+ default String theme(HttpServletRequest req) {
543
+ if (req.getCookies() == null) return "light";
544
+ return Arrays.stream(req.getCookies())
545
+ .filter(c -> "theme".equals(c.getName()))
546
+ .map(Cookie::getValue)
547
+ .findFirst()
548
+ .orElse("light");
549
+ }
550
+ }
551
+ ```