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.
- package/generators/devops.js +6 -0
- package/generators/kratos.js +8 -1
- package/generators/security.js +125 -2
- package/package.json +1 -1
package/generators/devops.js
CHANGED
|
@@ -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:
|
package/generators/kratos.js
CHANGED
|
@@ -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
|
-
|
|
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
|
),
|
package/generators/security.js
CHANGED
|
@@ -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.
|
|
159
|
+
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
|
41
160
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
42
161
|
}
|
|
43
|
-
|
|
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 {
|