From baee38ad6adc87756bd56b724a57c86754a8cc81 Mon Sep 17 00:00:00 2001 From: Alex Guerrieri Date: Thu, 26 Feb 2026 15:58:04 +0100 Subject: [PATCH 1/5] fix: update test commands to use go run for tparse --- Makefile | 4 ++-- go.work.sum | 30 ++++++++++++++++++++++++++++++ tools/go.mod | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 047d7b8..cf4efde 100644 --- a/Makefile +++ b/Makefile @@ -11,13 +11,13 @@ test-clean: go clean -testcache test: test-clean - go test -run=$(TEST) $(TEST_FLAGS) -json ./... | tparse --all --follow + go test -run=$(TEST) $(TEST_FLAGS) -json ./... | go run github.com/mfridman/tparse --all --follow test-rerun: test-clean go run github.com/goware/rerun/cmd/rerun -watch ./ -run 'make test' test-coverage: - go test -run=$(TEST) $(TEST_FLAGS) -cover -coverprofile=coverage.out -json ./... | tparse --all --follow + go test -run=$(TEST) $(TEST_FLAGS) -cover -coverprofile=coverage.out -json ./... | go run github.com/mfridman/tparse --all --follow test-coverage-inspect: test-coverage go tool cover -html=coverage.out diff --git a/go.work.sum b/go.work.sum index 01f0bef..13fdcca 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,17 +1,43 @@ cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= +github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY= +github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/webrpc/gen-typescript v0.16.1/go.mod h1:xQzYnVaSMfcygDXA5SuW8eYyCLHBHkj15wCF7gcJF5Y= github.com/webrpc/webrpc v0.22.0/go.mod h1:eeABnLz9BC4F9GGw6UKebVPkzkFYLrZRlcOvh6o8n10= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= @@ -21,9 +47,13 @@ golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= diff --git a/tools/go.mod b/tools/go.mod index 1433737..ee5ec22 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/goware/rerun v0.0.9 github.com/webrpc/webrpc v0.22.1 + github.com/mfridman/tparse v0.18.0 ) require ( From 1be484409937e26af227760504876bc7faf35191 Mon Sep 17 00:00:00 2001 From: Alex Guerrieri Date: Thu, 26 Feb 2026 15:58:14 +0100 Subject: [PATCH 2/5] feat: implement asymmetric JWT authentication and update Options struct --- middleware.go | 20 +++++++++++++------ middleware_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/middleware.go b/middleware.go index 9ef5d3c..2b0ad46 100644 --- a/middleware.go +++ b/middleware.go @@ -25,13 +25,16 @@ type Options struct { // It is used to validate the `scope` claim for admin sessions. ServiceName string - // JWTsecret is required, and it is used for the JWT verification. - // If a Project Store is also provided and the request has a project claim, - // it could be replaced by the a specific verifier. + // JWTSecret is used to create the default Auth (HS256) when Auth is not provided. JWTSecret string - // ProjectStore is a pluggable backends that verifies if the project from the claim exists. - // When provived, it checks the Project from the JWT, and can override the JWT Auth. + // Auth is the JWT verifier. If not provided, it is created from JWTSecret. + // If a ProjectStore is also provided and the request has a project claim, + // it can be overridden by a project-specific Auth. + Auth *Auth + + // ProjectStore is a pluggable backend that verifies if the project from the claim exists. + // When provided, it checks the Project from the JWT, and can override the JWT Auth. ProjectStore ProjectStore // AccessKeyFuncs are used to extract the access key from the request. @@ -62,6 +65,11 @@ func (o *Options) ApplyDefaults() { if o.ErrHandler == nil { o.ErrHandler = errHandler } + + // Create default Auth from JWTSecret if not provided + if o.Auth == nil { + o.Auth = NewAuth(o.JWTSecret) + } } func VerifyToken(cfg Options) func(next http.Handler) http.Handler { @@ -74,7 +82,7 @@ func VerifyToken(cfg Options) func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - auth := NewAuth(cfg.JWTSecret) + auth := cfg.Auth if cfg.ProjectStore != nil { projectID, err := findProjectClaim(r) diff --git a/middleware_test.go b/middleware_test.go index 626a022..cb21dc1 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -458,3 +458,53 @@ func TestProjectVerifier(t *testing.T) { assert.True(t, ok) assert.NoError(t, err) } + +func TestAsymmetricAuth(t *testing.T) { + ctx := context.Background() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + publicRaw, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + require.NoError(t, err) + + public := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: publicRaw, + }) + + opts := authcontrol.Options{ + Auth: &authcontrol.Auth{ + Algorithm: "RS256", + Public: public, + }, + } + + r := chi.NewRouter() + r.Use(authcontrol.VerifyToken(opts)) + r.Use(authcontrol.Session(opts)) + r.Handle("/*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + + // Valid token signed with the private key + _, token, err := jwtauth.New("RS256", privateKey, nil).Encode(map[string]any{ + "service": "test-service", + }) + require.NoError(t, err) + + ok, err := executeRequest(t, ctx, r, "", jwt(token)) + assert.True(t, ok) + assert.NoError(t, err) + + // Token signed with a different private key should be rejected + otherKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + _, wrongToken, err := jwtauth.New("RS256", otherKey, nil).Encode(map[string]any{ + "service": "test-service", + }) + require.NoError(t, err) + + ok, err = executeRequest(t, ctx, r, "", jwt(wrongToken)) + assert.False(t, ok) + assert.ErrorIs(t, err, proto.ErrUnauthorized) +} From 9ca8254818341e3d04fa9bf64ff75061cd8bfa8f Mon Sep 17 00:00:00 2001 From: Alex Guerrieri Date: Thu, 26 Feb 2026 17:02:36 +0100 Subject: [PATCH 3/5] refactor: update S2SToken calls to use Options struct for JWTSecret --- middleware_test.go | 28 ++++++++++++++-------------- s2s.go | 11 ++++++++--- s2s_test.go | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/middleware_test.go b/middleware_test.go index cb21dc1..82ff983 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -140,7 +140,7 @@ func TestSession(t *testing.T) { options = append(options, accessKey(tc.AccessKey)) } if claims != nil { - options = append(options, jwt(authcontrol.S2SToken(JWTSecret, claims))) + options = append(options, jwt(authcontrol.S2SToken(authcontrol.Options{JWTSecret: JWTSecret}, claims))) } session := tc.Session @@ -229,33 +229,33 @@ func TestInvalid(t *testing.T) { claims := map[string]any{"service": "client_service"} // Valid S2S Request - ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(options, claims))) assert.True(t, ok) assert.NoError(t, err) // Invalid request path with wrong not enough parts in path for valid RPC request, this will delegate to next handler and return no error - ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(options, claims))) assert.True(t, ok) assert.NoError(t, err) // Invalid request path with wrong "rpc", this will delegate to next handler and return no error - ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/pcr/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/pcr/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(options, claims))) assert.True(t, ok) assert.NoError(t, err) // Invalid Service, this will delegate to next handler and return no error - ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceNameInvalid, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceNameInvalid, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(options, claims))) assert.True(t, ok) assert.NoError(t, err) // Invalid Method, this will delegate to next handler and return no error - ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodNameInvalid), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodNameInvalid), accessKey(AccessKey), jwt(authcontrol.S2SToken(options, claims))) assert.True(t, ok) assert.NoError(t, err) // Expired JWT Token claims["exp"] = time.Now().Add(-5 * time.Minute).Unix() // Note: Session() middleware allows some skew. - expiredJWT := authcontrol.S2SToken(JWTSecret, claims) + expiredJWT := authcontrol.S2SToken(options, claims) // Expired JWT Token valid method ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(expiredJWT)) @@ -274,25 +274,25 @@ func TestInvalid(t *testing.T) { // Valid Admin Request (no scope claim) claims = map[string]any{"account": AdminAddress, "admin": true} - ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(options, claims))) assert.True(t, ok) assert.NoError(t, err) // Valid Admin Request (with matching scope claim) claims = map[string]any{"account": AdminAddress, "admin": true, "scope": ServiceName} - ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(options, claims))) assert.True(t, ok) assert.NoError(t, err) // Valid Admin Request (with multiple scope claims) claims = map[string]any{"account": AdminAddress, "admin": true, "scope": ServiceName + ",other_service"} - ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(options, claims))) assert.True(t, ok) assert.NoError(t, err) // Invalid Admin Request (with non-matching scope claim) claims = map[string]any{"account": AdminAddress, "admin": true, "scope": "other_service"} - ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err = executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(options, claims))) assert.False(t, ok) assert.ErrorIs(t, err, proto.ErrInvalidScope) } @@ -353,7 +353,7 @@ func TestCustomErrHandler(t *testing.T) { claims := map[string]any{"service": "client_service"} // Valid Request - ok, err := executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(JWTSecret, claims))) + ok, err := executeRequest(t, ctx, r, fmt.Sprintf("/rpc/%s/%s", ServiceName, MethodName), accessKey(AccessKey), jwt(authcontrol.S2SToken(authcontrol.Options{JWTSecret: JWTSecret}, claims))) assert.True(t, ok) assert.NoError(t, err) @@ -375,7 +375,7 @@ func TestOrigin(t *testing.T) { r.Use(authcontrol.Session(opts)) r.Handle("/*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - token := authcontrol.S2SToken(JWTSecret, map[string]any{ + token := authcontrol.S2SToken(authcontrol.Options{JWTSecret: JWTSecret}, map[string]any{ "user": "123", "ogn": "http://localhost", }) @@ -424,7 +424,7 @@ func TestProjectVerifier(t *testing.T) { authStore[projectID] = authcontrol.NewAuth(JWTSecret) - token := authcontrol.S2SToken(JWTSecret, map[string]any{ + token := authcontrol.S2SToken(authcontrol.Options{JWTSecret: JWTSecret}, map[string]any{ "project_id": projectID, }) diff --git a/s2s.go b/s2s.go index 127ecf1..443549a 100644 --- a/s2s.go +++ b/s2s.go @@ -36,7 +36,7 @@ func S2SClient(cfg *S2SClientConfig) *http.Client { transport.SetHeader("User-Agent", fmt.Sprintf("sequence/%s", serviceName)), transport.If(cfg.JWTSecret != "", transport.SetHeaderFunc("Authorization", func(req *http.Request) string { - return "BEARER " + S2SToken(cfg.JWTSecret, map[string]any{"service": serviceName}) + return "BEARER " + S2SToken(Options{JWTSecret: cfg.JWTSecret}, map[string]any{"service": serviceName}) }), ), transport.If(cfg.JWTToken != "", @@ -51,8 +51,13 @@ func S2SClient(cfg *S2SClientConfig) *http.Client { } // Create a short-lived service-to-service JWT token for internal communication between Sequence services. -func S2SToken(jwtSecret string, claims map[string]any) string { - jwtAuth, _ := NewAuth(jwtSecret).GetVerifier(nil) +func S2SToken(o Options, claims map[string]any) string { + auth := o.Auth + if auth == nil { + auth = NewAuth(o.JWTSecret) + } + + jwtAuth, _ := auth.GetVerifier(nil) now := time.Now().UTC() c := maps.Clone(claims) diff --git a/s2s_test.go b/s2s_test.go index 927d0e7..e6b8055 100644 --- a/s2s_test.go +++ b/s2s_test.go @@ -12,7 +12,7 @@ import ( ) func TestS2SToken(t *testing.T) { - token := authcontrol.S2SToken(JWTSecret, map[string]any{"service": "test"}) + token := authcontrol.S2SToken(authcontrol.Options{JWTSecret: JWTSecret},map[string]any{"service": "test"}) auth := jwtauth.New("HS256", []byte(JWTSecret), nil) From 0d20e6c6730a0abc50853bc307242936c79032db Mon Sep 17 00:00:00 2001 From: Alex Guerrieri Date: Thu, 26 Feb 2026 17:38:34 +0100 Subject: [PATCH 4/5] refactor: replace NewAuth with direct Auth initialization and deprecate JWTSecret usage --- common.go | 7 +------ middleware.go | 4 +++- middleware_test.go | 2 +- s2s.go | 3 ++- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/common.go b/common.go index b3843a6..22a6495 100644 --- a/common.go +++ b/common.go @@ -130,11 +130,6 @@ func (t ACL) Includes(session proto.SessionType) bool { return t&ACL(1< Date: Tue, 3 Mar 2026 17:11:01 +0100 Subject: [PATCH 5/5] refactor: replace ACL with SessionTypes and update related methods --- common.go | 26 -------------------------- common_test.go | 26 +++++++++++++------------- middleware.go | 6 +++++- middleware_test.go | 24 ++++++++++++------------ proto/proto.go | 30 ++++++++++++++++++++++++++++-- 5 files changed, 58 insertions(+), 54 deletions(-) diff --git a/common.go b/common.go index 22a6495..4c48aac 100644 --- a/common.go +++ b/common.go @@ -104,32 +104,6 @@ func (c Config[any]) Verify(webrpcServices map[string][]string) error { return errors.Join(errList...) } -// ACL is a list of session types, encoded as a bitfield. -// SessionType(n) is represented by n=-the bit. -type ACL uint64 - -// NewACL returns a new ACL with the given session types. -func NewACL(sessions ...proto.SessionType) ACL { - var acl ACL - for _, v := range sessions { - acl = acl.And(v) - } - return acl -} - -// And returns a new ACL with the given session types added. -func (a ACL) And(session ...proto.SessionType) ACL { - for _, v := range session { - a |= 1 << v - } - return a -} - -// Includes returns true if the ACL includes the given session type. -func (t ACL) Includes(session proto.SessionType) bool { - return t&ACL(1<