textmachine/platform/internal/auth/principal.go

76 lines
3.4 KiB
Go

package auth
import (
"context"
"time"
)
// Presentation is HOW the session was presented. It exists for the CSRF layer and for logs, never
// for authorization: a session is a session whichever way it arrived.
type Presentation string
const (
ViaCookie Presentation = "cookie"
ViaBearer Presentation = "bearer"
)
// Principal is the authenticated caller.
//
// It carries the caller's identity AND the means to re-ask whether that identity still holds. The
// two travel together deliberately: a handler that holds a principal can always find out whether the
// session behind it is still usable, so "authenticated once, and never asked again" is not a state
// this package can hand anybody (register row PD-379, where exactly that state was the vulnerability).
type Principal struct {
UserID string
Via Presentation
// life is the store handle bound at authentication time. Unexported, and reachable only through
// StillLive: a handler is given the QUESTION and never the credential behind it.
life SessionLife
}
// StillLive re-asks the store whether the session that authenticated this request may keep a
// long-lived response open. It answers nil while it may, ErrNoSession once it may not, and a store
// error when it could not be asked — which is NOT an answer, and a caller must not read it as one.
//
// Only the event stream needs it. Every other handler is admitted at the door and answers in
// milliseconds, so Require deciding once is the whole story; that route runs for hours on a single
// request, and until this existed "sign out everywhere" and both session clocks left it running.
//
// ⚠ THE IDLE WINDOW IS NOT PART OF THE QUESTION, and that is a correction rather than an oversight.
// The idle deadline slides on a REQUEST (Require, below), and a stream is a single request for its
// whole life — so a stream can never slide its own window, and asking the idle question here would
// end the stream of a user who is sitting and watching it. What ends a stream is the two facts a
// stream cannot influence by existing: the session was revoked, or its ABSOLUTE ceiling passed.
//
// The zero Principal answers ErrNoSession rather than nil. There is no principal without a guard, so
// this is unreachable through the middleware; it is written this way because the one direction it
// could ever fail in must be the closed one.
func (p Principal) StillLive(ctx context.Context) error {
if p.life.sessions == nil {
return ErrNoSession
}
return p.life.sessions.StillLive(ctx, p.life.digest, p.life.now())
}
type principalKey struct{}
// withPrincipal is deliberately unexported: the principal is created in the middleware of this
// package and nowhere else (D39.84). A handler that could mint one could also invent a user.
func withPrincipal(ctx context.Context, p Principal) context.Context {
return context.WithValue(ctx, principalKey{}, p)
}
// FromContext returns the principal established by Authenticator.
func FromContext(ctx context.Context) (Principal, bool) {
p, ok := ctx.Value(principalKey{}).(Principal)
return p, ok
}
// SessionLife is what a Principal needs to ask its question again: the store, the digest of the
// credential that was presented, and this Authenticator's clock. It is a field of Principal and
// never a value a handler receives, which is what keeps the digest inside this package.
type SessionLife struct {
sessions SessionStore
digest []byte
now func() time.Time
}