Skip to content

Commit 5700ca0

Browse files
authored
Merge pull request #60 from AgentWorkforce/migration/relayfile-phase2-remove-hs256
migration(auth): remove HS256 path + JWTSecret config (phase 2)
2 parents fe2a39e + b6ef5f8 commit 5700ca0

7 files changed

Lines changed: 130 additions & 162 deletions

File tree

cmd/relayfile/main.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ func main() {
4545
ExternalWritebackMode: boolEnv("RELAYFILE_EXTERNAL_WRITEBACK", true),
4646
})
4747
server, err := httpapi.NewServerWithConfig(store, httpapi.ServerConfig{
48-
JWTSecret: os.Getenv("RELAYFILE_JWT_SECRET"),
4948
JWKSURL: strings.TrimSpace(os.Getenv("RELAYAUTH_JWKS_URL")),
50-
AcceptHS256: !strings.EqualFold(strings.TrimSpace(os.Getenv("RELAYFILE_VERIFIER_ACCEPT_HS256")), "false"),
5149
InternalHMACSecret: os.Getenv("RELAYFILE_INTERNAL_HMAC_SECRET"),
5250
InternalMaxSkew: durationEnv("RELAYFILE_INTERNAL_MAX_SKEW", 5*time.Minute),
5351
RateLimitMax: intEnv("RELAYFILE_RATE_LIMIT_MAX", 0),

internal/httpapi/auth.go

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ import (
2020
)
2121

2222
const (
23-
defaultRelayAuthJWKSURL = "https://api.relayauth.dev/.well-known/jwks.json"
2423
defaultJWKSFetchTimeout = 5 * time.Second
2524
jwksCacheTTL = 5 * time.Minute
2625
jwksStaleGraceWindow = time.Minute
2726
)
2827

28+
var defaultRelayAuthJWKSURL = "https://api.relayauth.dev/.well-known/jwks.json"
29+
2930
type authError struct {
3031
status int
3132
code string
@@ -44,8 +45,6 @@ type tokenClaims struct {
4445
}
4546

4647
type bearerVerifier struct {
47-
jwtSecret string
48-
acceptHS256 bool
4948
jwksURL string
5049
jwksFetchTimeout time.Duration
5150
jwksCache *jwksCache
@@ -76,8 +75,6 @@ type jwkKey struct {
7675

7776
func newBearerVerifier(cfg ServerConfig) *bearerVerifier {
7877
return &bearerVerifier{
79-
jwtSecret: cfg.JWTSecret,
80-
acceptHS256: cfg.AcceptHS256,
8178
jwksURL: cfg.JWKSURL,
8279
jwksFetchTimeout: cfg.JWKSFetchTimeout,
8380
jwksCache: &jwksCache{
@@ -161,16 +158,6 @@ func parseBearer(authHeader string, verifier *bearerVerifier, now time.Time) (to
161158

162159
signingInput := parts[0] + "." + parts[1]
163160
switch header.Alg {
164-
case "HS256":
165-
if !verifier.acceptHS256 {
166-
return tokenClaims{}, &authError{status: 401, code: "unauthorized", message: "hs256 disabled"}
167-
}
168-
mac := hmac.New(sha256.New, []byte(verifier.jwtSecret))
169-
_, _ = mac.Write([]byte(signingInput))
170-
expected := mac.Sum(nil)
171-
if !hmac.Equal(sigBytes, expected) {
172-
return tokenClaims{}, &authError{status: 401, code: "unauthorized", message: "jwt signature mismatch"}
173-
}
174161
case "RS256":
175162
publicKey, authErr := verifier.lookupRSAPublicKey(header.Kid, now)
176163
if authErr != nil {

internal/httpapi/auth_test.go

Lines changed: 11 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package httpapi
22

33
import (
44
"crypto"
5-
"crypto/hmac"
65
"crypto/rand"
76
"crypto/rsa"
87
"crypto/sha256"
@@ -285,60 +284,21 @@ func TestParseBearerRS256ExpiredToken(t *testing.T) {
285284
}
286285
}
287286

288-
func TestParseBearerHS256AcceptedWhenEnabled(t *testing.T) {
289-
t.Parallel()
290-
291-
now := time.Now().UTC()
292-
token := mustTestHS256JWT(t, "test-secret", map[string]any{
293-
"workspace_id": "ws-hs",
294-
"agent_name": "LegacyWorker",
295-
"scopes": []string{"fs:read"},
296-
"exp": now.Add(time.Hour).Unix(),
297-
"aud": "relayfile",
298-
})
299-
300-
claims, authErr := parseBearer("Bearer "+token, newBearerVerifier(ServerConfig{
301-
JWTSecret: "test-secret",
302-
AcceptHS256: true,
303-
}), now)
304-
if authErr != nil {
305-
t.Fatalf("parseBearer returned auth error: %+v", authErr)
306-
}
307-
if claims.WorkspaceID != "ws-hs" || claims.AgentName != "LegacyWorker" {
308-
t.Fatalf("unexpected claims: %+v", claims)
309-
}
310-
}
311-
312-
func TestParseBearerHS256RejectedWhenDisabled(t *testing.T) {
313-
t.Parallel()
314-
315-
now := time.Now().UTC()
316-
token := mustTestHS256JWT(t, "test-secret", map[string]any{
317-
"workspace_id": "ws-hs",
318-
"agent_name": "LegacyWorker",
319-
"scopes": []string{"fs:read"},
320-
"exp": now.Add(time.Hour).Unix(),
321-
"aud": "relayfile",
322-
})
323-
324-
_, authErr := parseBearer("Bearer "+token, newBearerVerifier(ServerConfig{
325-
JWTSecret: "test-secret",
326-
}), now)
327-
if authErr == nil {
328-
t.Fatal("expected auth error when HS256 is disabled")
329-
}
330-
if authErr.message != "hs256 disabled" {
331-
t.Fatalf("expected hs256 disabled message, got %q", authErr.message)
332-
}
333-
}
334-
335287
func TestParseBearerClaimNormalization(t *testing.T) {
336288
t.Parallel()
337289

338290
now := time.Now().UTC()
291+
privateKey := mustRSATestKey(t)
292+
jwksServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
293+
_ = json.NewEncoder(w).Encode(jwksDocument{
294+
Keys: []jwkKey{mustRSATestJWK("kid-1", &privateKey.PublicKey)},
295+
})
296+
}))
297+
defer jwksServer.Close()
298+
339299
verifier := newBearerVerifier(ServerConfig{
340-
JWTSecret: "test-secret",
341-
AcceptHS256: true,
300+
JWKSURL: jwksServer.URL,
301+
JWKSFetchTimeout: time.Second,
342302
})
343303

344304
tests := []struct {
@@ -390,9 +350,7 @@ func TestParseBearerClaimNormalization(t *testing.T) {
390350
for _, tt := range tests {
391351
tt := tt
392352
t.Run(tt.name, func(t *testing.T) {
393-
t.Parallel()
394-
395-
token := mustTestHS256JWT(t, "test-secret", tt.payload)
353+
token := mustTestRS256JWT(t, privateKey, "kid-1", tt.payload)
396354
claims, authErr := parseBearer("Bearer "+token, verifier, now)
397355
if authErr != nil {
398356
t.Fatalf("parseBearer returned auth error: %+v", authErr)
@@ -515,19 +473,6 @@ func mustRSATestKey(t *testing.T) *rsa.PrivateKey {
515473
return privateKey
516474
}
517475

518-
func mustTestHS256JWT(t *testing.T, secret string, payload map[string]any) string {
519-
t.Helper()
520-
521-
return mustTestJWTWithClaims(t, map[string]any{
522-
"alg": "HS256",
523-
"typ": "JWT",
524-
}, payload, func(signingInput string) []byte {
525-
mac := hmac.New(sha256.New, []byte(secret))
526-
_, _ = mac.Write([]byte(signingInput))
527-
return mac.Sum(nil)
528-
})
529-
}
530-
531476
func mustTestRS256JWT(t *testing.T, privateKey *rsa.PrivateKey, kid string, payload map[string]any) string {
532477
t.Helper()
533478

internal/httpapi/server.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ import (
2222
)
2323

2424
type ServerConfig struct {
25-
JWTSecret string
2625
JWKSURL string
27-
AcceptHS256 bool
2826
JWKSFetchTimeout time.Duration
2927
InternalHMACSecret string
3028
InternalMaxSkew time.Duration
@@ -55,30 +53,20 @@ type rateEntry struct {
5553
}
5654

5755
func NewServer(store *relayfile.Store) *Server {
58-
server, err := NewServerWithConfig(store, ServerConfig{
59-
JWTSecret: "dev-secret",
60-
AcceptHS256: true,
61-
})
56+
server, err := NewServerWithConfig(store, ServerConfig{})
6257
if err != nil {
6358
panic(err)
6459
}
65-
log.Println("WARNING: using default JWTSecret — set a strong secret via configuration for production use")
6660
return server
6761
}
6862

6963
func NewServerWithConfig(store *relayfile.Store, cfg ServerConfig) (*Server, error) {
70-
if !cfg.AcceptHS256 && cfg.JWTSecret == "" && cfg.JWKSURL == "" && cfg.JWKSFetchTimeout == 0 {
71-
cfg.AcceptHS256 = true
72-
}
7364
if cfg.JWKSURL == "" {
7465
cfg.JWKSURL = defaultRelayAuthJWKSURL
7566
}
7667
if cfg.JWKSFetchTimeout <= 0 {
7768
cfg.JWKSFetchTimeout = defaultJWKSFetchTimeout
7869
}
79-
if cfg.AcceptHS256 && cfg.JWTSecret == "" {
80-
return nil, fmt.Errorf("RELAYFILE_JWT_SECRET is required when RELAYFILE_VERIFIER_ACCEPT_HS256 is not false")
81-
}
8270
if cfg.InternalHMACSecret == "" {
8371
cfg.InternalHMACSecret = "dev-internal-secret"
8472
log.Println("WARNING: using default InternalHMACSecret — set a strong secret via configuration for production use")

internal/httpapi/server_test.go

Lines changed: 6 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -656,8 +656,6 @@ func TestForkTreeMergesParentOverlayWritesAndDeletes(t *testing.T) {
656656

657657
func TestWriteFilePayloadTooLarge(t *testing.T) {
658658
server := mustNewServerWithConfig(t, relayfile.NewStore(), ServerConfig{
659-
JWTSecret: "dev-secret",
660-
AcceptHS256: true,
661659
MaxBodyBytes: 128,
662660
})
663661
token := mustTestJWT(t, "dev-secret", "ws_payload_limit", "Worker1", []string{"fs:write"}, time.Now().Add(time.Hour))
@@ -687,10 +685,7 @@ func TestWriteFilePayloadTooLarge(t *testing.T) {
687685
}
688686

689687
func TestBinaryEncodingRoundTripAndExport(t *testing.T) {
690-
server := mustNewServerWithConfig(t, relayfile.NewStoreWithOptions(relayfile.StoreOptions{DisableWorkers: true}), ServerConfig{
691-
JWTSecret: "dev-secret",
692-
AcceptHS256: true,
693-
})
688+
server := mustNewServerWithConfig(t, relayfile.NewStoreWithOptions(relayfile.StoreOptions{DisableWorkers: true}), ServerConfig{})
694689
token := mustTestJWT(t, "dev-secret", "ws_binary", "Worker1", []string{"fs:read", "fs:write"}, time.Now().Add(time.Hour))
695690
encoded := base64.StdEncoding.EncodeToString([]byte{0x00, 0x7f, 0xff, 0x10})
696691

@@ -2326,8 +2321,6 @@ func TestInternalWebhookIngressHMAC(t *testing.T) {
23262321

23272322
func TestInternalWebhookIngressPayloadTooLarge(t *testing.T) {
23282323
server := mustNewServerWithConfig(t, relayfile.NewStore(), ServerConfig{
2329-
JWTSecret: "dev-secret",
2330-
AcceptHS256: true,
23312324
InternalHMACSecret: "dev-internal-secret",
23322325
MaxBodyBytes: 256,
23332326
})
@@ -4605,8 +4598,6 @@ func TestInternalIngressAppliesToFilesystemAPI(t *testing.T) {
46054598

46064599
func TestRateLimitingByWorkspaceAndAgent(t *testing.T) {
46074600
server := mustNewServerWithConfig(t, relayfile.NewStore(), ServerConfig{
4608-
JWTSecret: "dev-secret",
4609-
AcceptHS256: true,
46104601
InternalHMACSecret: "dev-internal-secret",
46114602
RateLimitMax: 2,
46124603
RateLimitWindow: time.Minute,
@@ -5482,33 +5473,17 @@ func mustTestJWTWithAudience(t *testing.T, secret, workspaceID, agentName string
54825473

54835474
func mustTestJWTWithAudienceClaim(t *testing.T, secret, workspaceID, agentName string, scopes []string, aud any, exp time.Time) string {
54845475
t.Helper()
5485-
headerBytes, err := json.Marshal(map[string]any{
5486-
"alg": "HS256",
5487-
"typ": "JWT",
5488-
})
5489-
if err != nil {
5490-
t.Fatalf("marshal jwt header: %v", err)
5491-
}
5492-
payloadBytes, err := json.Marshal(map[string]any{
5476+
_ = secret
5477+
5478+
return mustTestRS256JWT(t, testBearerPrivateKey, testBearerJWTKID, map[string]any{
5479+
"wks": workspaceID,
54935480
"workspace_id": workspaceID,
5481+
"sub": agentName,
54945482
"agent_name": agentName,
54955483
"scopes": scopes,
54965484
"exp": exp.Unix(),
54975485
"aud": aud,
54985486
})
5499-
if err != nil {
5500-
t.Fatalf("marshal jwt payload: %v", err)
5501-
}
5502-
h := base64.RawURLEncoding.EncodeToString(headerBytes)
5503-
p := base64.RawURLEncoding.EncodeToString(payloadBytes)
5504-
signingInput := h + "." + p
5505-
sig := mustHMAC(secret, signingInput)
5506-
sigBytes, err := hexToBytes(sig)
5507-
if err != nil {
5508-
t.Fatalf("decode signature: %v", err)
5509-
}
5510-
jwtSig := base64.RawURLEncoding.EncodeToString(sigBytes)
5511-
return signingInput + "." + jwtSig
55125487
}
55135488

55145489
func mustHMAC(secret, data string) string {
@@ -5517,32 +5492,6 @@ func mustHMAC(secret, data string) string {
55175492
return fmt.Sprintf("%x", mac.Sum(nil))
55185493
}
55195494

5520-
func hexToBytes(h string) ([]byte, error) {
5521-
if len(h)%2 != 0 {
5522-
return nil, fmt.Errorf("invalid hex")
5523-
}
5524-
out := make([]byte, len(h)/2)
5525-
for i := 0; i < len(h); i += 2 {
5526-
var b byte
5527-
for j := 0; j < 2; j++ {
5528-
ch := h[i+j]
5529-
b <<= 4
5530-
switch {
5531-
case ch >= '0' && ch <= '9':
5532-
b |= ch - '0'
5533-
case ch >= 'a' && ch <= 'f':
5534-
b |= ch - 'a' + 10
5535-
case ch >= 'A' && ch <= 'F':
5536-
b |= ch - 'A' + 10
5537-
default:
5538-
return nil, fmt.Errorf("invalid hex char")
5539-
}
5540-
}
5541-
out[i/2] = b
5542-
}
5543-
return out, nil
5544-
}
5545-
55465495
type serverFailingAdapter struct {
55475496
provider string
55485497
}

internal/httpapi/test_jwks_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package httpapi
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/rsa"
6+
"encoding/json"
7+
"net/http"
8+
"net/http/httptest"
9+
"os"
10+
"testing"
11+
)
12+
13+
const testBearerJWTKID = "test-bearer-kid"
14+
15+
var testBearerPrivateKey *rsa.PrivateKey
16+
17+
func TestMain(m *testing.M) {
18+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
19+
if err != nil {
20+
panic(err)
21+
}
22+
testBearerPrivateKey = privateKey
23+
24+
jwksServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25+
_ = json.NewEncoder(w).Encode(jwksDocument{
26+
Keys: []jwkKey{mustRSATestJWK(testBearerJWTKID, &privateKey.PublicKey)},
27+
})
28+
}))
29+
30+
prevDefaultJWKSURL := defaultRelayAuthJWKSURL
31+
defaultRelayAuthJWKSURL = jwksServer.URL
32+
33+
code := m.Run()
34+
35+
defaultRelayAuthJWKSURL = prevDefaultJWKSURL
36+
jwksServer.Close()
37+
os.Exit(code)
38+
}

0 commit comments

Comments
 (0)