Help us shape the future of Go! Take the 2026 Go Developer Survey

middleware

package
v1.9.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Overview

Package middleware provides HTTP middleware for authentication and context resolution.

Index

Constants

View Source
const DefaultStepUpWindow = 5 * time.Minute

DefaultStepUpWindow is how recently a caller must have signed in for a sensitive change (enrolling a second factor, registering a passkey) to proceed on session auth alone.

View Source
const PublishableKeyHeader = "X-Publishable-Key"

PublishableKeyHeader is the canonical request header carrying a `pk_*` publishable key. Frontends set this once on every request so the server can route signup, signin, and other public-auth calls to the correct app without an explicit `app_id` field in the body.

View Source
const PublishableKeyQuery = "publishable_key"

PublishableKeyQuery is the query-string fallback for GET endpoints where setting a custom header is awkward (mirrors the existing /v1/client-config?key=… convention).

View Source
const RequestIDHeader = "X-Request-ID"

RequestIDHeader carries the request id back to the client on a 500, so a support ticket can quote it and the log line can be found.

Variables

View Source
var ErrDPoPBindingFailed = errors.New("authsome: token requires proof of possession")

ErrDPoPBindingFailed reports a bound token presented without an acceptable proof, for callers outside the middleware chain that only get to answer yes or no and cannot write an RFC 9449 challenge.

Functions

func ActorsFrom

func ActorsFrom(ctx context.Context) (principal.Chain, bool)

ActorsFrom returns the actor chain previously stored with WithActors.

func AppFrom

func AppFrom(ctx context.Context) (*app.App, bool)

AppFrom retrieves the resolved app from the context.

func AppIDFrom

func AppIDFrom(ctx context.Context) (id.AppID, bool)

AppIDFrom retrieves the app ID from the context.

func AuthMethodFrom

func AuthMethodFrom(ctx context.Context) (string, bool)

AuthMethodFrom retrieves the authentication method from the context.

func AuthMiddleware

func AuthMiddleware(resolveSession SessionResolver, resolveUser UserResolver, logger log.Logger, binding ...SessionBindingConfig) forge.Middleware

AuthMiddleware extracts the session token from the Authorization header, resolves the session and user, and stores them in context. This middleware is the forge.Scope producer — it resolves the authenticated identity and sets AppID/OrgID on context for all downstream extensions.

func AuthMiddlewareWithJWT

func AuthMiddlewareWithJWT(
	resolveSession SessionResolver,
	resolveUser UserResolver,
	strategies StrategyAuthenticator,
	jwtValidator JWTValidator,
	logger log.Logger,
	binding ...SessionBindingConfig,
) forge.Middleware

AuthMiddlewareWithJWT creates middleware that supports JWT token validation in addition to opaque session tokens and strategy auth. JWT tokens are validated stateless (no DB lookup for the access token itself).

func AuthMiddlewareWithStrategies

func AuthMiddlewareWithStrategies(
	resolveSession SessionResolver,
	resolveUser UserResolver,
	strategies StrategyAuthenticator,
	logger log.Logger,
	binding ...SessionBindingConfig,
) forge.Middleware

AuthMiddlewareWithStrategies creates middleware that tries bearer-session resolution first, then falls back to the strategy registry for alternative auth methods (API keys, etc.). Bearer tokens with the "ask_" prefix skip session resolution and go directly to the strategy chain.

When a JWTValidator is provided, JWT tokens (detected by containing two dots) are validated stateless — no DB lookup needed for the access token.

Flow:

  1. Extract bearer token from Authorization header
  2. If token is JWT and jwtValidator is set → validate stateless
  3. If token exists and is NOT an API key prefix → resolve as session
  4. If no session resolved → try strategies.Authenticate(ctx, r)
  5. If strategy succeeds → set context with User, Session, AppID, Scope
  6. If all fail → continue unauthenticated (RequireAuth enforces)

func AuthzActorsFrom

func AuthzActorsFrom(ctx context.Context) principal.Chain

AuthzActorsFrom returns the actor chain Warden must independently authorize for this request.

The session is asked first, because Session.AuthzActors is where the impersonation exception lives: an admin acting as a user must be evaluated as that user alone, so the chain it returns is deliberately empty. Reading sess.Actors here instead would intersect the admin's own permissions into every check and invert impersonation, letting the admin do LESS than the person they are standing in for.

The context chain is the fallback, for callers that arrive without a session row behind them.

func AutoRefreshMiddleware

func AutoRefreshMiddleware(
	refresher SessionRefresher,
	configResolver AutoRefreshConfigResolver,
	logger log.Logger,
	cookieSetter ...CookieSetter,
) forge.Middleware

AutoRefreshMiddleware checks if the authenticated session's access token is near expiry and, if so, transparently refreshes it. New tokens are returned in response headers:

  • X-Auth-Token: the new access token
  • X-Auth-Refresh-Token: the new refresh token (only if ExposeRefreshToken is true)
  • X-Auth-Token-Expires-At: RFC 3339 expiration timestamp

When a CookieSetter is provided, the session cookie is also updated with the new access token so the browser cookie stays in sync.

This middleware MUST run after AuthMiddleware so the session is on context. On refresh failure, the original response is returned unchanged.

func CSRF

func CSRF(cfg CSRFConfig) forge.Middleware

CSRF refuses an unsafe request that was authenticated by the session cookie unless the browser says it came from the same site, or its Origin is one the operator listed. A cookie travels with any request a browser makes, including one another site's page provoked; the Sec-Fetch-Site header (every current browser sends it) and the Origin header (sent on cross-site unsafe requests) are what tell those apart. A request with neither header is refused: a browser that sends none is older than this check, and a non-browser client does not use cookies.

Requests authenticated by bearer token, DPoP or API key are untouched: a credential the script had to attach itself cannot be attached by another site's page.

func CallerSubjectFrom

func CallerSubjectFrom(ctx context.Context) (principal.Ref, bool)

CallerSubjectFrom returns the principal a permission check must evaluate.

The subject is who the request is FOR, never who is doing the acting: on a delegated session that is the human, and the agent is in the actor chain. Resolution order matches PrincipalRefFrom (a resolved principal, then a user, then a non-human session subject) with one addition at the end. A human session whose user row failed to resolve still has its user ID on the context, and falling back to it keeps this exactly as permissive as the UserIDFrom gate RequirePermission used before chains existed: no more, no less.

func CaptchaMiddleware

func CaptchaMiddleware(opts CaptchaOptions) forge.Middleware

CaptchaMiddleware gates a request on a successful captcha verification.

The token is read from header X-Captcha-Token first, falling back to the form/query field "captcha_token" so the same middleware works for JSON APIs and HTML form posts.

Behaviour:

  • If auth.captcha_required is false for the resolved app: pass through.
  • Token missing: 403 with type="captcha_required".
  • Token rejected by verifier: 403 with type="captcha_invalid".
  • Verifier transient failure: 503 with type="captcha_unavailable".

The verifier is constructed lazily and cached per (appID, provider, secret) so secret rotation in the dashboard takes effect on the next request without a process restart.

Order: this middleware must run BEFORE any CPU-bound work (password hashing, etc.) so a captcha-failed probe doesn't pay argon2 cost server-side.

func ClientAuthMiddleware

func ClientAuthMiddleware(client *authclient.Client, logger log.Logger, bind ...SessionBindingConfig) forge.Middleware

ClientAuthMiddleware validates tokens by calling the remote authsome server. Opaque tokens are validated via the /v1/introspect endpoint. Results are cached briefly to avoid per-request HTTP calls.

This middleware sets the same context keys as the engine-based AuthMiddleware (WithUser, WithUserID, WithAppID, WithOrgID, WithSessionID, WithAuthMethod) so downstream code works identically regardless of which middleware is used.

bind is optional and at most one is read. It carries the DPoP validator this service enforces RFC 9449 with. Enforcement is a property of the token, not of the service holding it, so a token whose introspection response carries a cnf claim needs a proof here exactly as it would at the identity server. Pass no config and a bound token is refused outright, because a service that cannot check a binding must not admit it unchecked.

func ClientAutoRefreshMiddleware

func ClientAutoRefreshMiddleware(
	refresher ClientSessionRefresher,
	cfg AutoRefreshConfig,
	logger log.Logger,
) forge.Middleware

ClientAutoRefreshMiddleware is the client-mode counterpart to AutoRefreshMiddleware: it keeps a browser's cookie session alive by rotating it shortly before the access token expires.

Why client mode needs its own:

AutoRefreshMiddleware and SessionActivityMiddleware both reach into the session store — one to read sess.ExpiresAt and call Engine.Refresh, the other to extend the row — so neither can run in a service that has no engine. Extension.Middlewares() therefore returned only ClientAuthMiddleware in client mode, and the comment there said session refresh was "Portal's responsibility". Nothing took up that responsibility, so a cookie session died at the access token's TTL (one hour by default) no matter how actively it was being used: the cookie's own Max-Age is that same TTL, and ResolveSessionByToken refuses the session the moment it lapses.

How it works:

POST /v1/refresh is cookie-first, rotating from the session's server-side refresh token when the request carries a valid session cookie. So this middleware never needs the refresh token — which is just as well, since a client-mode service is only ever shown the access token. It forwards the browser's Cookie header, then replays the identity server's Set-Cookie response onto its own response so cookie attributes stay resolved in exactly one place.

Why the work happens BEFORE the handler:

Forge writes the status line and headers straight through to the connection — it does not buffer the response. A header set after next() returns has already missed the wire, however healthy it looks on an httptest recorder (whose Header() map keeps accepting writes long after WriteHeader snapshotted it). Rotating first is what makes the new cookie actually reach the browser.

The cost is a round-trip in front of the handler, paid only inside the refresh window rather than on every request.

Ordering: this MUST run after ClientAuthMiddleware, which is what records the expiry it reads. A refresh failure is non-fatal — the handler runs either way and the session keeps its original expiry.

func ClientIP

func ClientIP(r *http.Request) string

ClientIP resolves the real client IP for a request. X-Forwarded-For and X-Real-IP are trusted ONLY when the immediate peer is a trusted proxy; otherwise the peer address is returned, so a direct client cannot forge its IP. When the peer is trusted, the X-Forwarded-For chain is walked right-to-left and the first non-proxy address (the real client) is returned.

func CredentialSchemeFrom

func CredentialSchemeFrom(ctx context.Context) (string, bool)

CredentialSchemeFrom returns how the request presented its credential.

func EnforceDPoPForRequest

func EnforceDPoPForRequest(
	ctx context.Context,
	cfg SessionBindingConfig,
	r *http.Request,
	boundJKT, scheme, token, appID, sessionID string,
	logger log.Logger,
) error

EnforceDPoPForRequest applies the rule enforceDPoP applies, for a credential resolved outside the HTTP middleware chain. The forge auth providers are the case that needs it: they return an auth context rather than a response, so they cannot answer with a challenge, but the binding still has to hold. A caller that can write a response should go through the middleware instead, so the client learns which retry would work.

func EnvIDFrom

func EnvIDFrom(ctx context.Context) (id.EnvironmentID, bool)

EnvIDFrom retrieves the environment ID from the context.

func EnvironmentFrom

func EnvironmentFrom(ctx context.Context) (*environment.Environment, bool)

EnvironmentFrom retrieves the full environment from the context.

func EnvironmentMiddleware

func EnvironmentMiddleware(cfg EnvironmentMiddlewareConfig) forge.Middleware

EnvironmentMiddleware resolves the active environment for each request.

Resolution order:

  1. X-Environment-ID header (explicit override)
  2. API key prefix-based detection (if DetectFromKey is configured)
  3. Fall back to the app's default environment

After resolution, it stores the environment, its ID, and its resolved settings on the request context for downstream handlers.

func EnvironmentSettingsFrom

func EnvironmentSettingsFrom(ctx context.Context) (*environment.Settings, bool)

EnvironmentSettingsFrom retrieves the resolved environment settings from the context.

func ExtractCredential

func ExtractCredential(r *http.Request, cookieName string) (scheme, token string)

ExtractCredential pulls the token out of a request and reports which scheme carried it, for auth paths outside this package that have to read a credential the way the middleware reads it. cookieName may be empty for the default session cookie.

func ExtractCredentialFromContext

func ExtractCredentialFromContext(ctx context.Context, r *http.Request, cookieName string) (scheme, token string)

ExtractCredentialFromContext pulls the token out of a request and reports which scheme carried it, accounting for a cookie the engine bridged into the Authorization header earlier in this request. Auth paths outside this package that hold a request context should prefer it over ExtractCredential, which cannot tell a bridged cookie from a real Bearer header.

func HasScopes

func HasScopes(sess *session.Session, scopes ...string) bool

HasScopes reports whether sess holds every scope in scopes.

func IPAccessMiddleware

func IPAccessMiddleware(cfg IPAccessConfig) forge.Middleware

IPAccessMiddleware restricts access based on client IP address ranges. Block list is evaluated first; if the IP matches a blocked range, the request is rejected. If an allow list is configured, only IPs in the allow list are permitted.

func ImpersonatorFrom

func ImpersonatorFrom(ctx context.Context) (id.UserID, bool)

ImpersonatorFrom retrieves the impersonator admin user ID from the context. Returns the zero value and false if the session is not impersonated.

func InternalError

func InternalError(ctx forge.Context, err error) error

InternalError logs err with the request id, puts that id on the response, and returns the generic 500. Handlers use it wherever they used to hand forge the raw error, which put the cause (a store's error text, a path, a driver message) in the log without the id that finds it.

func InternalErrorCtx

func InternalErrorCtx(ctx context.Context, err error) error

InternalErrorCtx is InternalError for code that holds a context.Context.

func IsHTTPS

func IsHTTPS(r *http.Request) bool

IsHTTPS reports whether the client reached the service over TLS, judged the way RequestURL judges it: the socket, or a trusted proxy's word.

func OrgIDFrom

func OrgIDFrom(ctx context.Context) (id.OrgID, bool)

OrgIDFrom retrieves the organization ID from the context.

func PrincipalFrom

func PrincipalFrom(ctx context.Context) (*principal.Principal, bool)

PrincipalFrom returns the resolved caller previously stored with WithPrincipal.

func PrincipalRefFrom

func PrincipalRefFrom(ctx context.Context) (principal.Ref, bool)

PrincipalRefFrom returns the authenticated caller as a principal ref, whatever kind of caller it is.

Three sources, in order, which is what lets this work now and keep working once the principal package is wired all the way through the middleware. First a principal already resolved onto the context. Then a user, which is every human request today. Then a session whose subject is not a person, which is how service accounts, agents and workloads arrive before that wiring exists.

A session with no resolved user behind it is deliberately not a caller. An empty PrincipalKind means user, so such a session is a human one whose user could not be loaded, and treating it as authenticated would turn a failed lookup into an authorization bypass.

func PublishableKeyMiddleware

func PublishableKeyMiddleware(resolver AppResolver, logger log.Logger) forge.Middleware

PublishableKeyMiddleware extracts a publishable key from the request, resolves it to an *app.App via the supplied resolver, and stashes both the App and its AppID on the request context (WithApp + WithAppID).

On success: handlers can read the App via AppFrom or the ID via AppIDFrom. On a missing key: the middleware is a no-op — handlers decide whether the absence is an error (signup/signin reject; some endpoints don't need an app context). On an unknown key: the request still proceeds with no context AppID set, so the handler's resolver returns its own well-shaped 400. The middleware itself never aborts — that keeps composition with rate-limit / captcha / auth middlewares predictable.

resolver must be non-nil; passing nil panics at construction time so a misconfigured server fails loudly during startup rather than silently admitting every request to the platform app.

func RateLimit

func RateLimit(limiter ratelimit.Limiter, cfg RateLimitConfig) forge.Middleware

RateLimit returns a middleware that enforces rate limits using the given limiter.

func RawTokenFrom

func RawTokenFrom(ctx context.Context) (string, bool)

RawTokenFrom extracts the raw bearer token from context.

func RequestURL

func RequestURL(r *http.Request) string

RequestURL reconstructs the absolute URL the client called, for comparison against a DPoP proof's htu claim.

Behind a load balancer r.Host and r.TLS describe the hop, not the client's URL, so X-Forwarded-Proto and X-Forwarded-Host are honoured only when the immediate peer is a trusted proxy. That is the same rule ClientIP applies and it reuses the same configuration, so there is one notion of trusted proxy in this codebase rather than two.

Query and fragment are excluded: RFC 9449 compares htu without them.

func RequireAnyRole

func RequireAnyRole(checker RoleChecker, roles ...string) forge.Middleware

RequireAnyRole returns middleware that checks the user has at least one of the given roles. It first checks for a direct role slug match (fast path), then falls back to Warden's full RBAC+ReBAC+ABAC evaluation via PermissionChecker. This allows platform roles with wildcard permissions (e.g. platform_owner with *:*) to satisfy any role check without explicit slug matching.

func RequireAuth

func RequireAuth() forge.Middleware

RequireAuth returns a forge middleware that rejects unauthenticated requests.

Authenticated means a resolved principal of any kind, not a user specifically. A machine caller carries no *user.User, so gating on one turned away every service account, agent and workload credential before it reached a handler.

func RequireChainPermission

func RequireChainPermission(checker ChainPermissionChecker, action, resource string) forge.Middleware

RequireChainPermission returns a forge.Middleware that checks the caller's subject AND every actor standing between that subject and this request.

This is the guard that makes "delegation can only narrow" true on the request path rather than only inside the engine. Without it, an agent that exchanged a grant for a session whose subject is the human it acts for would be evaluated as that human alone, with its own permissions never intersected and the grant's narrowing thrown away.

func RequireEnvironment

func RequireEnvironment() forge.Middleware

RequireEnvironment returns middleware that rejects requests without a resolved environment context.

func RequirePermission

func RequirePermission(checker PermissionChecker, action, resource string) forge.Middleware

RequirePermission returns a forge.Middleware that checks the authenticated caller has the given permission before continuing.

The route is chosen per request, on whether the caller actually carries an actor chain, and not once at construction on whether the checker happens to have a Can method.

no actors                       -> requireUserPermission, unchanged
actors + chain-aware checker    -> RequireChainPermission
actors + checker without Can    -> refuse

Reading the actors first looks like a weakening of the chain fix and is not. It narrows WHEN the new path runs without narrowing WHAT it enforces. With an empty chain the two paths are semantically identical: HasPermission is literally Can with a nil chain (see Engine.HasPermission), so a chainless request gets the same answer either way. Every request that carries a chain still goes through Can, which is the whole point of the fix.

What it buys is that upgrading does not silently depend on every existing PermissionChecker's Can being correct. Any implementation that satisfies ChainPermissionChecker with a weak or unimplemented Can, in this repository or out of tree, would otherwise start denying requests it used to allow, with no compile error and no obvious symptom. That happened here: a plugin test double answered HasPermission with real logic and Can with a bare `return false, nil`, and every mutating route it guarded turned 403.

The last case does not fall back to evaluating the subject alone. A caller with a chain and no way to check it is exactly the Critical this fix exists to close, so it is refused and logged rather than quietly granted the human's full authority.

func RequireScope

func RequireScope(scopes ...string) forge.Middleware

RequireScope admits a parked OAuth2 session to the route when it holds every named scope, and answers 403 insufficient_scope when it does not (RFC 6750 section 3.1). With no scopes named, any OAuth2 session is admitted. An ordinary session, or no session, passes through untouched: the route's own authentication rules still apply to those.

func ResourceMetadataChallenge

func ResourceMetadataChallenge(metadataURL string) forge.Middleware

ResourceMetadataChallenge adds the RFC 9728 section 5.1 resource_metadata parameter to the WWW-Authenticate header on a 401.

It is how a client bootstraps discovery from a failed call instead of from a URL somebody handed it, which is the path the MCP spec uses. An empty metadataURL makes the middleware inert, so a deployment that has not configured discovery emits nothing rather than a broken hint.

A handler that already set its own challenge keeps it. The RFC 7592 routes set a realm of their own, and clobbering it would lose information the client can use.

Detection reads the error the inner handler returned rather than the response status: forge's Context does not expose a way to swap or inspect the underlying http.ResponseWriter from middleware, and forge's own error handler (which runs at the top of the middleware chain, after this middleware returns) is what actually turns a 401 error into a written response. forge.Unauthorized and friends implement an interface with StatusCode() int and ResponseBody() any; errors.As is used instead of a bare type assertion so a wrapped 401 still matches.

This only sees errors returned by other MIDDLEWARE further down the chain, such as middleware/rbac.go and middleware/auth.go. A route handler's returned error is converted to a written response by forge inside its own handler-conversion wrapper before it is ever handed back to enclosing middleware, so this middleware cannot observe a 401 minted that way at all, whether or not the handler set its own header first.

A route handler that wants the hint on its own 401s has to set the header itself before returning. plugins/oauth2provider/register.go's authenticateRegistration already did this, for a different reason (it sets its own realm on ctx before returning).

plugins/oauth2provider registers three route-handler 401s in the same group; each is handled differently, and the difference is deliberate:

  • handleUserInfo (GET /v1/oauth/userinfo) sets its own hint the same way authenticateRegistration does. AuthMiddleware only soft-resolves the bearer token (middleware/auth.go) rather than rejecting, so handleUserInfo's own forge.Unauthorized is the actual point of enforcement on this endpoint, and this middleware never sees it. userinfo is the one protected-resource endpoint in the group, which is exactly what RFC 9728 discovery is for, so it gets the hint.
  • handleToken's client-authentication 401s (POST /v1/oauth/token) are left without the hint on purpose: a client at the token endpoint already holds the authorization server's URL, since it just called it, so the hint would tell it nothing new. See the comment on handleToken.
  • handleAuthorize's 401 (GET /v1/oauth/authorize) is left without the hint for a different reason again: it fires because the end user is not signed in, not because a client failed to authenticate, and the response goes to a browser mid-redirect, not to a machine parsing a challenge header. The correct response there is a login redirect; resource_metadata would be read by nobody. See the comment on handleAuthorize.

Known limit: this only sees a 401 that comes back as a Go error at all. plugin.SessionGuard (plugin/authz.go) gates /v1/oauth/device/complete in this plugin, and routes in plugins/subscription and plugins/apikey elsewhere. It is backed by forge's auth registry middleware (forge/extensions/auth registry.go), which on failure calls ctx.String(http.StatusUnauthorized, "Unauthorized") directly and returns that call's result, not a typed error. A route behind SessionGuard gets no hint either, for the same underlying reason a route handler's 401 does not: nothing reaches this middleware as an error for it to inspect.

func SecurityHeaders

func SecurityHeaders(opts SecurityHeadersOptions) forge.Middleware

SecurityHeaders is a forge.Middleware that emits a baseline set of security response headers on every served request.

Headers (set unconditionally unless an option overrides them):

  • Content-Security-Policy
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: deny-list of sensor/payment APIs
  • Strict-Transport-Security (only when HSTSMaxAgeSeconds > 0)

The middleware is conservative by design: callers can widen the CSP (e.g. to allow a CDN) but the default closes XSS and clickjacking surfaces that the dashboard cannot exercise legitimately.

func SecurityHeadersForAPI

func SecurityHeadersForAPI(opts SecurityHeadersOptions) forge.Middleware

SecurityHeadersForAPI is a convenience preset that drops the CSP (irrelevant for JSON responses) but keeps the rest. Use this on routers that serve only JSON.

func SessionActivityMiddleware

func SessionActivityMiddleware(
	toucher SessionToucher,
	configResolver SessionActivityConfigResolver,
	logger log.Logger,
	cookieSetter ...CookieSetter,
) forge.Middleware

SessionActivityMiddleware extends session expiry on each authenticated request, implementing a sliding session window. It updates LastActivityAt and extends ExpiresAt to now + InactivityTimeout.

To avoid a database write on every single request, the session is only touched if LastActivityAt is older than minTouchInterval (60 seconds).

When a CookieSetter is provided, the session cookie is re-set with the extended MaxAge so the browser cookie stays in sync with the server-side session lifetime.

This middleware MUST run after AuthMiddleware so the session is on context.

func SessionFrom

func SessionFrom(ctx context.Context) (*session.Session, bool)

SessionFrom retrieves the session from the context.

func SessionIDFrom

func SessionIDFrom(ctx context.Context) (id.SessionID, bool)

SessionIDFrom retrieves the session ID from the context.

func SessionIsFresh

func SessionIsFresh(ctx context.Context, now time.Time, window time.Duration, lookup SessionLookup) bool

SessionIsFresh reports whether the caller signed in within window: the session on the context, or failing that the one lookup returns for the context's session id, was created no longer than window ago. A missing session is never fresh, so a caller that cannot show when they signed in is asked to sign in again.

func SetInternalErrorLogger

func SetInternalErrorLogger(l log.Logger)

SetInternalErrorLogger installs the logger InternalError reports causes to.

func SetTrustedProxies

func SetTrustedProxies(nets []*net.IPNet)

SetTrustedProxies replaces the trusted-proxy CIDR set. Passing nil restores the built-in defaults. Primarily used for configuration and tests.

func TrustedProxiesAreDefault

func TrustedProxiesAreDefault() bool

TrustedProxiesAreDefault reports whether the built-in private-range set is in use because nobody configured one. A deployment behind a load balancer on a public address, or with untrusted workloads on the same private network, should set AUTHSOME_TRUSTED_PROXIES.

func UserFrom

func UserFrom(ctx context.Context) (*user.User, bool)

UserFrom retrieves the user from the context.

func UserIDFrom

func UserIDFrom(ctx context.Context) (id.UserID, bool)

UserIDFrom retrieves the user ID from the context.

func WithActors

func WithActors(ctx context.Context, c principal.Chain) context.Context

WithActors returns ctx carrying the actor chain acting on the subject's behalf. Delegates to the principal package's own context functions for the same shared-key reason as WithPrincipal.

func WithApp

func WithApp(ctx context.Context, a *app.App) context.Context

WithApp stores the resolved app entity in the context. Set by the publishable-key middleware so handlers can validate against the full app without re-querying the store.

func WithAppID

func WithAppID(ctx context.Context, appID id.AppID) context.Context

WithAppID stores an app ID in the context.

func WithAuthMethod

func WithAuthMethod(ctx context.Context, method string) context.Context

func WithCookieBridgedToken

func WithCookieBridgedToken(ctx context.Context, token string) context.Context

WithCookieBridgedToken records that the Authorization header on this request was synthesized from a session cookie rather than sent by the client.

The engine bridges the cookie into "Authorization: Bearer <token>" so every downstream reader sees one credential shape. That is convenient and it also erased the one fact RFC 9449 section 7.1 turns on, namely whether the credential arrived in an Authorization header at all. Recording the bridged value here puts the fact back without changing what downstream readers see.

func WithCredentialScheme

func WithCredentialScheme(ctx context.Context, scheme string) context.Context

WithAuthMethod stores the authentication method used (e.g. "session", "strategy"). WithCredentialScheme records how the request presented its credential (bearer, dpop or cookie), for the checks that only apply to one of them.

func WithEnvID

func WithEnvID(ctx context.Context, envID id.EnvironmentID) context.Context

WithEnvID stores an environment ID in the context.

func WithEnvironment

func WithEnvironment(ctx context.Context, env *environment.Environment) context.Context

WithEnvironment stores a full environment entity in the context.

func WithEnvironmentSettings

func WithEnvironmentSettings(ctx context.Context, s *environment.Settings) context.Context

WithEnvironmentSettings stores the resolved environment settings in the context.

func WithImpersonator

func WithImpersonator(ctx context.Context, adminID id.UserID) context.Context

WithImpersonator stores the ID of the admin who initiated the impersonation.

func WithOrgID

func WithOrgID(ctx context.Context, orgID id.OrgID) context.Context

WithOrgID stores an organization ID in the context.

func WithPendingOAuthSession

func WithPendingOAuthSession(ctx context.Context, p *PendingOAuthSession) context.Context

WithPendingOAuthSession parks p on ctx. The auth middleware calls it; tests call it to stand in for the middleware.

func WithPrincipal

func WithPrincipal(ctx context.Context, p *principal.Principal) context.Context

WithPrincipal returns ctx carrying the resolved caller.

This delegates to the principal package's own context functions rather than defining a separate key, so a plugin reading through principal.FromContext sees the exact value this middleware wrote, and PrincipalRefFrom's first branch (above) resolves against it.

func WithRawToken

func WithRawToken(ctx context.Context, token string) context.Context

WithRawToken stores the raw bearer token in context so downstream services can forward it to other services (e.g. TwinOS → Portal for user-scoped ops).

func WithRequestInfo

func WithRequestInfo(ctx context.Context, info RequestInfo) context.Context

WithRequestInfo stores the request's correlation data on the context.

func WithSession

func WithSession(ctx context.Context, s *session.Session) context.Context

WithSession stores a session in the context.

func WithSessionID

func WithSessionID(ctx context.Context, sessionID id.SessionID) context.Context

WithSessionID stores a session ID in the context.

func WithUser

func WithUser(ctx context.Context, u *user.User) context.Context

WithUser stores a user in the context.

func WithUserID

func WithUserID(ctx context.Context, userID id.UserID) context.Context

WithUserID stores a user ID in the context.

Types

type AppResolver

type AppResolver interface {
	ResolveAppByPublicKey(ctx context.Context, publicKey string) (*app.App, error)
}

AppResolver resolves a publishable key to its owning app. The engine satisfies this interface via Engine.ResolveAppByPublicKey; defining it locally keeps the middleware package free of an import cycle on the authsome root package.

type AutoRefreshConfig

type AutoRefreshConfig struct {
	// Enabled turns on auto-refresh.
	Enabled bool

	// Threshold is the time before access token expiry to trigger refresh.
	// Default: 5 minutes.
	Threshold time.Duration

	// ExposeRefreshToken controls whether the refresh token is included in
	// the X-Auth-Refresh-Token response header. When false (recommended),
	// only the access token is returned in headers. The refresh token should
	// only be obtained via the /v1/refresh endpoint.
	ExposeRefreshToken bool
}

AutoRefreshConfig controls automatic token refresh behavior.

type AutoRefreshConfigResolver

type AutoRefreshConfigResolver func(ctx context.Context) AutoRefreshConfig

AutoRefreshConfigResolver returns the auto-refresh configuration for the current request context (may vary per app).

type CSRFConfig

type CSRFConfig struct {
	// Enabled turns the check on.
	Enabled bool
	// AllowedOrigins are origins (scheme://host[:port], compared case
	// insensitively) whose cross-site cookie requests are accepted.
	AllowedOrigins []string
}

CSRFConfig governs the cross-site check on cookie sessions.

type CaptchaOptions

type CaptchaOptions struct {
	// Settings is the settings manager used to resolve per-app captcha
	// configuration at request time. Required.
	Settings *settings.Manager

	// ResolveAppID returns the app id for a request. Defaults to a
	// resolver that reads forge.AppIDFrom(ctx). Returning ok=false skips
	// app-scoped settings (global cascade only).
	ResolveAppID func(forge.Context) (id.AppID, bool)

	// VerifierFor builds a captcha.Verifier from a (provider, secret) pair.
	// Defaults to defaultCaptchaVerifierFor which knows about Turnstile.
	VerifierFor func(provider, secret string) (captcha.Verifier, error)

	// Action is the optional captcha action binding (e.g. "signup",
	// "signin"). Empty disables the action check.
	Action string

	// Chronicle, if non-nil, records audit events with Action="captcha.verify".
	Chronicle bridge.Chronicle

	// Logger for transient verifier failures. May be nil.
	Logger log.Logger
}

CaptchaOptions configures CaptchaMiddleware.

type CaptchaResult

type CaptchaResult struct {
	Allowed      bool
	RejectCode   string
	RejectStatus int
	Reason       string
}

CaptchaResult is the outcome of an out-of-middleware captcha check. Allowed reports whether the request may proceed; when false, RejectCode is the same stable type code the middleware would have written ("captcha_required", "captcha_invalid", "captcha_unavailable", "captcha_misconfigured"). RejectStatus is the HTTP status code that matches.

func VerifyCaptchaForRequest

func VerifyCaptchaForRequest(ctx context.Context, opts CaptchaOptions, r *http.Request, appID id.AppID, action string) CaptchaResult

VerifyCaptchaForRequest is a non-forge variant of CaptchaMiddleware for callers that aren't part of forge's standard middleware chain (e.g. dashboard auth-pages handlers, which receive a *router.PageContext rather than a forge.Context). The semantics match the middleware exactly:

  • If auth.captcha_required resolves to false: returns Allowed=true.
  • Token missing: Allowed=false, RejectCode="captcha_required".
  • Token rejected: Allowed=false, RejectCode="captcha_invalid".
  • Verifier transient failure: Allowed=false, RejectCode="captcha_unavailable".
  • Verifier construction failure: Allowed=false, RejectCode="captcha_misconfigured".

The optional appID scopes setting resolution; pass id.AppID{} for global only. Audit recording is the caller's responsibility (the middleware records it internally; out-of-middleware callers may want different fields).

type ChainPermissionChecker

type ChainPermissionChecker interface {
	Can(ctx context.Context, subject principal.Ref, actors principal.Chain, action, resource string) (bool, error)
}

ChainPermissionChecker is the chain-aware counterpart to PermissionChecker: it evaluates a permission for a subject given the actors acting on that subject's behalf.

It sits beside PermissionChecker rather than replacing it, following the same convention as PermissionChecker and UserResolver: this package must not import the engine, so the capability arrives as a narrow interface the engine happens to satisfy. *authsome.Engine implements it via Can, and plugin.Engine carries Can on its core interface, so every real engine reaching RequirePermission is chain-aware without any call site changing.

Why this matters: with an actor chain present, every party must allow the action. A session minted by a token exchange carries the human as its subject and the agent in its chain, and checking the human alone would hand the agent the human's entire authority. Delegation may only narrow, and this interface is how the request path gets to say so.

type ClientSessionRefresher

type ClientSessionRefresher func(ctx context.Context, cookieHeader string) (*authclient.RefreshedSession, error)

ClientSessionRefresher rotates the session behind a browser's Cookie header and reports the new tokens plus the Set-Cookie headers the identity server issued. Client.RefreshTokensWithCookies fulfills it.

func NewClientSessionRefresher

func NewClientSessionRefresher(client *authclient.Client) ClientSessionRefresher

NewClientSessionRefresher adapts an authclient to ClientSessionRefresher.

type CookieNameResolver

type CookieNameResolver func(ctx context.Context) string

CookieNameResolver returns the session cookie name for the current context. When nil, the default "authsome_session_token" is used.

type CookieSetter

type CookieSetter func(ctx forge.Context, token string, maxAge int)

CookieSetter re-sets the session cookie with the given token and max-age. Used by activity and auto-refresh middleware to keep the browser cookie in sync with the server-side session lifetime.

type DefaultEnvironmentResolver

type DefaultEnvironmentResolver func(appID id.AppID) (*environment.Environment, error)

DefaultEnvironmentResolver loads the default environment for an app.

type EnvironmentDetector

type EnvironmentDetector func(rawKey string) (environment.Type, bool)

EnvironmentDetector detects an environment type from a raw API key string. Returns the detected type and true, or empty and false if unknown.

type EnvironmentMiddlewareConfig

type EnvironmentMiddlewareConfig struct {
	// ResolveEnvironment loads an environment by ID.
	ResolveEnvironment EnvironmentResolver

	// ResolveDefault loads the default environment for an app.
	ResolveDefault DefaultEnvironmentResolver

	// DetectFromKey detects environment type from an API key prefix (optional).
	DetectFromKey EnvironmentDetector

	// Logger for debug/warn messages.
	Logger log.Logger
}

EnvironmentMiddlewareConfig configures the environment resolution middleware.

type EnvironmentResolver

type EnvironmentResolver func(envID id.EnvironmentID) (*environment.Environment, error)

EnvironmentResolver loads an environment by ID.

type ExpectedAudienceResolver

type ExpectedAudienceResolver func(ctx context.Context, appID string) []string

ExpectedAudienceResolver returns the resource identifiers this deployment answers to for the app the presented token was minted under. Nil, or an empty result, disables the audience check.

The app id handed to the resolver is the TOKEN's, never the request's. A resource identifier is configured per app, and the request context only carries an app id when the caller sent a publishable key, which is optional. Resolving from the request would let a caller switch the check off by omitting a header, and would leave a resource server that never installs PublishableKeyMiddleware with no app-scoped enforcement at all.

type IPAccessConfig

type IPAccessConfig struct {
	// AllowList contains CIDR ranges that are explicitly allowed.
	// If non-empty, only requests from these ranges are permitted.
	AllowList []string `json:"allow_list"`

	// BlockList contains CIDR ranges that are always blocked.
	// Evaluated before AllowList — a blocked IP is rejected even if
	// it also appears in the allow list.
	BlockList []string `json:"block_list"`
}

IPAccessConfig configures IP-based access control.

type InternalHTTPError

type InternalHTTPError struct {
	RequestID string
	// contains filtered or unexported fields
}

InternalHTTPError is the 500 a client sees for a failure that is ours: a fixed message, never the cause. Forge masks every 5xx body to its status text, so the request id that ties the response to the log line travels on the X-Request-ID response header instead. The cause stays on the error for errors.Is and errors.As, and in the log line written when the error was made, keyed by the same request id.

func (*InternalHTTPError) Error

func (e *InternalHTTPError) Error() string

func (*InternalHTTPError) ResponseBody

func (e *InternalHTTPError) ResponseBody() any

ResponseBody is what forge serialises (and then masks, as for any 5xx).

func (*InternalHTTPError) StatusCode

func (e *InternalHTTPError) StatusCode() int

func (*InternalHTTPError) Unwrap

func (e *InternalHTTPError) Unwrap() error

type JWTValidator

type JWTValidator interface {
	ValidateJWT(token string) (*tokenformat.TokenClaims, error)
}

JWTValidator validates JWT access tokens and returns claims. The engine implements this via its TokenFormatForApp method.

type PendingOAuthSession

type PendingOAuthSession struct {
	Session *session.Session
	// contains filtered or unexported fields
}

PendingOAuthSession is an OAuth2-issued session the auth middleware resolved but has not admitted to the request.

func PendingOAuthSessionFrom

func PendingOAuthSessionFrom(ctx context.Context) (*PendingOAuthSession, bool)

PendingOAuthSessionFrom returns the parked OAuth2 session, if any.

type PermissionChecker

type PermissionChecker interface {
	HasPermission(ctx context.Context, userID id.UserID, action, resource string) (bool, error)
}

PermissionChecker can verify if a user has a specific permission.

type PrincipalResolver

type PrincipalResolver func(ctx context.Context, ref principal.Ref) (*principal.Principal, error)

PrincipalResolver resolves a caller by ref. Middleware takes this as a function rather than an engine so it does not import the engine package.

type RateLimitConfig

type RateLimitConfig struct {
	// Limit is the maximum number of requests per window.
	Limit int

	// Window is the sliding window duration.
	Window time.Duration

	// KeyFunc extracts the rate limit key from the request (default: client IP).
	KeyFunc func(ctx forge.Context) string

	// FailOpen lets the request through when the limiter returns an error.
	// Off by default: a limiter that cannot answer refuses with 503, so an
	// outage of the shared store does not become an unlimited window.
	FailOpen bool

	// OnError is called with the limiter's error, for a metric or a log.
	OnError func(err error)

	// OnReject is called when a request is refused for exceeding the limit,
	// and OnFailOpen when a limiter error let one through. Both are for
	// metrics.
	OnReject   func()
	OnFailOpen func()
}

RateLimitConfig configures the rate limit middleware for a specific endpoint.

type RefreshRequest

type RefreshRequest struct {
	// RefreshToken is the session's current refresh token when the caller
	// holds it. A session loaded from the store carries none: the store keeps
	// refresh tokens as hashes, so the middleware sends SessionToken instead
	// and the refresher rotates the session behind that access token.
	RefreshToken string
	// SessionToken is the access token the request authenticated with.
	SessionToken string

	// IPAddress and UserAgent are validated against the session's stored
	// values when session binding is enabled.
	IPAddress string
	UserAgent string

	// DPoPProof is the raw DPoP header from this request. It is the same proof
	// the auth middleware already accepted, and re-checking it is safe: the
	// request-scoped record of accepted proofs keeps the replay cache from
	// reading it as a replay of itself.
	DPoPProof string
	// Method and RequestURL are this request's, for the proof's htm and htu.
	Method     string
	RequestURL string

	// KeepRefreshToken asks for the access token alone to be rotated. Set
	// when the refresh token is not exposed in the response: the client
	// keeps the one it has, and it stays valid.
	KeepRefreshToken bool
}

RefreshRequest carries everything the engine needs to re-check a session's bindings while rotating it. Auto-refresh happens on an ordinary request rather than a call to /v1/refresh, so the request's own details are what the checks run against: the DPoP proof is bound to this method and URL, and the IP and User-Agent are this request's.

type RequestInfo

type RequestInfo = hook.RequestInfo

RequestInfo re-exports hook.RequestInfo so handlers and plugins can read the request correlation data without importing the hook package.

func RequestInfoFrom

func RequestInfoFrom(ctx context.Context) (RequestInfo, bool)

RequestInfoFrom returns the request's correlation data.

type RoleChecker

type RoleChecker interface {
	ListUserRoleSlugs(ctx context.Context, userID id.UserID) ([]string, error)
}

RoleChecker can list a user's role slugs.

type SecurityHeadersOptions

type SecurityHeadersOptions struct {
	// HSTSMaxAgeSeconds is the max-age value emitted in the
	// Strict-Transport-Security header. Zero (the default) means
	// "do not emit HSTS" — appropriate for local development. Set
	// to 31536000 (1 year) for production deployments behind TLS.
	HSTSMaxAgeSeconds int

	// HSTSIncludeSubdomains adds includeSubDomains to the HSTS
	// directive. Only meaningful when HSTSMaxAgeSeconds > 0.
	HSTSIncludeSubdomains bool

	// HSTSPreload adds the preload directive (the operator must
	// also submit the domain to hstspreload.org). Only meaningful
	// when HSTSMaxAgeSeconds > 0 and HSTSIncludeSubdomains is true.
	HSTSPreload bool

	// CSP overrides the default Content-Security-Policy. When
	// empty, a conservative default is used: own-origin scripts
	// and styles, no framing, no inline scripts, no plugins.
	// Pass an empty value via DisableCSP to opt out entirely
	// (e.g. for JSON-only APIs where CSP has no effect).
	CSP string

	// DisableCSP omits the Content-Security-Policy header.
	// Useful for pure-JSON API routes where CSP is irrelevant
	// and a misconfigured policy can confuse responders.
	DisableCSP bool

	// FrameOptions is the value of X-Frame-Options. Defaults to
	// "DENY". Set to "SAMEORIGIN" if the dashboard ever needs to
	// embed itself; never set to ALLOW-FROM (deprecated).
	FrameOptions string

	// ReferrerPolicy is the value of Referrer-Policy. Defaults to
	// "strict-origin-when-cross-origin" — the OWASP-recommended
	// default that preserves analytics on same-origin while
	// stripping query strings on cross-origin navigations.
	ReferrerPolicy string

	// PermissionsPolicy is the value of Permissions-Policy.
	// Defaults to a conservative deny-list that disables sensors,
	// payment, and serial APIs the dashboard never uses.
	PermissionsPolicy string
}

SecurityHeadersOptions configures SecurityHeaders.

Defaults are tuned for the dashboard's HTML routes. JSON-only API surfaces should pass APIRoutes:true to drop the HTML-shaped CSP.

type SessionActivityConfig

type SessionActivityConfig struct {
	// Enabled turns on session extension on activity.
	Enabled bool

	// InactivityTimeout is how long a session lives without activity.
	// On each authenticated request, ExpiresAt is reset to now + InactivityTimeout.
	// Default: 30 minutes.
	InactivityTimeout time.Duration

	// AbsoluteLifetime caps the sliding window: ExpiresAt is never moved
	// past CreatedAt + AbsoluteLifetime. Zero means no cap.
	AbsoluteLifetime time.Duration
}

SessionActivityConfig controls sliding session extension behavior.

type SessionActivityConfigResolver

type SessionActivityConfigResolver func(ctx context.Context) SessionActivityConfig

SessionActivityConfigResolver returns the activity extension configuration for the current request context (may vary per app).

type SessionBindingConfig

type SessionBindingConfig struct {
	// BindToIP rejects requests when the client IP differs from the
	// IP recorded at session creation.
	BindToIP bool

	// BindToDevice rejects requests when the User-Agent differs from
	// the one recorded at session creation.
	BindToDevice bool

	// CookieNameResolver returns the session cookie name for the current
	// request context. Falls back to "authsome_session_token" if nil or empty.
	CookieNameResolver CookieNameResolver

	// JWTSessionChecker, when set, cross-checks JWT tokens against the session
	// store. This enables JWT revocation — revoked sessions are rejected even
	// if the JWT signature is valid. Also enables IP/device binding for JWTs.
	JWTSessionChecker SessionExistsChecker

	// ExpectedAudienceResolver returns the resource identifiers this
	// deployment answers to for the app that minted the presented token.
	// Nil, or an empty result, disables the check.
	//
	// Per app rather than per process: two apps in one deployment are two
	// different resources, and a token minted for one must not authenticate at
	// the other. This runs per request on the same path as
	// CookieNameResolver, so it should read a cached setting rather than hit
	// the database.
	ExpectedAudienceResolver ExpectedAudienceResolver
	// PrincipalResolver, when set, resolves the session's subject onto the
	// context as a *principal.Principal after every auth path that produces a
	// session. Threaded through this config (rather than as a standalone
	// parameter on each middleware constructor) so it defaults to nil and
	// every existing caller keeps working unchanged.
	PrincipalResolver PrincipalResolver

	// DPoPValidator enforces RFC 9449 proof-of-possession. When nil, DPoP is
	// not enforced at all, which is only correct in tests: the engine always
	// supplies one.
	DPoPValidator *dpop.Validator

	// DPoPNonceSigner mints the nonces served with a use_dpop_nonce challenge.
	// Nil disables nonce challenges.
	DPoPNonceSigner *dpop.NonceSigner

	// DPoPNonceRequired reports whether the app owning this session demands a
	// nonce. Nil means never.
	DPoPNonceRequired func(ctx context.Context, appID string) bool

	// DPoPAudit records a DPoP failure. Nil disables recording.
	DPoPAudit func(ctx context.Context, action string, md map[string]string)

	// JWTSessionContextChecker is JWTSessionChecker taking the request's
	// context. When both are set this one is used.
	JWTSessionContextChecker SessionExistsContextChecker
}

SessionBindingConfig controls session binding validation.

type SessionExistsChecker

type SessionExistsChecker func(appID, sessionID string) (*session.Session, error)

SessionExistsChecker checks whether a session ID still exists in the store. Used for JWT revocation support — when enabled, JWT tokens are cross-checked against the session store so revoked sessions are rejected immediately. appID is the token's own app, so the check honours that app's setting rather than the global one.

type SessionExistsContextChecker

type SessionExistsContextChecker func(ctx context.Context, appID, sessionID string) (*session.Session, error)

SessionExistsContextChecker is SessionExistsChecker with the request's context, so the store lookup carries the request's values (its trace span among them).

type SessionLookup

type SessionLookup func(ctx context.Context, sessionID id.SessionID) (*session.Session, error)

SessionLookup loads a session by id when the request carries only the id (a JWT, for instance) and not the row itself.

type SessionRefresher

type SessionRefresher func(ctx context.Context, req RefreshRequest) (*session.Session, error)

SessionRefresher refreshes a session and returns the updated session with new tokens. The extension adapts the engine's Refresh and RefreshBySessionToken to it, picking by which credential the request holds.

type SessionResolver

type SessionResolver func(ctx context.Context, token string) (*session.Session, error)

SessionResolver loads a session from a token.

type SessionToucher

type SessionToucher func(ctx context.Context, sessionID id.SessionID, lastActivityAt, expiresAt time.Time) error

SessionToucher updates a session's last activity time and expiry.

type StrategyAuthenticator

type StrategyAuthenticator interface {
	Authenticate(ctx context.Context, r *http.Request) (*strategy.Result, error)
}

StrategyAuthenticator authenticates requests using registered strategies. The strategy.Registry implements this interface.

type UserResolver

type UserResolver func(ctx context.Context, userID string) (*user.User, error)

UserResolver loads a user by ID string.

Jump to

Keyboard shortcuts

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