sessionrefresh

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 27 Imported by: 0

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

func SliceMetadata() *metadata.SliceMeta

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

func NewHandler(svc *Service, cookieTTL time.Duration) *Handler

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

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):

  1. If the inbound __Host-gocell_rt cookie is present, its value is used as the refresh token; the request body refreshToken field is ignored.
  2. 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

func (s *Service) Refresh(ctx context.Context, refreshToken string) (dto.TokenPair, error)

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):

  1. refreshStore.Peek + verifySession
  2. **session-state inline check (RevokedAt)** — revoked → cascadeRevoke + uniform 401 ErrAuthRefreshFailed; user is never loaded.
  3. subject-mismatch check → cascadeRevoke + uniform 401
  4. fetchUserForRefresh (user lookup)
  5. rejectIfUserNotActive — uniform 401 ErrAuthRefreshFailed (ADR §A13 single-envelope: indistinguishable from revoked/stale/reuse to caller)
  6. rejectIfStaleEpoch → cascadeRevoke + uniform 401
  7. 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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL