@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,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
|
+
```
|