Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ In this mode epoxy can be used as a regular reverse proxy or static file server.
* `CF_JWKS_URL` Cloudflare JWKS Url to [validate JWT](https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/validating-json/)\
e.g. `https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/certs`
* `CF_APP_AUD` Cloudflare Application Audience (AUD) Tag.
* `CF_SERVICE_TOKEN_ALLOWLIST` Comma separated list of Cloudflare Service Tokens to allow.

#### Dev mode server
* `DEV_ADDR` address to serve at, e.g. `":8080"` or `"127.0.0.1:8080"`
Expand Down
23 changes: 22 additions & 1 deletion cmd/epoxyd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,27 @@ import (

func main() {
cfg := config.Get()

log.New().
WithField("CF_ADDR", cfg.CfAddr).
WithField("CF_JWKS_URL", cfg.CfJwkUrl).
WithField("CF_APP_AUD", cfg.CfAppAud).
WithField("CF_SERVICE_TOKEN_ALLOWLIST", cfg.CfServiceTokenAllowlist).
WithField("EXT_JWKS_URL", cfg.ExtJwkUrl).
WithField("EXT_JWT_URL", cfg.ExtJwtUrl).
WithField("EXT_JWT_SUBJECT_PATH", cfg.ExtJwtSubjectPath).
WithField("DEV_ADDR", cfg.DevAddr).
WithField("DEV_ALLOWED_USER_SUFFIX", cfg.DevAllowedUserSuffix).
WithField("DEV_SESSION_DURATION", cfg.DevSessionDuration).
WithField("DEV_DISABLE_SECURE_COOKIE", cfg.DevDisableSecureCookie).
WithField("NO_AUTH_ENABLE", cfg.NoAuthEnable).
WithField("NO_AUTH_ADDR", cfg.NoAuthAddr).
WithField("PUBLIC_DIR", cfg.PublicDir).
WithField("PUBLIC_PREFIX", cfg.PublicPrefix).
WithField("CONTENT_SECURITY_POLICY", cfg.ContentSecurityPolicy).
WithField("ROUTES", cfg.Routes).
Info("epoxy config")

var publicFs fs.FS
if cfg.PublicDir != "" {
publicFs = os.DirFS(cfg.PublicDir)
Expand All @@ -38,7 +59,7 @@ func main() {
return gzhttp.GzipHandler(h)
}
middlewares := []epoxy.Middleware{
extjwt.Middleware(cfg.ExtJwkUrl, cfg.ExtJwtUrl),
extjwt.Middleware(cfg.ExtJwkUrl, cfg.ExtJwtUrl, cfg.CfServiceTokenAllowlist),
cf.Middleware(cfg.CfAppAud, cfg.CfJwkUrl),
nocache.Middleware,
gzipMiddleware,
Expand Down
1 change: 1 addition & 0 deletions internal/cf/cf.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ type Claims struct {
Type string `json:"type"`
IdentityNonce string `json:"identity_nonce"`
Country string `json:"country"`
CommonName string `json:"common_name"`
}
81 changes: 45 additions & 36 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type config struct {
CfJwksUrl string `env:"CF_JWKS_URL"`
CfAppAud string `env:"CF_APP_AUD"`

CfServiceTokenAllowlist string `env:"CF_SERVICE_TOKEN_ALLOWLIST"`

DevAddr string `env:"DEV_ADDR" envDefault:":7070"`
DevAllowedUserSuffix string `env:"DEV_ALLOWED_USER_SUFFIX"`
DevBcryptHash string `env:"DEV_BCRYPT_HASH"`
Expand Down Expand Up @@ -66,24 +68,30 @@ func Get() Config {
log.New().Fatal("error: DEV_SESSION_DURATION is negative")
}

var serviceTokenAllowList []string
if c.CfServiceTokenAllowlist != "" {
serviceTokenAllowList = strings.Split(strings.TrimSpace(c.CfServiceTokenAllowlist), ",")
}

cfg = Config{
Routes: routes,
PublicDir: strings.TrimSpace(c.PublicDir),
PublicPrefix: strings.TrimSpace(c.PublicPrefix),
CfAddr: strings.TrimSpace(c.CfAddr),
CfJwkUrl: strings.TrimSpace(c.CfJwksUrl),
CfAppAud: strings.TrimSpace(c.CfAppAud),
ExtJwkUrl: strings.TrimSpace(c.ExtJwksUrl),
ExtJwtUrl: strings.TrimSpace(c.ExtJwtUrl),
ExtJwtSubjectPath: strings.TrimSpace(c.ExtJwtSubjectPath),
DevAddr: strings.TrimSpace(c.DevAddr),
DevBcryptHash: strings.TrimSpace(c.DevBcryptHash),
DevAllowedUserSuffix: strings.TrimSpace(c.DevAllowedUserSuffix),
NoAuthEnable: c.NoAuthEnable,
NoAuthAddr: strings.TrimSpace(c.NoAuthAddr),
DevSessionDuration: c.DevSessionDuration,
DevDisableSecureCookie: c.DevDisableSecureCookie,
ContentSecurityPolicy: c.ContentSecurityPolicy,
Routes: routes,
PublicDir: strings.TrimSpace(c.PublicDir),
PublicPrefix: strings.TrimSpace(c.PublicPrefix),
CfAddr: strings.TrimSpace(c.CfAddr),
CfJwkUrl: strings.TrimSpace(c.CfJwksUrl),
CfAppAud: strings.TrimSpace(c.CfAppAud),
CfServiceTokenAllowlist: serviceTokenAllowList,
ExtJwkUrl: strings.TrimSpace(c.ExtJwksUrl),
ExtJwtUrl: strings.TrimSpace(c.ExtJwtUrl),
ExtJwtSubjectPath: strings.TrimSpace(c.ExtJwtSubjectPath),
DevAddr: strings.TrimSpace(c.DevAddr),
DevBcryptHash: strings.TrimSpace(c.DevBcryptHash),
DevAllowedUserSuffix: strings.TrimSpace(c.DevAllowedUserSuffix),
NoAuthEnable: c.NoAuthEnable,
NoAuthAddr: strings.TrimSpace(c.NoAuthAddr),
DevSessionDuration: c.DevSessionDuration,
DevDisableSecureCookie: c.DevDisableSecureCookie,
ContentSecurityPolicy: c.ContentSecurityPolicy,
}

if strings.TrimSpace(c.JwtEc256) != "" {
Expand All @@ -106,25 +114,26 @@ func Get() Config {
}

type Config struct {
Routes []epoxy.Route
PublicDir string
PublicPrefix string
CfAddr string
CfJwkUrl string
CfAppAud string
DevAddr string
DevAllowedUserSuffix string
DevBcryptHash string
DevSessionDuration time.Duration
DevDisableSecureCookie bool
ExtJwkUrl string
ExtJwtUrl string
ExtJwtSubjectPath string
NoAuthEnable bool
NoAuthAddr string
JwtEc256 *ecdsa.PrivateKey
JwtEc256Pub *ecdsa.PublicKey
ContentSecurityPolicy string
Routes []epoxy.Route
PublicDir string
PublicPrefix string
CfAddr string
CfJwkUrl string
CfAppAud string
CfServiceTokenAllowlist []string
DevAddr string
DevAllowedUserSuffix string
DevBcryptHash string
DevSessionDuration time.Duration
DevDisableSecureCookie bool
ExtJwkUrl string
ExtJwtUrl string
ExtJwtSubjectPath string
NoAuthEnable bool
NoAuthAddr string
JwtEc256 *ecdsa.PrivateKey
JwtEc256Pub *ecdsa.PublicKey
ContentSecurityPolicy string
}

func parseRoutes(routesString string) ([]epoxy.Route, error) {
Expand Down
43 changes: 39 additions & 4 deletions internal/extjwt/extjwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/modfin/epoxy/internal/cf"
"github.com/modfin/epoxy/internal/log"
"github.com/modfin/epoxy/internal/simplecache"
"github.com/modfin/epoxy/pkg/epoxy"
"github.com/modfin/epoxy/pkg/jwk"
"io"
"net/http"
"time"
)

type contextKey struct{}

func Middleware(extJwkUrl string, extJwtUrl string) epoxy.Middleware {
func Middleware(extJwkUrl string, extJwtUrl string, cfServiceTokenAllowlist []string) epoxy.Middleware {
if extJwkUrl == "" || extJwtUrl == "" {
log.New().Fatal("extjwt: missing required parameters")
}
Expand All @@ -34,6 +35,14 @@ func Middleware(extJwkUrl string, extJwtUrl string) epoxy.Middleware {
w.WriteHeader(http.StatusInternalServerError)
return
}
// Check if this is a service token by looking for common_name in claims
serviceTokenClaims := checkCloudflareServiceToken(r.Context(), cfAuth, cfServiceTokenAllowlist)
if serviceTokenClaims != nil {
ctx := context.WithValue(r.Context(), contextKey{}, *serviceTokenClaims)
next.ServeHTTP(w, r.WithContext(ctx))
return
}

extJwt, err := getAndParseExtJwt(r.Context(), jwkCache, extJwtCache, extJwkUrl, extJwtUrl, cfAuth.Raw)
if err != nil {
log.New().WithError(fmt.Errorf("extjwt: error getting and parsing token: %w", err)).AddToContext(r.Context())
Expand All @@ -46,6 +55,32 @@ func Middleware(extJwkUrl string, extJwtUrl string) epoxy.Middleware {
}
}

func checkCloudflareServiceToken(ctx context.Context, cfAuth *jwt.Token, cfServiceTokenAllowlist []string) *jwt.MapClaims {
if claims, hasClaims := cfAuth.Claims.(jwt.MapClaims); hasClaims {
if commonName, hasCommonName := claims["common_name"].(string); hasCommonName && commonName != "" {
for _, allowedId := range cfServiceTokenAllowlist {
if commonName == allowedId {
log.New().WithField("service_token", commonName).AddToContext(ctx)
// Create synthetic ext claims for service token
serviceTokenClaims := jwt.MapClaims{
"iss": "epoxy:service-token",
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
"user": map[string]any{
"email": commonName,
"full_name": commonName,
"first_name": "Service Token",
"groups": []string{"service-token"},
},
}
return &serviceTokenClaims
}
}
}
}
return nil
}

func ExtValidationClaims(ctx context.Context) (jwt.MapClaims, error) {
if c, ok := ctx.Value(contextKey{}).(jwt.MapClaims); ok {
return c, nil
Expand Down