go-duck-cli 1.1.16 → 1.1.19

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.
@@ -96,6 +96,11 @@ services:
96
96
  - "${dbPort}:5432"
97
97
  volumes:
98
98
  - postgres_data:/var/lib/postgresql/data
99
+ healthcheck:
100
+ test: ["CMD-SHELL", "pg_isready -U ${config.datasource?.username || 'postgres'} -d ${config.datasource?.database || 'go_duck_master'}"]
101
+ interval: 5s
102
+ timeout: 5s
103
+ retries: 5
99
104
  networks:
100
105
  - go-duck-net
101
106
 
@@ -161,6 +166,13 @@ services:
161
166
  environment:
162
167
  KEYCLOAK_ADMIN: admin
163
168
  KEYCLOAK_ADMIN_PASSWORD: admin
169
+ KC_DB: postgres
170
+ KC_DB_URL: jdbc:postgresql://postgres:5432/${config.datasource?.database || 'go_duck_master'}
171
+ KC_DB_USERNAME: ${config.datasource?.username || 'postgres'}
172
+ KC_DB_PASSWORD: ${config.datasource?.password || 'password'}
173
+ depends_on:
174
+ postgres:
175
+ condition: service_healthy
164
176
  volumes:
165
177
  - ./keycloak/realm-config:/opt/keycloak/data/import
166
178
  ports:
@@ -234,7 +246,7 @@ services:
234
246
  - GO_DUCK_ELASTICSEARCH_ADDRESSES=http://elasticsearch:9200
235
247
  depends_on:
236
248
  postgres:
237
- condition: service_started
249
+ condition: service_healthy
238
250
  redis:
239
251
  condition: service_started
240
252
  mosquitto:
@@ -248,8 +260,7 @@ services:
248
260
 
249
261
  networks:
250
262
  go-duck-net:
251
- external: true
252
- name: devops_go-duck-net
263
+ driver: bridge
253
264
  `;
254
265
 
255
266
  // --- 5. MQTT Broker Config ---
@@ -336,7 +336,14 @@ func NewGRPCServer(conf *config.Config, repo *repository.Repository) *grpc.Serve
336
336
  grpc.Middleware(
337
337
  recovery.Recovery(),
338
338
  kjwt.Server(func(token *jwt.Token) (interface{}, error) {
339
- return []byte(conf.GoDuck.Security.KeycloakAppClientSecret), nil
339
+ if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
340
+ return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
341
+ }
342
+ kid, ok := token.Header["kid"].(string)
343
+ if !ok {
344
+ return nil, fmt.Errorf("missing kid header")
345
+ }
346
+ return my_middleware.GetPublicKeyByKid(kid, conf)
340
347
  }, kjwt.WithClaims(func() jwt.Claims { return &jwt.MapClaims{} })),
341
348
  TenantServerInterceptor(conf, repo.DB),
342
349
  ),
@@ -10,15 +10,134 @@ export const generateSecurityMiddleware = async (config, outputDir) => {
10
10
  package middleware
11
11
 
12
12
  import (
13
+ "crypto/rsa"
14
+ "encoding/base64"
15
+ "encoding/json"
13
16
  "fmt"
17
+ "math/big"
14
18
  "net/http"
15
19
  "strings"
20
+ "sync"
21
+ "time"
16
22
 
17
23
  "github.com/gin-gonic/gin"
18
24
  "github.com/golang-jwt/jwt/v4"
19
25
  "{{app_name}}/config"
20
26
  )
21
27
 
28
+ type JWK struct {
29
+ Kid string \`json:"kid"\`
30
+ Kty string \`json:"kty"\`
31
+ Alg string \`json:"alg"\`
32
+ Use string \`json:"use"\`
33
+ N string \`json:"n"\`
34
+ E string \`json:"e"\`
35
+ X5c []string \`json:"x5c"\`
36
+ }
37
+
38
+ type JWKKeySet struct {
39
+ Keys []JWK \`json:"keys"\`
40
+ }
41
+
42
+ var (
43
+ jwkCache = make(map[string]*rsa.PublicKey)
44
+ cacheMu sync.RWMutex
45
+ lastFetch time.Time
46
+ )
47
+
48
+ func GetPublicKeyByKid(kid string, cfg *config.Config) (interface{}, error) {
49
+ cacheMu.RLock()
50
+ pubKey, exists := jwkCache[kid]
51
+ cacheMu.RUnlock()
52
+
53
+ if exists {
54
+ return pubKey, nil
55
+ }
56
+
57
+ // Rate limit fetching to once every 10 seconds to prevent resource exhaustion
58
+ cacheMu.Lock()
59
+ defer cacheMu.Unlock()
60
+
61
+ // Double check cache
62
+ if pubKey, exists = jwkCache[kid]; exists {
63
+ return pubKey, nil
64
+ }
65
+
66
+ if time.Since(lastFetch) < 10*time.Second && len(jwkCache) > 0 {
67
+ return nil, fmt.Errorf("public key not found in cache and fetch rate limited")
68
+ }
69
+
70
+ // Fetch from Keycloak JWKS endpoint
71
+ keycloakHost := cfg.GoDuck.Security.KeycloakHost
72
+ realm := cfg.GoDuck.Security.KeycloakRealm
73
+ if keycloakHost == "" || realm == "" {
74
+ return nil, fmt.Errorf("keycloak-host or keycloak-realm not configured")
75
+ }
76
+
77
+ jwksURL := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/certs", strings.TrimSuffix(keycloakHost, "/"), realm)
78
+
79
+ client := &http.Client{Timeout: 5 * time.Second}
80
+ resp, err := client.Get(jwksURL)
81
+ if err != nil {
82
+ return nil, fmt.Errorf("failed to fetch JWKS from Keycloak: %w", err)
83
+ }
84
+ defer resp.Body.Close()
85
+
86
+ if resp.StatusCode != http.StatusOK {
87
+ return nil, fmt.Errorf("unexpected status from JWKS: %d", resp.StatusCode)
88
+ }
89
+
90
+ var jwks JWKKeySet
91
+ if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil {
92
+ return nil, fmt.Errorf("failed to decode JWKS JSON: %w", err)
93
+ }
94
+
95
+ // Update cache
96
+ newCache := make(map[string]*rsa.PublicKey)
97
+ for _, jwk := range jwks.Keys {
98
+ if jwk.Kty != "RSA" || jwk.N == "" || jwk.E == "" {
99
+ continue
100
+ }
101
+
102
+ // Decode modulus (n)
103
+ nBytes, err := base64.RawURLEncoding.DecodeString(jwk.N)
104
+ if err != nil {
105
+ continue
106
+ }
107
+
108
+ // Decode exponent (e)
109
+ eBytes, err := base64.RawURLEncoding.DecodeString(jwk.E)
110
+ if err != nil {
111
+ continue
112
+ }
113
+
114
+ var eVal int
115
+ for _, b := range eBytes {
116
+ eVal = (eVal << 8) | int(b)
117
+ }
118
+
119
+ pub := &rsa.PublicKey{
120
+ N: new(big.Int).SetBytes(nBytes),
121
+ E: eVal,
122
+ }
123
+
124
+ newCache[jwk.Kid] = pub
125
+ }
126
+
127
+ // Replace old cache
128
+ for k, v := range newCache {
129
+ jwkCache[k] = v
130
+ }
131
+ lastFetch = time.Now()
132
+
133
+ pubKey, exists = jwkCache[kid]
134
+ if !exists {
135
+ return nil, fmt.Errorf("key id %s not found in JWKS", kid)
136
+ }
137
+
138
+ return pubKey, nil
139
+ }
140
+
22
141
  // JWTMiddleware validates Keycloak JWTs
23
142
  func JWTMiddleware() gin.HandlerFunc {
24
143
  return func(c *gin.Context) {
@@ -37,10 +156,14 @@ func JWTMiddleware() gin.HandlerFunc {
37
156
  tokenString := parts[1]
38
157
 
39
158
  token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
40
- if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
159
+ if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
41
160
  return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
42
161
  }
43
- return []byte(config.GetConfig().GoDuck.Security.KeycloakAppClientSecret), nil
162
+ kid, ok := token.Header["kid"].(string)
163
+ if !ok {
164
+ return nil, fmt.Errorf("missing kid header")
165
+ }
166
+ return GetPublicKeyByKid(kid, config.GetConfig())
44
167
  })
45
168
 
46
169
  if err != nil || !token.Valid {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-duck-cli",
3
- "version": "1.1.16",
3
+ "version": "1.1.19",
4
4
  "description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
5
5
  "main": "index.js",
6
6
  "type": "module",