go-duck-cli 1.1.16 → 1.1.18

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.
@@ -161,6 +161,12 @@ services:
161
161
  environment:
162
162
  KEYCLOAK_ADMIN: admin
163
163
  KEYCLOAK_ADMIN_PASSWORD: admin
164
+ KC_DB: postgres
165
+ KC_DB_URL: jdbc:postgresql://postgres:5432/${config.datasource?.database || 'go_duck_master'}
166
+ KC_DB_USERNAME: ${config.datasource?.username || 'postgres'}
167
+ KC_DB_PASSWORD: ${config.datasource?.password || 'password'}
168
+ depends_on:
169
+ - postgres
164
170
  volumes:
165
171
  - ./keycloak/realm-config:/opt/keycloak/data/import
166
172
  ports:
@@ -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.18",
4
4
  "description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
5
5
  "main": "index.js",
6
6
  "type": "module",