Help us shape the future of Go! Take the 2026 Go Developer Survey

authsetup

package
v0.45.0 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package authsetup builds the instance's sign-in providers and keeps the live set of them — the ONE place a provider is constructed, whether the environment/config file or the Admin → Identity providers page asked for it.

Why this package exists

Until v0.43.0 the admin page wrote `auth.<name>.*` settings rows that nothing read: the server built its drivers from FILEX_AUTH_DRIVERS and the config file only, once, at boot (release-candidate sweep, 2026-09-21). The page looked like it changed who could sign in and changed nothing — the worst kind of screen. The owner's decision: the page really manages sign-in, applied without a restart, and it can never lock the instance out.

The rules, in one place

  • One construction path: Build. server.go calls it for every provider the environment lists; Live calls it for every provider the page enabled.
  • The environment wins, visibly. A provider the environment lists is built from the environment only; rows the page may hold under the same name are kept but not used, and the page shows the provider read-only with where it comes from (Live.EnvDefines, FromWords).
  • Page providers are ADDED after the environment's, never in front of them: `local` stays the first password judge wherever the operator put it, so a directory that hangs never sits in front of the administrator's password.
  • Password sign-in (`local`) and the recovery sign-in (the bootstrap administrator's break-glass, FILEX_AUTH_RECOVERY_LOGIN) are the environment's to decide and nobody else's. The page cannot add or remove either — see docs/SSO.md, "Why nothing on this page can lock you out".
  • A page provider that cannot start is left out and says why; it never takes anything else down with it (assemble).

Index

Constants

View Source
const (
	OriginEnvironment = "environment"
	OriginPage        = "page"
)

Where a provider's configuration comes from.

View Source
const (

	// SchemaSetting records that the one-time upgrade below has run.
	SchemaSetting = "auth.providers.schema"
)

Settings keys. `auth.<name>.<field>` is the shape the page always wrote; two control keys sit beside the fields.

Variables

View Source
var ErrOIDCOff = errors.New("OIDC is not configured")

ErrOIDCOff answers an OIDC flow when no OIDC provider is running.

View Source
var ErrPasswordSignInOff = errors.New("password sign-in is off")

ErrPasswordSignInOff answers a password sign-in when no driver takes one.

View Source
var ErrSecretKeyRequired = errors.New("a client secret or bind password is stored sealed, and that needs FILEX_SECRET_KEY")

ErrSecretKeyRequired: a secret was given and there is no FILEX_SECRET_KEY to seal it with. A secret is never stored in the clear.

View Source
var Managed = []string{"oidc", "ldap", "proxy-header"}

Managed lists the providers the admin page can configure, in the order they are added to the chain. `local` and `api-token` are not among them: password sign-in is the environment's to decide (see the package comment) and API tokens are always on.

View Source
var Schema = map[string][]Field{
	"oidc": {
		{Key: "issuer", Kind: FieldText, Required: true},
		{Key: "client_id", Kind: FieldText, Required: true},
		{Key: "client_secret", Kind: FieldSecret},
		{Key: "redirect_url", Kind: FieldText},
		{Key: "scopes", Kind: FieldText},
		{Key: "role_claim", Kind: FieldText},
		{Key: "admin_group", Kind: FieldText},
	},
	"ldap": {
		{Key: "url", Kind: FieldText, Required: true},
		{Key: "base_dn", Kind: FieldText, Required: true},
		{Key: "bind_dn", Kind: FieldText},
		{Key: "bind_password", Kind: FieldSecret},
		{Key: "user_filter", Kind: FieldText},
		{Key: "email_attr", Kind: FieldText},
		{Key: "start_tls", Kind: FieldBool},
		{Key: "ca_file", Kind: FieldText},
		{Key: "protocol_login", Kind: FieldBool, Default: "true"},
	},
	"proxy-header": {
		{Key: "trusted_proxies", Kind: FieldText, Required: true},
		{Key: "header_user", Kind: FieldText},
		{Key: "header_email", Kind: FieldText},
		{Key: "header_name", Kind: FieldText},
		{Key: "header_roles", Kind: FieldText},
		{Key: "admin_role", Kind: FieldText},
		{Key: "auto_provision", Kind: FieldBool, Default: "true"},
	},
}

Schema is every field the page may store per provider. A key outside it is dropped on save: the settings table is not a scratchpad, and an unknown key is either a typo or a client that means something this server does not do.

Functions

func Build

func Build(ctx context.Context, store db.Store, name string, cfg map[string]any) (auth.Driver, error)

Build constructs and initialises ONE sign-in provider from a configuration map (the keys the driver's Init reads).

⚠⚠ The single construction path. The environment/config file (server.go) and the admin page (Live.Reload) both come here, so a provider configured on the page is exactly the driver the same values in the environment would have produced — no second, subtly different path. A caller that needs a retry (the environment's OIDC, whose IdP often boots alongside filex) wraps this call; it does not grow its own construction.

func Canonical

func Canonical(name string) string

Canonical returns the name a provider is known by. The environment has always accepted a few spellings of the header driver; the page and the wire use one.

func DraftConfig

func DraftConfig(s *Stored, draft map[string]any, box *secretbox.Box, opts Options) (map[string]any, error)

DraftConfig is the configuration "Test now" checks: what is stored with the form's unsaved values laid over it. A secret the form left blank, or sent back as "***", is the stored one — opened for the test, never returned.

func DriverConfig

func DriverConfig(s *Stored, box *secretbox.Box, opts Options) (map[string]any, bool, error)

DriverConfig turns stored rows into the map the driver's Init reads — secrets opened, booleans as booleans, defaults filled — plus whether an LDAP provider also judges passwords on the file protocols.

⚠ The opened map lives in memory only, for the length of a build or a test. It is never logged and never sent anywhere but the driver.

func FromWords

func FromWords(from, lang string) string

FromWords says where an environment provider is defined, in the reader's language: config.Auth.DriversFrom is "FILEX_AUTH_DRIVERS", "file:<path>" or "default"; anything else is shown as it is.

The words are the server catalogue's (`server.auth_provider.from_*`), so a language pack's language reads them too — not the English half of a hard-coded en/tr pair.

func IsManaged

func IsManaged(name string) bool

IsManaged reports whether the page may configure a provider.

func LoadStored

func LoadStored(ctx context.Context, store db.Store) (map[string]*Stored, error)

LoadStored reads every managed provider's rows.

func Save

func Save(ctx context.Context, store db.Store, next *Stored) error

Save writes a provider's next state. A save is a review: it clears the "saved before v0.43.0, never applied" mark.

func UpgradeLegacy

func UpgradeLegacy(ctx context.Context, store db.Store, box *secretbox.Box, log *slog.Logger) error

UpgradeLegacy runs once per database: rows the page saved before v0.43.0 were never applied by any server, so turning them on now would switch on, on upgrade, a sign-in configuration an operator may have typed long ago and forgotten. Each such provider is imported SWITCHED OFF and marked "saved before this version, never applied" for the page to show; the operator reviews and enables it.

A secret among those rows is sealed with FILEX_SECRET_KEY. With no key it cannot be sealed, and it is not kept in the clear either: it is cleared (it was never used), the page shows it as not set, and the log says which provider and which field — never the value.

func WithRecoveryLogin

func WithRecoveryLogin(loginDrvs []auth.LoginDriver, enabled bool, store db.Store) ([]auth.LoginDriver, bool)

WithRecoveryLogin appends the recovery sign-in driver when no enabled login driver is `local` and recovery is on, and reports whether it did.

The owner's ruling, 2026-09-14: on an installation that signs in through an identity provider alone, the administrator created at installation must still be able to sign in with a password — for recovery only. Without it an unreachable IdP, an expired client secret or a broken realm locks out the one person who can repair filex's side of it. local.RecoveryLogin accepts that account and no other, so password sign-in stays off for everyone else.

⚠ It is switched off by the environment only (FILEX_AUTH_RECOVERY_LOGIN): the admin page, which can break the identity provider it would recover from, can never switch off the way back.

It goes LAST: a directory driver in the list judges its own accounts first.

func WithSessionAuthenticator

func WithSessionAuthenticator(enabled []auth.Driver, store db.Store) []auth.Driver

WithSessionAuthenticator makes sure something in the chain can turn a filex session back into a user, whichever login drivers are enabled.

⚠⚠ Issue #24. Every sign-in — a password, an LDAP bind, an OIDC callback — ends in the same sessions row and the same `filex_session` cookie, but only the `local` driver's Authenticate reads that row: OIDC's and LDAP's answer "unauthorized" by design. With `FILEX_AUTH_DRIVERS=oidc` the callback minted a session the very next request refused, so nobody could sign in and every open session died on the restart that applied the setting.

The validator is not a LoginDriver, so it enables no password sign-in; and when `local` is already in the list it adds nothing, because that driver does the same lookup.

Types

type Change

type Change struct {
	Enabled *bool
	Values  map[string]any
}

Change is what a save asks for. Values holds the fields sent; a secret sent as "" or "***" keeps the stored one (the page never has it to send back).

type Directory

type Directory interface {
	VerifyPassword(ctx context.Context, identifier, password string) (*model.User, error)
}

Directory is an external password authority (protocolauth.Directory).

type Entry

type Entry struct {
	Name   string
	Origin string
	// From says where an environment provider is defined
	// (FILEX_AUTH_DRIVERS, the config file, the built-in default).
	From string
	// Driver is nil when the provider could not start; Err says why.
	Driver auth.Driver
	Err    string
	// Directory: an LDAP provider that also judges passwords on the file
	// protocols (WebDAV, SFTP, FTPS, S3, NFS).
	Directory bool
	// Display is the configuration shown on the page for an environment
	// provider — secrets left out (Secrets says which are set).
	Display map[string]any
	// Secrets names the secret fields an environment provider has set.
	Secrets []string
	// contains filtered or unexported fields
}

Entry is one provider the instance knows of.

func NewEnvEntry

func NewEnvEntry(name, from string, cfg map[string]any, d auth.Driver, err error, directory bool) Entry

NewEnvEntry records a provider the environment/config file defines. cfg is what Build was given; the page is shown a copy with every secret removed.

func (Entry) Config

func (e Entry) Config() map[string]any

Config is the configuration the environment gave a provider (secrets included; in memory only).

func (Entry) Live

func (e Entry) Live() bool

Live reports whether the provider is running.

type Field

type Field struct {
	Key      string    `json:"key"`
	Kind     FieldKind `json:"kind"`
	Required bool      `json:"required,omitempty"`
	// Default is what an absent value means, said so the page can show it.
	Default string `json:"default,omitempty"`
}

Field is one setting a managed provider takes on the page.

func FieldOf

func FieldOf(name, key string) (Field, bool)

FieldOf returns a provider's field by key.

type FieldKind

type FieldKind string

FieldKind is how a page field is stored and handed to the driver.

const (
	FieldText   FieldKind = "text"
	FieldSecret FieldKind = "secret"
	FieldBool   FieldKind = "bool"
)

Field kinds.

type Live

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

Live owns the running providers and swaps them when the page saves.

func New

func New(ctx context.Context, opts Options, env []Entry) (*Live, error)

New prepares a Live over the environment's providers (already built by the caller through Build) and installs a first set holding only those, so the password sign-in the environment configured works before anything the page saved has been looked at.

func (*Live) AdminPaths

func (l *Live) AdminPaths(ctx context.Context, without string) []string

AdminPaths lists the ways an administrator can still sign in with the running set, leaving out one provider (the one about to be switched off).

  • "password": password sign-in is on (`local`) and at least one enabled administrator has a password;
  • "recovery": the bootstrap administrator's recovery sign-in is on, and that account exists, is enabled and has a password;
  • the name of every other running identity provider or directory.

⚠ An identity provider counts as a way in although filex cannot prove an administrator's account is on the other side of it: the rule this serves is "never switch off the LAST way in", and a provider that is running is a way in until shown otherwise. The two password paths are checked for real, because an instance whose administrators all came in through SSO has a password form nobody can use.

func (*Live) Current

func (l *Live) Current() *Set

Current is the running set.

func (*Live) Dir

func (l *Live) Dir() Directory

Dir is the directory the file protocols consult.

func (*Live) EnvDefines

func (l *Live) EnvDefines(name string) (Entry, bool)

EnvDefines reports whether the environment lists a provider — in which case it is the effective one, and the page may not change it.

func (*Live) Environment

func (l *Live) Environment() []Entry

Environment is the environment's providers, in the order it listed them.

func (*Live) Login

func (l *Live) Login() auth.LoginDriver

Login is the password sign-in the handlers hold. It always asks the running set, so a reload changes what it does without re-wiring anything.

func (*Live) LoginName

func (l *Live) LoginName() string

LoginName names the password chain, for the boot line.

func (*Live) OIDC

func (l *Live) OIDC() auth.OIDCDriver

OIDC is the OIDC flow the handlers hold.

func (*Live) OnSwap

func (l *Live) OnSwap(f func(*Set))

OnSwap registers a callback run with every new set (and once, now).

func (*Live) Options

func (l *Live) Options() Options

Options returns the options Live runs with.

func (*Live) Reload

func (l *Live) Reload(ctx context.Context) error

Reload reads the page's providers and swaps in the set they make.

⚠ A provider that did not change is not rebuilt (an OIDC discovery per save of an unrelated LDAP field would be a network round trip for nothing), and a provider that fails to start is left out with its reason — the environment's providers and every other page provider stay exactly as they were. There is no path through here that drops `local`.

func (*Live) Start

func (l *Live) Start(ctx context.Context)

Start imports what an earlier version saved (switched off), builds the page's enabled providers and, when one could not start, keeps re-trying it in the background.

type Options

type Options struct {
	Store         db.Store
	Box           *secretbox.Box
	MultiTenant   bool
	RecoveryLogin bool
	PublicURL     string
	// BuildTimeout bounds one page provider's construction (an OIDC
	// discovery that hangs must not hold a save, or the boot, hostage).
	BuildTimeout time.Duration
	// RetryBackoff re-tries, after boot, a page provider that could not
	// start — its IdP may be booting alongside filex.
	RetryBackoff []time.Duration
	Log          *slog.Logger
}

Options is what Live needs from the server.

type Set

type Set struct {
	Entries []Entry
	// contains filtered or unexported fields
}

Set is one immutable arrangement of the providers. Live swaps whole sets, so a request never sees half of a reload.

func (*Set) Entry

func (s *Set) Entry(name string) (Entry, bool)

Entry returns a provider by name.

func (*Set) HasDirectory

func (s *Set) HasDirectory() bool

HasDirectory reports whether a directory judges passwords on the file protocols.

func (*Set) Names

func (s *Set) Names() []string

Names are the sign-in methods the login page offers: every provider the environment lists (as before) and every page provider that is running.

func (*Set) PasswordLogin

func (s *Set) PasswordLogin() bool

PasswordLogin reports whether the password form is answered at all.

func (*Set) Recovery

func (s *Set) Recovery() bool

Recovery reports whether the bootstrap administrator's recovery sign-in is on.

type Stored

type Stored struct {
	Name    string
	Enabled bool
	// Legacy: saved before v0.43.0 and never applied — review and enable.
	Legacy bool
	// Values are the stored field values; a secret stays SEALED here.
	Values map[string]string
	// Exists: at least one row is stored for this provider.
	Exists bool
}

Stored is what the settings table holds for one managed provider.

func Merge

func Merge(cur *Stored, ch Change, box *secretbox.Box) (*Stored, []string, error)

Merge applies a change to what is stored, without writing anything, and returns the next stored state plus the names of the fields that changed (never their values — this list goes into the audit row).

Jump to

Keyboard shortcuts

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