Skip to content

Configuration

wiki edited this page Sep 4, 2026 · 1 revision

Configuration

app := rex.New(security.WithSecurity(security.NewConfig(
	security.WithScheme(bearerScheme),
	security.WithScheme(sessionScheme),
	security.WithCSRFPolicy(origins),
)))

security.WithSecurity(nil) takes the defaults — which register no schemes, so every route declaring one fails startup validation.

⚠ A non-nil config is used verbatim

There is no field-by-field merge. A partial struct literal leaves everything else at its zero value:

// Wrong: Schemes nil, CSRF nil.
security.WithSecurity(&security.Config{DisableCSRF: true})

// Right.
security.WithSecurity(security.NewConfig(
	security.WithScheme(s),
	security.WithoutCSRF(),
))

The merge used to exist and behaved as a trap: fields it forgot were silently ignored, fields it copied unconditionally were zeroed by a partial literal, and a deliberate zero could not be expressed at all.

Fields

type Config struct {
	Schemes     []SecurityScheme
	DisableCSRF bool         // phrased as a negative on purpose
	CSRF        *CSRFConfig  // nil takes the defaults
}

DisableCSRF is a negative so that turning protection off is something you have to write. See CSRFWhy default-on.

Options

WithScheme(s) register a scheme
WithCSRFPolicy(p) the origin allowlist CSRF checks against
WithCSRF(cfg) replace the whole CSRF configuration
WithCSRFInsecureTransport() token cookie over plain HTTP — local dev only
WithoutCSRF() disable protection application-wide

Share the origin policy with CORS

An application trusts one set of origins. Writing the list twice is how the two drift apart.

origins := rextension.OriginPolicy{
	AllowedOrigins:   []string{"https://app.example.com"},
	AllowCredentials: true,
}

app := rex.New(
	cors.WithCORS(cors.NewConfig(cors.WithPolicy(origins))),
	security.WithSecurity(security.NewConfig(
		security.WithScheme(scheme),
		security.WithCSRFPolicy(origins),
	)),
)

OriginPolicy is declared in rextension, so neither module imports the other.

A production shape

func securityConfig(origins rextension.OriginPolicy, store security.SessionStore) *security.Config {
	sessions := security.NewSessionStoreValidator(store,
		security.WithIdleTimeout(30*time.Minute),
		security.WithAbsoluteTimeout(12*time.Hour),
	)

	return security.NewConfig(
		// Machine-to-machine.
		security.WithScheme(
			security.NewBearerScheme("serviceToken", tokenValidator).
				SetBearerFormat("JWT").
				SetRolesClaim("realm_access.roles"),
		),
		// Browser session.
		security.WithScheme(
			security.NewSessionCookieScheme("session", "session_id", sessions).
				WithCookieOptions(security.CookieOptions{
					MaxAge:   int((12 * time.Hour).Seconds()),
					SameSite: http.SameSiteLaxMode,
					HttpOnly: true,
					// AllowInsecureTransport false → Secure
				}),
		),
		security.WithCSRFPolicy(origins),
	)
}

Local development adds exactly two things, and nothing else changes:

security.WithCSRFInsecureTransport()
// and CookieOptions{AllowInsecureTransport: true}

Reaching the registry

var reg *security.SchemeRegistry
_ = app.Container().Resolve(&reg)

reg.All()            // []SecurityScheme, in registration order
reg.Get("jwt")       // one scheme, or nil
reg.Names()          // registration order

It also satisfies rextension.SchemeRegistry, which is how the OpenAPI extension reads it without importing this module.

Middleware configuration

type MiddlewareConfig struct {
	SchemeRegistry *SchemeRegistry
	Logger         rx.Logger
}

Set the logger. The problem document a client receives is deliberately generic — a validator's error text routinely names another account, an internal host, or the shape of the authorization model. Without somewhere to write the specifics they are simply lost, which makes the redaction rule unmaintainable in practice.

The extension configures this for you from r.Logger().

Clone this wiki locally