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

auth

package
v0.19.0 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package auth keeps authentication behind an interface.

Invariant 4 in CLAUDE.md: no handler knows about GitLab, GitHub or WebAuthn directly. Everything outside this package works with a player_id passed through the request context. Which provider produced it is invisible beyond that boundary (docs/adr/0003).

Index

Constants

View Source
const FobWindowDuration = 60 * time.Second

FobWindowDuration is how long "Fob koppeln" waits for a fob at the reader. Long enough to walk to the table and press a button, short enough that a window nobody used does not keep the next person waiting (docs/adr/0025).

View Source
const SessionCookieName = "schmetterpause_session"

SessionCookieName is the cookie a returning player is recognised by.

Variables

View Source
var ErrBadUID = errors.New("a uid is 4, 7 or 10 bytes of hex")

ErrBadUID is a UID that is not what a reader produces: hex, 4, 7 or 10 bytes. It is a bug on the sending side, not a card, so it says so.

View Source
var ErrInvalidCookie = errors.New("session cookie is not valid")

ErrInvalidCookie reports a cookie whose signature does not match. It is not an error the player caused — an old cookie after a key rotation looks the same — so callers treat it as "nobody is signed in" and move on.

View Source
var ErrUnknownFob = errors.New("unknown fob")

ErrUnknownFob is the answer for a fob nobody owns while no window is open. It carries nothing about the fob on purpose: whoever logs it logs nothing that identifies a card (docs/adr/0025).

Functions

func Middleware

func Middleware(a Authenticator, log *slog.Logger) func(http.Handler) http.Handler

Middleware puts the identified player_id into the context. It turns nobody away — that is each handler's decision, via RequirePlayer.

A failure to identify is logged and treated as anonymous rather than answered with a 500. If the database is down, the pages that need no player should still render, and /healthz must answer regardless.

func NewSubject

func NewSubject() string

NewSubject mints an opaque subject for the local provider.

Deliberately not the player_id: the cookie should say nothing about the record behind it, and a later provider supplies its own subject anyway.

func PlayerID

func PlayerID(ctx context.Context) (uuid.UUID, bool)

PlayerID reads the player_id from the context. The second return value is false when the request is not attributed to any player.

func RequireAdmin

func RequireAdmin(is AdminCheck, log *slog.Logger) func(http.Handler) http.Handler

RequireAdmin lets through only requests from somebody who may act for other people (docs/adr/0008).

It sits on top of RequirePlayer rather than beside it: an admin acts in their own signed-in session, which is the property that makes an action attributable to a person instead of to a browser. Without that, this would be the kiosk again with a different name.

A failed check is refused rather than logged and waved through. That is the opposite of what Middleware does with a failed identification, and the reason is the direction of the mistake: a page that cannot say who you are should still render, and an action on somebody else's record should not happen if we are not sure who asked.

func RequirePlayer

func RequirePlayer(next http.Handler) http.Handler

RequirePlayer lets through only requests attributed to a player.

func WithPlayerID

func WithPlayerID(ctx context.Context, id uuid.UUID) context.Context

WithPlayerID stores the player_id in the context.

Types

type AdminCheck

type AdminCheck func(ctx context.Context, id uuid.UUID) (bool, error)

AdminCheck answers whether a player_id holds the admin flag.

A function rather than the repository itself, so this package keeps knowing nothing about storage — and so a test can say "this one is an admin" without a database.

type Anonymous

type Anonymous struct{}

Anonymous recognises nobody. Useful in tests and wherever a server should run without a login implementation.

func (Anonymous) ClearCookie

func (Anonymous) ClearCookie(http.ResponseWriter)

ClearCookie has nothing to clear.

func (Anonymous) EndSession

EndSession has no session to end.

func (Anonymous) Identify

func (Anonymous) Identify(*http.Request) (uuid.UUID, error)

Identify recognises nobody.

func (Anonymous) SetCookie

func (Anonymous) SetCookie(http.ResponseWriter, string)

SetCookie remembers nobody.

type Authenticator

type Authenticator interface {
	// Identify returns the player_id for a request. Nobody being signed in is
	// an ordinary state, reported as uuid.Nil with a nil error — only a
	// genuine failure, such as an unreachable database, returns an error.
	Identify(r *http.Request) (uuid.UUID, error)
}

Authenticator determines the player behind a request.

The MVP has exactly one implementation: recognition through a signed cookie (AP2). OIDC and WebAuthn arrive as further implementations without any handler changing.

type CookieAuthenticator

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

CookieAuthenticator recognises a player by a signed cookie.

The cookie carries an opaque subject, not the player_id. Resolving it goes through the identities table, which is the same path OIDC and WebAuthn will take later (docs/adr/0003) — so adding those changes this package and nothing else.

func NewCookieAuthenticator

func NewCookieAuthenticator(identities repository.IdentityRepository, key []byte, secure bool) *CookieAuthenticator

NewCookieAuthenticator builds the authenticator. The key signs cookies and comes from the environment; it must survive restarts, or every player is a stranger after a deployment.

func (*CookieAuthenticator) ClearCookie

func (a *CookieAuthenticator) ClearCookie(w http.ResponseWriter)

ClearCookie removes the session cookie.

func (*CookieAuthenticator) EndSession

EndSession forgets this browser.

The cookie goes first and unconditionally: whatever else happens, the browser has to stop being recognised, because that is what was asked for. Removing the identity is the part that can fail, and a failure there leaves a row nobody can present — the subject only ever existed in the cookie that has just been cleared.

func (*CookieAuthenticator) Identify

func (a *CookieAuthenticator) Identify(r *http.Request) (uuid.UUID, error)

Identify resolves the signed cookie to a player.

A missing, malformed or unknown cookie yields uuid.Nil and no error: not being signed in is an ordinary state, not a failure. Only a database problem produces an error.

func (*CookieAuthenticator) SetCookie

func (a *CookieAuthenticator) SetCookie(w http.ResponseWriter, subject string)

SetCookie signs subject and writes the session cookie.

type FobCheckin added in v0.17.0

type FobCheckin struct {
	PlayerID  uuid.UUID
	CheckinID uuid.UUID
	// Linked is true when this resolve linked the fob, which the caller logs:
	// a new way in to somebody's account deserves a line.
	Linked bool
}

FobCheckin is what a successful resolve hands the Zählwerk.

type Fobs added in v0.17.0

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

Fobs resolves key fobs to players for the Zählwerk and lets a player link and remove their own.

It sits here, beside the cookie, because a fob is a provider (invariant 4): nothing outside this package knows that a fob is stored as an HMAC or which provider name it has. It is not an Authenticator, though. A UID can be read and copied by anybody standing close enough, so a fob names a player for a match that still has to be confirmed and never opens a session.

func NewFobs added in v0.17.0

func NewFobs(store repository.Store, key []byte) *Fobs

NewFobs builds the resolver. key is SP_RFID_UID_KEY; the caller does not build a Fobs at all without one.

func (*Fobs) CloseWindow added in v0.17.0

func (f *Fobs) CloseWindow(ctx context.Context, playerID uuid.UUID) error

CloseWindow gives playerID's window up early.

func (*Fobs) ForPlayer added in v0.17.0

func (f *Fobs) ForPlayer(ctx context.Context, playerID uuid.UUID) ([]domain.Identity, error)

ForPlayer lists playerID's fobs, oldest first.

func (*Fobs) OpenWindow added in v0.17.0

func (f *Fobs) OpenWindow(ctx context.Context, playerID uuid.UUID) (domain.FobWindow, error)

OpenWindow opens the link window for playerID. While somebody else holds one, the error is domain.ErrConflict and the window returned is theirs, so the page can say until when.

func (*Fobs) Remove added in v0.17.0

func (f *Fobs) Remove(ctx context.Context, playerID uuid.UUID, subject string) error

Remove unlinks one of playerID's fobs. A subject that is not one of theirs is domain.ErrNotFound, so nobody removes somebody else's fob by guessing at a form value.

func (*Fobs) Resolve added in v0.17.0

func (f *Fobs) Resolve(ctx context.Context, uid string, half domain.Half) (FobCheckin, error)

Resolve names the player behind uid and records their check-in at half.

A known fob resolves to its owner. An unknown one is linked to whoever holds the open window, and that also checks them in. An unknown one with no window is ErrUnknownFob, and nothing is written.

A fob that already belongs to somebody is never relinked, and it does not use up somebody else's window.

func (*Fobs) Window added in v0.17.0

func (f *Fobs) Window(ctx context.Context) (domain.FobWindow, error)

Window returns the open window, whoever holds it, or domain.ErrNotFound.

func (*Fobs) WithClock added in v0.17.0

func (f *Fobs) WithClock(now func() time.Time) *Fobs

WithClock replaces the clock. Tests only.

type SessionAuthenticator

type SessionAuthenticator interface {
	Authenticator
	// SetCookie writes the recognition cookie for subject.
	SetCookie(w http.ResponseWriter, subject string)
	// ClearCookie removes it.
	ClearCookie(w http.ResponseWriter)
	// EndSession forgets this browser: it clears the recognition cookie and
	// removes whatever the provider stored to recognise it by, so the
	// session is gone from the database as well as from the browser.
	//
	// Only this browser. A player holds several identities by design
	// (docs/adr/0003), so signing out on a borrowed phone leaves the one at
	// home signed in.
	//
	// The cookie is cleared whether or not the rest succeeds. A caller that
	// gets an error has still been signed out; the error is worth logging
	// and not worth refusing the request over, because the alternative is
	// telling somebody they are still signed in on a device they wanted to
	// leave.
	EndSession(w http.ResponseWriter, r *http.Request) error
}

SessionAuthenticator is an Authenticator that can also start and end a session. Handlers that sign somebody in need this; everything else is happier with the narrower Authenticator.

Jump to

Keyboard shortcuts

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