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
- Variables
- func Middleware(a Authenticator, log *slog.Logger) func(http.Handler) http.Handler
- func NewSubject() string
- func PlayerID(ctx context.Context) (uuid.UUID, bool)
- func RequireAdmin(is AdminCheck, log *slog.Logger) func(http.Handler) http.Handler
- func RequirePlayer(next http.Handler) http.Handler
- func WithPlayerID(ctx context.Context, id uuid.UUID) context.Context
- type AdminCheck
- type Anonymous
- type Authenticator
- type CookieAuthenticator
- func (a *CookieAuthenticator) ClearCookie(w http.ResponseWriter)
- func (a *CookieAuthenticator) EndSession(w http.ResponseWriter, r *http.Request) error
- func (a *CookieAuthenticator) Identify(r *http.Request) (uuid.UUID, error)
- func (a *CookieAuthenticator) SetCookie(w http.ResponseWriter, subject string)
- type FobCheckin
- type Fobs
- func (f *Fobs) CloseWindow(ctx context.Context, playerID uuid.UUID) error
- func (f *Fobs) ForPlayer(ctx context.Context, playerID uuid.UUID) ([]domain.Identity, error)
- func (f *Fobs) OpenWindow(ctx context.Context, playerID uuid.UUID) (domain.FobWindow, error)
- func (f *Fobs) Remove(ctx context.Context, playerID uuid.UUID, subject string) error
- func (f *Fobs) Resolve(ctx context.Context, uid string, half domain.Half) (FobCheckin, error)
- func (f *Fobs) Window(ctx context.Context) (domain.FobWindow, error)
- func (f *Fobs) WithClock(now func() time.Time) *Fobs
- type SessionAuthenticator
Constants ¶
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).
const SessionCookieName = "schmetterpause_session"
SessionCookieName is the cookie a returning player is recognised by.
Variables ¶
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
RequirePlayer lets through only requests attributed to a player.
Types ¶
type AdminCheck ¶
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.
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 ¶
func (a *CookieAuthenticator) EndSession(w http.ResponseWriter, r *http.Request) error
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 ¶
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
CloseWindow gives playerID's window up early.
func (*Fobs) OpenWindow ¶ added in v0.17.0
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
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
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.
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.