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 }