76 lines
3.4 KiB
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
|
|
}
|