Documentation
¶
Overview ¶
Package sessionrefresh implements the session-refresh slice: validates an opaque refresh token via refresh.Store and issues a fresh access JWT.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SliceMetadata ¶
SliceMetadata returns the package-scope *metadata.SliceMeta projected from slice.yaml. Composition roots consume this via cell.MustNewBaseSliceFromMeta(<slicePkg>.SliceMetadata()) — the typed funnel that replaces the legacy `cell.NewBaseSlice(id, cellID, level)` literal pattern.
Types ¶
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler is the route handler for the sessionrefresh slice. The generated handler emits Public:true so no JWT is required for this route.
func NewHandler ¶
NewHandler creates a sessionrefresh Handler using the generated refresh handler. cookieTTL is the refresh-token lifetime used as the Max-Age on the Set-Cookie header (BR-005 #1278). No policy argument: the refresh endpoint is Public (no JWT required).
func (*Handler) RegisterRoutes ¶
func (h *Handler) RegisterRoutes(mux kcell.RouteHandler) error
RegisterRoutes mounts the refresh contract handler on mux. The refresh endpoint is Public (no JWT required). Tenant is derived from the refreshed user's TenantID field (#1337 PR-2 stopgap; PR-3 will carry tenant in the session/refresh row for true RLS isolation).
The cookie middleware is applied via cellmw.NewHeaderInjectMux so that every registered route gets httpcookie.Middleware applied at the mux level.
type NewServiceParams ¶
type NewServiceParams struct {
SessionStore session.Store
RoleRepo ports.RoleRepository
UserRepo ports.UserRepository
RefreshStore refresh.Store
Issuer *auth.JWTIssuer
}
NewServiceParams holds the required dependencies for NewService. Keeping the required-dependency set in a struct reduces the positional-parameter count and makes call sites self-documenting (S107).
type Option ¶
type Option func(*Service)
Option configures a session-refresh Service.
func WithInvalidator ¶
func WithInvalidator(inv credentialinvalidate.Applier) Option
WithInvalidator injects the credential-invalidation funnel used to cascade epoch bump + session revoke + refresh chain revoke on refresh-token reuse detection. Required — NewService fails fast when nil. Nil is silently ignored to keep the option idempotent; final nil enforcement is in NewService.
Type rationale: the parameter is credentialinvalidate.Applier (the exported funnel interface), not *credentialinvalidate.Invalidator. The interface must live in credentialinvalidate so the callsite-level archtest (CREDENTIAL-INVALIDATE-UPSTREAM-CALLER-01) can resolve s.invalidator.Apply via info.Selections to a *types.Func with Pkg=credentialinvalidate. Production wiring still passes a *Invalidator (which satisfies Applier); unit tests inject a spy.
func WithTxManager ¶
func WithTxManager(tx persistence.CellTxManager) Option
WithTxManager wires the cross-store CellTxManager. The Refresh flow wraps the validate→update→rotate sequence in a single RunInTx so the session repo and refresh store updates share one commit boundary; nil tx is silently ignored to keep the option idempotent — final non-nil enforcement is in NewService. Callers obtain the sealed marker via persistence.WrapForCell from a composition root.
type RefreshAdapter ¶
type RefreshAdapter struct{ S *Service }
RefreshAdapter implements refreshgen.Service for http.auth.refresh.v1. It adapts the slice-internal Service (Refresh takes a raw token string) to the generated interface (Refresh takes *refreshgen.Request).
func (RefreshAdapter) Refresh ¶
func (a RefreshAdapter) Refresh(ctx context.Context, req *refreshgen.Request) (refreshgen.RefreshResponseObject, error)
Refresh implements refreshgen.Service. The generated handler already validates and decodes refreshToken from the request body.
Token source (cookie-first, body-fallback — BR-005 #1278):
- If the inbound __Host-gocell_rt cookie is present, its value is used as the refresh token; the request body refreshToken field is ignored.
- Otherwise the body refreshToken field is used.
On success a refreshed Set-Cookie is emitted via the httpcookie middleware so the browser's httpOnly jar is always kept current.
All rejections (revoked/subject-mismatch/user-not-active/stale-epoch/reuse) surface as 401 via framework fallback (ADR §A13 single-envelope); 503 is reserved for infra outages that prevent evaluation of the request.
Why every declared status here goes through (nil, err) framework fallback rather than typed Refresh{401,503}ErrorResponse structs: ADR D7 makes ADAPTER-RETURNS-DECLARED-TYPES-01 a *ceiling* — returning zero typed error structs is legal; the archtest only rejects returning an *un*declared typed status, it never forces declared statuses to be typed. Both paths apply the same status-aware redaction (httputil.WriteError derives status + strips 5xx details from errcode.Kind exactly as the generated typed path does), so for an endpoint whose errors carry no per-status business body beyond the shared envelope the framework path is the simpler equivalent — not a redaction gap. (Pre-existing convention; the 403 typed branch removed in #940 was the lone exception and is now gone.)
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service implements token refresh logic.
func NewService ¶
func NewService( clk clock.Clock, params NewServiceParams, logger *slog.Logger, opts ...Option, ) (*Service, error)
NewService creates a session-refresh Service.
userRepo is required (P1-3 fix): fetchPasswordResetRequired silently returns false when userRepo is nil, which bypasses the password-reset security gate.
refreshStore owns both token-state validation and rotation — the slice no longer parses JWTs or performs application-layer reuse detection.
opts allows future functional extensions without breaking callers (F8).
func (*Service) Refresh ¶
Refresh validates the presented opaque refresh token, checks the backing session and subject, mints a new access JWT, and rotates the refresh token. Token rejection surfaces ErrAuthRefreshFailed; dependency failures surface ErrAuthRefreshUnavailable so clients do not confuse an outage with invalid credentials.
检查顺序 (ADR §A11 重写 + §A12 wire-uniformity + §A13 single-envelope):
- refreshStore.Peek + verifySession
- **session-state inline check (RevokedAt)** — revoked → cascadeRevoke + uniform 401 ErrAuthRefreshFailed; user is never loaded.
- subject-mismatch check → cascadeRevoke + uniform 401
- fetchUserForRefresh (user lookup)
- rejectIfUserNotActive — uniform 401 ErrAuthRefreshFailed (ADR §A13 single-envelope: indistinguishable from revoked/stale/reuse to caller)
- rejectIfStaleEpoch → cascadeRevoke + uniform 401
- mint + rotate
Presenting an access JWT (or any string that does not parse as the opaque selector.verifier wire format) fails ParseOpaque inside refresh.Store and returns refresh.ErrRejected — the same fail-closed behavior the access-token confusion defense relies on.
Session lifecycle: refresh does NOT mutate session.Store. session.ID is stable from login to logout; the access JWT carries the same sid claim across rotations. AuthzEpoch staleness is enforced by sessionvalidate reading users.authz_epoch (S4b), not by session-row rotation. This aligns with OAuth2 RFC 6749 §6 (refresh = same authorization grant), OIDC Back-Channel Logout (sid stable across refresh), and the ory-fosite / zitadel / keycloak implementations.
Transactional scope: the Peek → verifySession → Rotate sequence runs inside txRunner.RunInTx so refresh-store writes commit atomically with the caller-supplied transaction boundary. session.Store is read-only on the refresh path; cascade revokes go through refreshStore.RevokeSessionDetached, which intentionally bypasses the outer transaction (PR#395 detached-context invariant).