warden

package module
v0.0.0-...-ec00bba Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 1 Imported by: 0

README

go-ruby-warden/warden

warden — go-ruby-warden

Docs License Go Coverage

A pure-Go (no cgo) model of the engine of Ruby's Warden — the Rack authentication middleware. It mirrors Warden's observable control flow — the env['warden'] proxy, scope-aware authentication, session-cached users, pluggable strategies, and the throw :warden → failure-app dispatch — without any Ruby runtime.

Warden is not an authentication system; it is the plumbing that lets a Rack app run pluggable strategies, cache the authenticated user per scope, and hand control to a failure application when authentication is required but unmet. This package models exactly that plumbing — the deterministic, interpreter-independent parts — and leaves the Ruby-defined pieces (the strategy bodies and the session) behind seams.

It is the Warden backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-rack (whose Env and Response it reuses), go-ruby-regexp and go-ruby-erb.

Fidelity basis

Modeled on the observable behaviour of the warden gem (the wardencommunity/warden Rack middleware, as bundled with MRI-4.0.5-era Rack apps):

  • Warden::Manager — the Rack middleware. call(env) injects the proxy as env['warden'], runs the downstream app inside catch(:warden), and on a throw dispatches to a redirect / custom response or the failure_app.
  • Warden::Proxy — the env['warden'] object: authenticate / authenticate!, authenticated?, user / set_user, logout, winning_strategy, message, scope-aware over :default and custom scopes.
  • Warden::Strategies::Base — each strategy's valid? predicate and authenticate! body, producing success! / fail! / fail / redirect! / custom! / pass.
  • Warden::SessionSerializerserialize_into_session / serialize_from_session, keying the user under warden.user.<scope>.key in the Rack session.
  • throw :warden and Warden::NotAuthenticated control flow.

The strategy bodies and the (de)serialization of a user are Ruby, so they are injectable seams, not baked in — see below.

The two seams

Everything interpreter-specific is injected, so the package stays free of any Ruby runtime:

// StrategyRun runs one registered strategy by label (valid? + authenticate!)
// and returns its outcome. A binding wires this to the host's strategy registry.
type StrategyRun func(name string, env rack.Env) StrategyResult

// SessionStore is serialize_into_session / serialize_from_session. The default
// SerializerStore stores the user key under "warden.user.<scope>.key" in
// env["rack.session"]; Serialize / Deserialize map a user to/from a storable key.
type SessionStore interface {
	SerializeIntoSession(env rack.Env, scope string, user any)
	SerializeFromSession(env rack.Env, scope string) (user any, present bool)
	Delete(env rack.Env, scope string)
	Reset(env rack.Env)
}

Install

go get github.com/go-ruby-warden/warden

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-rack/rack"
	"github.com/go-ruby-warden/warden"
)

type user struct{ id int; name string }

func main() {
	// A strategy that authenticates when the env carries the right token.
	run := func(name string, env rack.Env) warden.StrategyResult {
		if name == "token" && env["HTTP_AUTHORIZATION"] == "secret" {
			return warden.StrategyResult{Valid: true, Result: warden.ResultSuccess,
				User: &user{id: 1, name: "ada"}}
		}
		return warden.StrategyResult{Valid: true, Result: warden.ResultFailure,
			Halted: true, Message: "bad token"}
	}

	// The protected app calls authenticate! (which throws :warden on failure).
	app := func(env rack.Env) *rack.Response {
		u := warden.FromEnv(env).AuthenticateBang().(*user)
		return rack.NewResponseString("hello "+u.name, 200, nil)
	}

	// The failure app runs when authentication is thrown.
	failure := func(env rack.Env) *rack.Response {
		opts := env["warden.options"].(warden.ThrowOptions)
		return rack.NewResponseString("denied: "+opts.Message, 401, nil)
	}

	m := warden.New(app,
		warden.WithStrategyRun(run),
		warden.WithDefaultStrategies("token"),
		warden.WithFailureApp(failure),
	)

	ok := m.Call(rack.Env{"HTTP_AUTHORIZATION": "secret"})
	_, _, body := ok.Finish()
	fmt.Println(body[0]) // hello ada

	no := m.Call(rack.Env{})
	_, _, body = no.Finish()
	fmt.Println(body[0]) // denied: bad token
}

API

// middleware
type App func(env rack.Env) *rack.Response
func New(app App, opts ...Option) *Manager
func (m *Manager) Call(env rack.Env) *rack.Response

// options (Warden::Config)
func WithFailureApp(app App) Option
func WithStrategyRun(run StrategyRun) Option
func WithSessionStore(store SessionStore) Option
func WithDefaultStrategies(names ...string) Option
func WithScopeStrategies(scope string, names ...string) Option
func WithDefaultScope(scope string) Option
func WithIntercept401() Option

// the env['warden'] object
func FromEnv(env rack.Env) *Proxy
func (p *Proxy) Authenticate(opts ...AuthOptions) any      // authenticate
func (p *Proxy) AuthenticateBang(opts ...AuthOptions) any  // authenticate! (throws)
func (p *Proxy) Authenticated(scope ...string) bool        // authenticated?
func (p *Proxy) Unauthenticated(scope ...string) bool      // unauthenticated?
func (p *Proxy) User(scope ...string) any                  // user
func (p *Proxy) SetUser(user any, scope string, store bool) any // set_user
func (p *Proxy) Logout(scopes ...string)                   // logout
func (p *Proxy) WinningStrategy() string                   // winning_strategy
func (p *Proxy) Message() string                           // message
func (p *Proxy) Result(scope ...string) Result

// strategies
type Result string // ResultNone/Success/Failure/Redirect/Custom
type StrategyResult struct { Valid bool; Result Result; User any; Message string; Halted bool; Response *rack.Response }
type StrategyRun func(name string, env rack.Env) StrategyResult

// session
type SessionStore interface { /* serialize_into_session / serialize_from_session */ }
type SerializerStore struct { Serialize func(any) any; Deserialize func(any) (any, bool); SessionEnvKey string }
func NewSerializerStore() *SerializerStore
func SessionKeyFor(scope string) string // "warden.user.<scope>.key"

// control flow
type Throw struct { Options ThrowOptions }           // throw :warden
type NotAuthenticated struct { Scope, Message string } // Warden::NotAuthenticated

AuthenticateBang panics with a *Throw (Ruby's throw :warden), which Manager.Call recovers and turns into a failure response — a redirect / custom response when the winning strategy produced one, otherwise the failure app. With no failure app configured it panics with a *NotAuthenticated (the gem's "No Failure App" raise). A binding maps these to Ruby's throw/catch and Warden::NotAuthenticated.

Tests & coverage

Deterministic, Ruby-free tests drive every engine path — auth success / failure / throw, multi-scope isolation, set_user / logout, the serialize round-trip, redirect! / custom! dispatch, intercept_401, and the missing-failure-app raise — through fake strategy and session seams, holding coverage at 100% (so the qemu cross-arch and Windows lanes pass the gate).

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

CGO-free, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three OSes (Linux, macOS, Windows). Its one dependency is the pure-Go go-ruby-rack, for the Env and Response value types.

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-warden/warden authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package warden is a pure-Go (no cgo) model of the engine of Ruby's Warden Rack authentication middleware, faithful to the observable behaviour of the `warden` gem (as bundled with MRI-4.0.5-era Rack apps).

Warden is not an authentication *system*; it is the plumbing that lets a Rack app run pluggable authentication strategies, cache the authenticated user per scope in the session, and hand control to a failure application when authentication is required but not satisfied. This package models that plumbing — the deterministic, interpreter-independent control flow — while leaving the Ruby-defined pieces behind seams:

  • Manager is the Rack middleware. It wraps a downstream App, injects a Proxy as env["warden"], runs the app inside a catch for the throw :warden control-flow signal, and dispatches to the failure app / redirect / custom response when authentication was thrown.
  • Proxy is the env["warden"] object: Authenticate / AuthenticateBang, Authenticated, User / SetUser, Logout, WinningStrategy, Message. It is scope-aware ("default" plus any custom scope).
  • The strategy bodies (valid? + authenticate!) are Ruby, so they live behind the injectable StrategyRun seam, which returns a StrategyResult (success / failure / redirect / custom / pass, and whether it halted the chain).
  • The session is a Rack concern, so serialize_into_session / serialize_from_session go through the SessionStore seam; a faithful default, SerializerStore, stores the user key under "warden.user.<scope>.key" in env["rack.session"].

The Rack environment and response tuple are reused from the sibling module github.com/go-ruby-rack/rack: an environment is a rack.Env (a string-keyed map) and an app returns a *rack.Response — the SPEC [status, headers, body] tuple.

throw :warden is modeled as a panic of *Throw recovered by Manager.Call; NotAuthenticated models the error a binding raises when authentication is required but no failure app is configured (the gem's "No Failure App" raise).

The package is the Warden backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-rack, go-ruby-regexp and go-ruby-erb.

Index

Constants

View Source
const DefaultScope = "default"

DefaultScope is Warden's default authentication scope (:default).

View Source
const DefaultSessionEnvKey = "rack.session"

DefaultSessionEnvKey is the Rack env key under which the session hash lives, matching Rack's "rack.session".

View Source
const EnvKey = "warden"

EnvKey is the Rack env key under which the Proxy is injected, matching the gem's env['warden'].

Variables

This section is empty.

Functions

func SessionKeyFor

func SessionKeyFor(scope string) string

SessionKeyFor returns the session key under which Warden stores the serialized user for a scope, matching the gem's "warden.user.#{scope}.key".

Types

type App

type App func(env rack.Env) *rack.Response

App is a Rack application: it maps a rack.Env to the SPEC [status, headers, body] tuple, returned as a *rack.Response. Both the downstream app wrapped by a Manager and the failure app are Apps.

type AuthOptions

type AuthOptions struct {
	// Scope is the authentication scope; empty means the manager's default scope.
	Scope string
	// Strategies overrides the strategy labels tried for this call; empty means
	// the scope's configured strategies.
	Strategies []string
}

AuthOptions tunes a single Authenticate / AuthenticateBang call, mirroring the scope: and strategy-list arguments the gem accepts.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager is the Warden Rack middleware. It wraps a downstream App, injects a Proxy as env["warden"], runs the app inside a catch for the throw :warden signal, and on a throw dispatches to the failure app / redirect / custom response.

func New

func New(app App, opts ...Option) *Manager

New builds a Manager wrapping app, applying the given options. Without a SessionStore option it defaults to a NewSerializerStore; without a WithDefaultScope the scope is DefaultScope.

func (*Manager) Call

func (m *Manager) Call(env rack.Env) *rack.Response

Call runs the middleware for a single request. It injects the proxy, runs the downstream app under a throw :warden catch, and returns the app's response — or, when authentication was thrown (or a 401 was intercepted), the failure response.

type NotAuthenticated

type NotAuthenticated struct {
	Scope   string
	Message string
}

NotAuthenticated models Warden::NotAuthenticated — the error a binding raises when authentication was required (a throw :warden reached the failure stage) but no failure app is configured. It mirrors the gem's "No Failure App" raise. Manager.Call panics with a *NotAuthenticated in that case.

func (*NotAuthenticated) Error

func (e *NotAuthenticated) Error() string

Error implements error.

type Option

type Option func(*Manager)

Option configures a Manager at construction, mirroring the settings a Warden::Config exposes (default scope, per-scope strategies, failure app).

func WithDefaultScope

func WithDefaultScope(scope string) Option

WithDefaultScope overrides the default scope (default: DefaultScope).

func WithDefaultStrategies

func WithDefaultStrategies(names ...string) Option

WithDefaultStrategies sets the strategy labels tried for scopes without an explicit list, in order.

func WithFailureApp

func WithFailureApp(app App) Option

WithFailureApp sets the failure application invoked when authentication is thrown and no strategy produced a redirect/custom response.

func WithIntercept401

func WithIntercept401() Option

WithIntercept401 makes the manager treat a downstream 401 response as an authentication failure and run the failure handling, like Warden's intercept_401.

func WithScopeStrategies

func WithScopeStrategies(scope string, names ...string) Option

WithScopeStrategies sets the strategy labels tried for a specific scope.

func WithSessionStore

func WithSessionStore(store SessionStore) Option

WithSessionStore sets the SessionStore seam (defaults to a NewSerializerStore).

func WithStrategyRun

func WithStrategyRun(run StrategyRun) Option

WithStrategyRun sets the StrategyRun seam used to run strategies.

type Proxy

type Proxy struct {
	// contains filtered or unexported fields
}

Proxy is the env["warden"] object: the per-request, scope-aware authentication handle. It caches the user per scope, runs strategies, and records the winning strategy, result and message.

func FromEnv

func FromEnv(env rack.Env) *Proxy

FromEnv returns the Proxy injected into a Rack env by a Manager, or nil when none is present. Downstream apps use it to reach env["warden"].

func (*Proxy) Authenticate

func (p *Proxy) Authenticate(opts ...AuthOptions) any

Authenticate runs the strategies for a scope and returns the authenticated user, or nil on failure. It never throws.

func (*Proxy) AuthenticateBang

func (p *Proxy) AuthenticateBang(opts ...AuthOptions) any

AuthenticateBang runs the strategies for a scope and returns the user, or panics with a *Throw (the throw :warden signal) on failure — the authenticate! method.

func (*Proxy) Authenticated

func (p *Proxy) Authenticated(scope ...string) bool

Authenticated reports whether a user is set for a scope (authenticated?), loading it from the session if needed.

func (*Proxy) Logout

func (p *Proxy) Logout(scopes ...string)

Logout logs out the given scopes, deleting each from the session. With no scopes it logs out every loaded scope and resets the session.

func (*Proxy) Message

func (p *Proxy) Message() string

Message returns the winning strategy's message, or empty when none ran.

func (*Proxy) Result

func (p *Proxy) Result(scope ...string) Result

Result returns the winning result for a scope (empty when none halted).

func (*Proxy) SetUser

func (p *Proxy) SetUser(user any, scope string, store bool) any

SetUser sets the user for a scope. When store is true it is also serialized into the session (set_user with store: true); pass false to skip persistence (store: false).

func (*Proxy) Unauthenticated

func (p *Proxy) Unauthenticated(scope ...string) bool

Unauthenticated is the negation of Proxy.Authenticated.

func (*Proxy) User

func (p *Proxy) User(scope ...string) any

User returns the user for a scope, loading it from the session on first access, or nil when none is set.

func (*Proxy) WinningStrategy

func (p *Proxy) WinningStrategy() string

WinningStrategy returns the label of the last strategy that ran, or empty when none ran.

type Result

type Result string

Result is the outcome kind a strategy produces, mirroring the result symbols of Warden::Strategies::Base (:success, :failure, :redirect, :custom, or none).

const (
	// ResultNone is produced by pass — no decision, try the next strategy.
	ResultNone Result = ""
	// ResultSuccess is produced by success!(user) — halts with a user.
	ResultSuccess Result = "success"
	// ResultFailure is produced by fail!/fail — a failed attempt.
	ResultFailure Result = "failure"
	// ResultRedirect is produced by redirect! — halts with a redirect response.
	ResultRedirect Result = "redirect"
	// ResultCustom is produced by custom!(response) — halts with a raw response.
	ResultCustom Result = "custom"
)

type SerializerStore

type SerializerStore struct {
	// Serialize maps a user to the value stored in the session.
	Serialize func(user any) any
	// Deserialize maps a stored value back to a user; present reports whether it
	// still resolves.
	Deserialize func(key any) (user any, present bool)
	// SessionEnvKey overrides [DefaultSessionEnvKey] when non-empty.
	SessionEnvKey string
}

SerializerStore is the faithful default SessionStore. It stores the serialized user key under SessionKeyFor in the Rack session hash held at SessionEnvKey in the env. Serialize maps a user to a storable key and Deserialize maps a stored key back to a user (returning present=false when the key no longer resolves) — exactly the SessionSerializer#serialize / #deserialize seam of the gem. NewSerializerStore wires identity functions, suitable when the user object is itself directly storable.

func NewSerializerStore

func NewSerializerStore() *SerializerStore

NewSerializerStore returns a SerializerStore with identity serialize / deserialize functions, storing the user object itself in the session.

func (*SerializerStore) Delete

func (s *SerializerStore) Delete(env rack.Env, scope string)

Delete implements SessionStore.

func (*SerializerStore) Reset

func (s *SerializerStore) Reset(env rack.Env)

Reset implements SessionStore.

func (*SerializerStore) SerializeFromSession

func (s *SerializerStore) SerializeFromSession(env rack.Env, scope string) (any, bool)

SerializeFromSession implements SessionStore.

func (*SerializerStore) SerializeIntoSession

func (s *SerializerStore) SerializeIntoSession(env rack.Env, scope string, user any)

SerializeIntoSession implements SessionStore.

type SessionStore

type SessionStore interface {
	// SerializeIntoSession stores the serialized key for user under scope
	// (serialize_into_session).
	SerializeIntoSession(env rack.Env, scope string, user any)
	// SerializeFromSession returns the user stored for scope and whether one is
	// present (serialize_from_session).
	SerializeFromSession(env rack.Env, scope string) (user any, present bool)
	// Delete removes the stored user for scope (logout of a single scope).
	Delete(env rack.Env, scope string)
	// Reset clears the whole session (logout of every scope / reset_session!).
	Reset(env rack.Env)
}

SessionStore is the seam through which the Proxy persists the authenticated user across requests — the serialize_into_session / serialize_from_session pair. It is a seam because the session and the (de)serialization of a user to a storable key are host concerns.

type StrategyResult

type StrategyResult struct {
	// Valid reports the strategy's valid? predicate. When false the strategy is
	// skipped and the rest of the fields are ignored.
	Valid bool
	// Result is the outcome kind. ResultNone (from pass) or a non-halting
	// ResultFailure (from fail, without a bang) let the runner continue.
	Result Result
	// User is the authenticated user, set on ResultSuccess.
	User any
	// Message is the failure / status message (fail!/fail/redirect!).
	Message string
	// Halted reports whether the strategy halted the chain. success!, fail!,
	// redirect! and custom! halt; pass and the non-bang fail do not.
	Halted bool
	// Response is the raw Rack response carried by redirect! / custom!.
	Response *rack.Response
}

StrategyResult is the outcome of running one strategy — the observable state of a Warden::Strategies::Base after its valid? check and authenticate! body.

type StrategyRun

type StrategyRun func(name string, env rack.Env) StrategyResult

StrategyRun is the injectable seam that runs a single registered strategy by label against the Rack env and returns its StrategyResult. It stands in for Warden::Strategies[label].new(env,scope) followed by valid? and authenticate!, whose bodies are Ruby. A binding wires this to the host's strategy registry.

type Throw

type Throw struct {
	Options ThrowOptions
}

Throw is the Go analogue of Ruby's throw :warden. Proxy.AuthenticateBang panics with a *Throw when authentication fails; Manager.Call recovers it.

func (*Throw) Error

func (t *Throw) Error() string

Error implements error so a *Throw can also be surfaced as one by a binding.

type ThrowOptions

type ThrowOptions struct {
	// Scope is the authentication scope that failed ("default" or a custom one).
	Scope string
	// Action names the failure action; it becomes PATH_INFO ("/unauthenticated"
	// by default) when the failure app runs.
	Action string
	// Message is the winning strategy's failure message, if any.
	Message string
	// Result is the winning strategy's result (failure / redirect / custom), or
	// empty when no strategy halted.
	Result Result
	// Strategy is the label of the winning (last-run) strategy, if any.
	Strategy string
}

ThrowOptions carries the payload of Warden's throw(:warden, opts) — the control-flow signal raised when AuthenticateBang (authenticate!) fails. A binding maps a Ruby throw :warden to a panic of *Throw; Manager.Call recovers it and turns it into a failure response.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL