Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/ethereum/go-ethereum v1.17.3-0.20260421080339-499762852cf2
github.com/ethpandaops/ethwallclock v0.4.0
github.com/ethpandaops/go-eth2-client v0.1.2
github.com/ethpandaops/service-authenticatoor v0.0.0-20260509005850-92ddd15b91e6
github.com/ethpandaops/spamoor v1.1.18-0.20260428200401-f2423b8e80fb
github.com/glebarez/go-sqlite v1.22.0
github.com/golang-jwt/jwt/v5 v5.3.1
Expand Down Expand Up @@ -42,6 +43,8 @@ require (

require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/MicahParks/jwkset v0.11.0 // indirect
github.com/MicahParks/keyfunc/v3 v3.8.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/OffchainLabs/go-bitfield v0.0.0-20251031151322-f427d04d8506 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
Expand All @@ -59,7 +62,7 @@ require (
github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
github.com/ferranbt/fastssz v0.1.4 // indirect
github.com/fjl/geas v0.3.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
Expand Down
10 changes: 8 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
github.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8guNBfds=
github.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OffchainLabs/go-bitfield v0.0.0-20251031151322-f427d04d8506 h1:d/SJkN8/9Ca+1YmuDiUJxAiV4w/a9S8NcsG7GMQSrVI=
Expand Down Expand Up @@ -77,14 +81,16 @@ github.com/ethpandaops/ethwallclock v0.4.0 h1:+sgnhf4pk6hLPukP076VxkiLloE4L0Yk1y
github.com/ethpandaops/ethwallclock v0.4.0/go.mod h1:y0Cu+mhGLlem19vnAV2x0hpFS5KZ7oOi2SWYayv9l24=
github.com/ethpandaops/go-eth2-client v0.1.2 h1:nJr0YBmqHtbVeLeWEDyXwjCEO0AFt1Z0lIciMYlowfU=
github.com/ethpandaops/go-eth2-client v0.1.2/go.mod h1:U3KdR8QSq8vqs9LWSGAF4ETHJpcB62E1DFf0gVMgWv0=
github.com/ethpandaops/service-authenticatoor v0.0.0-20260509005850-92ddd15b91e6 h1:5nJ/QKEVGT3yHwmBirS1dsN/3zEQxOf4L17SNhUwwVM=
github.com/ethpandaops/service-authenticatoor v0.0.0-20260509005850-92ddd15b91e6/go.mod h1:+C6sQMBxY2E3u6BITSZakQrGRovNnB1CamuQh/RIaXI=
github.com/ethpandaops/spamoor v1.1.18-0.20260428200401-f2423b8e80fb h1:YwA+7Y6rYlVwHVWMJovW43qPW8vPbazJKqDHjUiawEM=
github.com/ethpandaops/spamoor v1.1.18-0.20260428200401-f2423b8e80fb/go.mod h1:AGTwB+kRoBFF6gMn9mKDzA6Ai0qV5qfaNY6rIIvtNOU=
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
github.com/fjl/geas v0.3.1 h1:FLiv/vqgq+Qkq8Acc3bAmNq77oi1AATz3eFJlanmIjw=
github.com/fjl/geas v0.3.1/go.mod h1:HjLwTY9i/Sn5pXobhKYWh7oG/26fjn4KqjfhwRCvdDQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
Expand Down
13 changes: 11 additions & 2 deletions pkg/assertoor/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/ethpandaops/assertoor/pkg/types"
"github.com/ethpandaops/assertoor/pkg/vars"
"github.com/ethpandaops/assertoor/pkg/web"
web_types "github.com/ethpandaops/assertoor/pkg/web/types"
"github.com/jmoiron/sqlx"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -179,7 +180,7 @@ func (c *Coordinator) Run(ctx context.Context) error {
return err
}

err = c.webserver.ConfigureRoutes(c.Config.Web.Frontend, c.Config.Web.API, c.Config.AI, c, false, c.eventBus)
err = c.webserver.ConfigureRoutes(c.Config.Web, c.Config.AI, c, false, c.eventBus)
if err != nil {
return err
}
Expand All @@ -191,7 +192,15 @@ func (c *Coordinator) Run(ctx context.Context) error {
return err
}

err = c.publicWebserver.ConfigureRoutes(c.Config.Web.Frontend, nil, nil, c, true, nil)
// Public server only serves the frontend — strip API/PublicServer
// references but keep AuthProviderURL so the SPA's runtime config
// still points at the same authenticatoor.
publicWeb := &web_types.WebConfig{
Frontend: c.Config.Web.Frontend,
AuthProviderURL: c.Config.Web.AuthProviderURL,
}

err = c.publicWebserver.ConfigureRoutes(publicWeb, nil, c, true, nil)
if err != nil {
return err
}
Expand Down
31 changes: 28 additions & 3 deletions pkg/events/sse.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/sirupsen/logrus"
)

// AuthTokenChecker is a function that validates an authorization token.
type AuthTokenChecker func(tokenStr string) *jwt.Token
// AuthTokenChecker is a function that validates an authorization token
// for a request bound to host. host is the request Host header stripped
// of any port — it's matched against the token's "scope" claim.
type AuthTokenChecker func(tokenStr, host string) *jwt.Token

// SSEHandler handles Server-Sent Events connections.
type SSEHandler struct {
Expand Down Expand Up @@ -157,11 +160,33 @@ func (h *SSEHandler) checkAuth(r *http.Request) bool {
return false
}

token := h.authChecker(authHeader)
token := h.authChecker(authHeader, stripPort(r.Host))

return token != nil && token.Valid
}

// stripPort removes a trailing ":port" from a request Host header value.
// Handles bracketed IPv6 hosts.
func stripPort(host string) string {
if host == "" {
return host
}

if host[0] == '[' {
if i := strings.IndexByte(host, ']'); i >= 0 {
return host[1:i]
}

return host
}

if i := strings.LastIndexByte(host, ':'); i >= 0 {
return host[:i]
}

return host
}

// sendEvent sends an SSE event to the client.
func (h *SSEHandler) sendEvent(w http.ResponseWriter, flusher http.Flusher, event *Event) {
data, err := json.Marshal(event)
Expand Down
11 changes: 2 additions & 9 deletions pkg/web/api/ai_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ type AIHandler struct {
database *db.Database
logger logrus.FieldLogger
authHandler *auth.Handler
disableAuth bool
sessionManager *ai.SessionManager
}

Expand All @@ -35,7 +34,6 @@ func NewAIHandler(
database *db.Database,
logger logrus.FieldLogger,
authHandler *auth.Handler,
disableAuth bool,
) *AIHandler {
var client *ai.OpenRouterClient
if config != nil && config.Enabled && config.OpenRouterKey != "" {
Expand All @@ -48,21 +46,16 @@ func NewAIHandler(
database: database,
logger: logger.WithField("module", "ai"),
authHandler: authHandler,
disableAuth: disableAuth,
sessionManager: ai.NewSessionManager(),
}
}

func (h *AIHandler) checkAuth(r *http.Request) bool {
if h.disableAuth {
if h.authHandler == nil || h.authHandler.IsOpen() {
return true
}

if h.authHandler == nil {
return true
}

token := h.authHandler.CheckAuthToken(r.Header.Get("Authorization"))
token := h.authHandler.CheckAuthToken(r.Header.Get("Authorization"), auth.StripPort(r.Host))

return token != nil && token.Valid
}
Expand Down
15 changes: 5 additions & 10 deletions pkg/web/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,24 @@ type APIHandler struct {
logger logrus.FieldLogger
coordinator types.Coordinator
authHandler *auth.Handler
disableAuth bool
}

func NewAPIHandler(logger logrus.FieldLogger, coordinator types.Coordinator, authHandler *auth.Handler, disableAuth bool) *APIHandler {
func NewAPIHandler(logger logrus.FieldLogger, coordinator types.Coordinator, authHandler *auth.Handler) *APIHandler {
return &APIHandler{
logger: logger,
coordinator: coordinator,
authHandler: authHandler,
disableAuth: disableAuth,
}
}

// checkAuth verifies the Authorization header. In open mode (no auth
// provider configured) it always returns true.
func (ah *APIHandler) checkAuth(r *http.Request) bool {
// If auth is disabled, allow all requests
if ah.disableAuth {
if ah.authHandler == nil || ah.authHandler.IsOpen() {
return true
}

if ah.authHandler == nil {
return true // No auth handler configured, allow all
}

token := ah.authHandler.CheckAuthToken(r.Header.Get("Authorization"))
token := ah.authHandler.CheckAuthToken(r.Header.Get("Authorization"), auth.StripPort(r.Host))
if token == nil || !token.Valid {
return false
}
Expand Down
80 changes: 71 additions & 9 deletions pkg/web/auth/check.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,88 @@
package auth

import (
"fmt"
"strings"

"github.com/golang-jwt/jwt/v5"

authpkg "github.com/ethpandaops/service-authenticatoor/pkg/auth"
)

func (h *Handler) CheckAuthToken(tokenStr string) *jwt.Token {
// Extract token from "Bearer <token>"
// openToken is the sentinel returned in open mode. Callers only inspect
// `token != nil && token.Valid`, so an empty *jwt.Token with Valid set is
// enough for them to treat the request as authenticated.
var openToken = &jwt.Token{Valid: true}

// CheckAuthToken validates a bearer token (with or without the "Bearer "
// prefix) for a request bound to host. In open mode it always returns a
// valid token; in remote mode it delegates to the JWKS verifier and
// returns nil on any failure (signature, exp, iss, aud, scope/host,
// services). host should be the request's Host header stripped of any
// port — the verifier matches it against the token's "scope" claim.
func (h *Handler) CheckAuthToken(tokenStr, host string) *jwt.Token {
if h.verifier == nil {
return openToken
}

parts := strings.SplitN(tokenStr, " ", 2)
if len(parts) == 2 && strings.EqualFold(parts[0], "bearer") {
tokenStr = parts[1]
}

token, _ := jwt.ParseWithClaims(tokenStr, &jwt.RegisteredClaims{}, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
if tokenStr == "" {
return nil
}

claims, err := h.verifier.Verify(tokenStr, authpkg.WithRequestHost(host))
if err != nil {
return nil
}

return &jwt.Token{Valid: true, Claims: claims}
}

// GetTokenSubject extracts the user identity (email) from a verified
// token. Returns "" when the token is missing/invalid or the handler is
// in open mode (no upstream identity to extract).
func (h *Handler) GetTokenSubject(authHeader, host string) string {
if h.verifier == nil || authHeader == "" {
return ""
}

token := h.CheckAuthToken(authHeader, host)
if token == nil || !token.Valid {
return ""
}

if c, ok := token.Claims.(*authpkg.Claims); ok {
if c.Email != "" {
return c.Email
}

return c.Subject
}

return ""
}

// StripPort removes a trailing ":port" from a request Host header value
// so it can be passed to CheckAuthToken. Handles bracketed IPv6 hosts.
func StripPort(host string) string {
if host == "" {
return host
}

if host[0] == '[' {
if i := strings.IndexByte(host, ']'); i >= 0 {
return host[1:i]
}

return []byte(h.tokenKey), nil
})
return host
}

if i := strings.LastIndexByte(host, ':'); i >= 0 {
return host[:i]
}

return token
return host
}
Loading