Documentation
¶
Overview ¶
Package shared provides middleware and error helpers used by both the public and admin HTTP servers.
Index ¶
- Variables
- func CORSMiddleware(cfg CORSConfig) func(http.Handler) http.Handler
- func ClaimsFromContext(ctx context.Context) (*crypto.AccessTokenClaims, bool)
- func ClientIP(r *http.Request) string
- func ExtractAuthToken(r *http.Request) (token, scheme string)
- func ExtractBearerToken(r *http.Request) string
- func ParseSameSite(s string) http.SameSite
- func RenderTemplate(ctx context.Context, w http.ResponseWriter, status int, ...)
- func RequestURL(r *http.Request) string
- func SafeRedirect(path, fallback string) string
- func SecurityHeaders(secure bool) func(http.Handler) http.Handler
- func UserIDFromContext(ctx context.Context) (string, bool)
- func WriteErrorPage(w http.ResponseWriter, r *http.Request, status int, title, message string)
- func WriteJSON(w http.ResponseWriter, status int, v any)
- func WriteOAuthError(w http.ResponseWriter, status int, errCode, description string)
- func WriteOAuthErrorWithConsent(w http.ResponseWriter, status int, errCode, description, consentURL string)
- func WriteOAuthErrorWithConsentAndCause(w http.ResponseWriter, status int, ...)
- type CORSConfig
- type DPoPJWTConfig
- type DPoPProofStore
- type JWKSProvider
- type JWTMiddleware
- type LockoutCallback
- type OAuthErrorResponse
- type RateLimiter
- type SessionMiddleware
- func (m *SessionMiddleware) CSRFToken(cookieValue string) string
- func (m *SessionMiddleware) ClearSessionCookie(w http.ResponseWriter)
- func (m *SessionMiddleware) DeriveKey(purpose string) []byte
- func (m *SessionMiddleware) SameSite() http.SameSite
- func (m *SessionMiddleware) Secure() bool
- func (m *SessionMiddleware) SetFailClosed(failClosed bool)
- func (m *SessionMiddleware) SetLogger(l *slog.Logger)
- func (m *SessionMiddleware) SetSessionCookie(w http.ResponseWriter, userID string)
- func (m *SessionMiddleware) SetUserStore(u output.UserStore)
- func (m *SessionMiddleware) ValidateCSRF(cookieValue, token string) bool
- func (m *SessionMiddleware) Wrap(next http.Handler) http.Handler
Constants ¶
This section is empty.
Variables ¶
var ErrorPageTmpl = template.Must(template.New("error").Parse(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} — Authplane</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
background:#f8fafc;display:flex;justify-content:center;align-items:center;
min-height:100vh;padding:24px;color:#0f172a}
.wrapper{width:100%;max-width:420px}
.logo{text-align:center;margin-bottom:32px}
.logo svg{width:40px;height:40px}
.logo-text{font-size:0.85em;font-weight:600;color:#64748b;letter-spacing:0.05em;
text-transform:uppercase;margin-top:8px}
.card{background:#fff;border-radius:16px;
box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(0,0,0,0.06);
padding:40px 36px;border:1px solid #e2e8f0;text-align:center}
.icon{margin-bottom:20px}
.icon svg{width:48px;height:48px}
h1{font-size:1.35em;font-weight:700;margin-bottom:12px;color:#0f172a;letter-spacing:-0.01em}
p{color:#475569;font-size:0.92em;line-height:1.6}
.footer{text-align:center;margin-top:24px;font-size:0.8em;color:#94a3b8}
</style>
</head>
<body>
<div class="wrapper">
<div class="logo">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#4f46e5"/>
<path d="M12 20.5L17.5 26L28 15" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div class="logo-text">Authplane</div>
</div>
<div class="card">
<div class="icon">
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="20" fill="#fef2f2" stroke="#fecaca" stroke-width="1.5"/>
<path d="M24 16v10" stroke="#dc2626" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="24" cy="32" r="1.5" fill="#dc2626"/>
</svg>
</div>
<h1>{{.Title}}</h1>
<p>{{.Message}}</p>
</div>
<div class="footer">Secured by Authplane</div>
</div>
</body>
</html>`))
ErrorPageTmpl is the shared error page template.
Functions ¶
func CORSMiddleware ¶
func CORSMiddleware(cfg CORSConfig) func(http.Handler) http.Handler
CORSMiddleware returns middleware that handles CORS preflight and response headers. Only endpoints that need cross-origin access (token, DCR, discovery, revoke) get CORS headers. Login/consent are same-origin only.
func ClaimsFromContext ¶
func ClaimsFromContext(ctx context.Context) (*crypto.AccessTokenClaims, bool)
ClaimsFromContext returns the validated JWT access token claims from the request context.
func ClientIP ¶
ClientIP extracts the client IP from the request. Uses RemoteAddr only -- X-Forwarded-For is ignored to prevent spoofing.
func ExtractAuthToken ¶
ExtractAuthToken extracts the access token from the Authorization header. Supports both "Bearer <token>" and "DPoP <token>" schemes (RFC 9449 §7.1). Returns the raw token and the scheme name ("Bearer" or "DPoP"). Returns empty strings if the header is missing or uses an unsupported scheme.
func ExtractBearerToken ¶
ExtractBearerToken returns the token value from an "Authorization: Bearer <token>" header. Returns empty string if the header is missing or malformed.
func ParseSameSite ¶
ParseSameSite converts a string to http.SameSite.
func RenderTemplate ¶
func RenderTemplate(ctx context.Context, w http.ResponseWriter, status int, tmpl *template.Template, data any)
RenderTemplate executes a template into a buffer and writes the result to w. If the template fails, it writes a plain 500 error instead of partial HTML.
func RequestURL ¶
RequestURL reconstructs the effective request URL (scheme + host + path) for DPoP htu validation (RFC 9449 §4.3). Respects X-Forwarded-Proto for reverse proxy deployments. Query string and fragment are stripped.
func SafeRedirect ¶
SafeRedirect validates that a redirect target is a safe relative path. Returns the path if valid, or fallback if the path is empty/unsafe. Rejects absolute URLs, protocol-relative URLs, and paths with authority.
func SecurityHeaders ¶
SecurityHeaders returns middleware that sets standard HTTP security headers.
func UserIDFromContext ¶
UserIDFromContext returns the authenticated user ID from the request context.
func WriteErrorPage ¶
WriteErrorPage renders a minimal HTML error page for non-redirectable errors (e.g., invalid client_id or redirect_uri where we must NOT redirect).
func WriteJSON ¶
func WriteJSON(w http.ResponseWriter, status int, v any)
WriteJSON writes a JSON response.
func WriteOAuthError ¶
func WriteOAuthError(w http.ResponseWriter, status int, errCode, description string)
WriteOAuthError writes an RFC 6749 / RFC 9457 hybrid error response. Includes both OAuth error fields and RFC 9457 Problem Details fields.
func WriteOAuthErrorWithConsent ¶
func WriteOAuthErrorWithConsent(w http.ResponseWriter, status int, errCode, description, consentURL string)
WriteOAuthErrorWithConsent writes an OAuth error response that includes a `consent_url` field pointing to the AS consent page for a vault-backed upstream service. Used only by the token endpoint when it detects a *domain.ConsentRequiredError.
: this is now a thin wrapper around WriteOAuthErrorWithConsentAndCause that omits the `cause` field on the wire (caller did not supply one).
func WriteOAuthErrorWithConsentAndCause ¶
func WriteOAuthErrorWithConsentAndCause(w http.ResponseWriter, status int, errCode, description, consentURL, cause string)
WriteOAuthErrorWithConsentAndCause writes an OAuth error response that includes a `consent_url` field and a `cause` sub-discriminator. The cause value is one of domain.CauseConsentMissing / domain.CauseScopeInsufficient; empty omits the field on the wire.
Types ¶
type CORSConfig ¶
type CORSConfig struct {
AllowedOrigins []string // Exact origins to allow. Empty = no CORS headers.
}
CORSConfig controls Cross-Origin Resource Sharing.
type DPoPJWTConfig ¶
DPoPJWTConfig holds optional DPoP configuration for the JWT middleware. When set, the middleware enforces DPoP proof-of-possession for DPoP-bound tokens.
type DPoPProofStore ¶
type DPoPProofStore interface {
ConsumeJTI(ctx context.Context, jti string, expiry time.Time) error
}
DPoPProofStore records consumed DPoP proof JTIs for replay detection at the resource server. Mirrors output.DPoPNonceStore.ConsumeJTI so the existing SQLite/Postgres adapters can be passed in by the composition root.
type JWKSProvider ¶
type JWKSProvider interface {
BuildJWKS(ctx context.Context) (*jose.JSONWebKeySet, error)
}
JWKSProvider provides the JWKS key set for JWT verification.
type JWTMiddleware ¶
type JWTMiddleware struct {
// contains filtered or unexported fields
}
JWTMiddleware validates Bearer (and optionally DPoP) JWT tokens and injects claims into context.
func NewJWTMiddleware ¶
func NewJWTMiddleware(jwks JWKSProvider, issuer string, obs *observability.Provider) *JWTMiddleware
NewJWTMiddleware creates a JWT validation middleware suitable for authorization-server-internal endpoints (introspection, userinfo, etc.) where audience is not a meaningful gate. Resource servers MUST use NewResourceJWTMiddleware instead, which makes audience and DPoP proof storage non-optional and prevents the "I forgot WithAudience" footgun that the 2026-05-18 audit flagged.
func NewResourceJWTMiddleware ¶
func NewResourceJWTMiddleware( jwks JWKSProvider, issuer, audience string, proofStore DPoPProofStore, dpopCfg DPoPJWTConfig, obs *observability.Provider, ) *JWTMiddleware
NewResourceJWTMiddleware creates a JWT validation middleware configured for a specific resource server. audience MUST be the resource URI (RFC 8707) and proofStore MUST be a replay store for DPoP proof JTIs — both are required because forgetting either re-opens the audience-confusion and DPoP-replay classes of bug, respectively. dpopCfg controls DPoP proof freshness; pass the zero value to disable DPoP enforcement for a Bearer-only resource.
Panics if audience or proofStore is empty/nil — the failure mode of "I shipped a resource server with audience=\"\"" is precisely what this constructor exists to make impossible.
func (*JWTMiddleware) WithAudience ¶
func (m *JWTMiddleware) WithAudience(aud string) *JWTMiddleware
WithAudience configures the expected audience (resource URI) for tokens accepted by this middleware. Once set, tokens whose `aud` claim does not contain `aud` are rejected with 401. Required for every resource-server deployment of this middleware (RFC 9068 §4, RFC 8707).
func (*JWTMiddleware) WithDPoP ¶
func (m *JWTMiddleware) WithDPoP(cfg DPoPJWTConfig) *JWTMiddleware
WithDPoP enables DPoP proof-of-possession validation on the JWT middleware. When enabled, DPoP-bound tokens (containing cnf.jkt) require a valid DPoP proof.
func (*JWTMiddleware) WithDPoPProofStore ¶
func (m *JWTMiddleware) WithDPoPProofStore(s DPoPProofStore) *JWTMiddleware
WithDPoPProofStore enables DPoP proof-JTI replay detection at the resource server. Without a store the middleware verifies the proof's structure and binding but does not consume the JTI, leaving a replay window equal to ProofLifetime. Required for any resource exposed to network attackers.
type LockoutCallback ¶
type LockoutCallback func(ip string)
LockoutCallback is called when a request is blocked by lockout.
type OAuthErrorResponse ¶
type OAuthErrorResponse struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
Type string `json:"type"`
Title string `json:"title"`
Detail string `json:"detail"`
Status int `json:"status"`
// ConsentURL is a URL to a consent page where the user can authorize the
// AS to access an upstream provider. Populated only by
// WriteOAuthErrorWithConsent / WriteOAuthErrorWithConsentAndCause; omitted
// from all other error responses.
ConsentURL string `json:"consent_url,omitempty"`
// Cause is a sub-discriminator for consent_required errors.
// Values currently in use: "consent_missing" (user never authorized this
// agent for this resource), "scope_insufficient" (user authorized a
// strict subset of the requested scopes). Empty for non-consent errors
// and for legacy consent errors that predate the field. Wire format is
// `cause` (lowercase, omitempty).
Cause string `json:"cause,omitempty"`
}
OAuthErrorResponse is the combined OAuth + RFC 9457 error response.
type RateLimiter ¶
type RateLimiter struct {
OnLockoutBlock LockoutCallback
// contains filtered or unexported fields
}
RateLimiter provides per-IP rate limiting and auth failure lockout.
func NewRateLimiter ¶
func NewRateLimiter(ctx context.Context, cfg config.RateLimitConfig) *RateLimiter
NewRateLimiter creates a new rate limiter from the given config. The provided context controls the lifetime of the background cleanup goroutine.
func (*RateLimiter) IsLockedOut ¶
func (rl *RateLimiter) IsLockedOut(key string) bool
IsLockedOut checks if a key is currently locked out due to too many failures.
func (*RateLimiter) Middleware ¶
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler
Middleware wraps an HTTP handler with rate limiting.
func (*RateLimiter) RecordAuthFailure ¶
func (rl *RateLimiter) RecordAuthFailure(key string)
RecordAuthFailure tracks a failed authentication attempt for the given key.
type SessionMiddleware ¶
type SessionMiddleware struct {
CookieName string
MaxAge time.Duration
// contains filtered or unexported fields
}
SessionMiddleware manages HMAC-signed stateless session cookies. Cookie format: userID|expiryUnix|base64(hmac-sha256(userID|expiryUnix))
func NewSessionMiddleware ¶
func NewSessionMiddleware(secret []byte, cookieName string, maxAge time.Duration, secure bool, sameSite http.SameSite) *SessionMiddleware
NewSessionMiddleware creates a new session middleware.
func (*SessionMiddleware) CSRFToken ¶
func (m *SessionMiddleware) CSRFToken(cookieValue string) string
CSRFToken generates a CSRF token from the session cookie value. Token = base64(hmac-sha256(cookieValue, secret+"csrf")).
func (*SessionMiddleware) ClearSessionCookie ¶
func (m *SessionMiddleware) ClearSessionCookie(w http.ResponseWriter)
ClearSessionCookie expires the session cookie.
func (*SessionMiddleware) DeriveKey ¶
func (m *SessionMiddleware) DeriveKey(purpose string) []byte
DeriveKey derives a purpose-specific key from the session secret via HMAC. Used for OIDC state signing so the raw session secret is not shared.
func (*SessionMiddleware) SameSite ¶
func (m *SessionMiddleware) SameSite() http.SameSite
SameSite returns the SameSite policy for cookies.
func (*SessionMiddleware) Secure ¶
func (m *SessionMiddleware) Secure() bool
Secure returns whether cookies should have the Secure flag.
func (*SessionMiddleware) SetFailClosed ¶
func (m *SessionMiddleware) SetFailClosed(failClosed bool)
SetFailClosed selects the policy for transient user-store lookup errors in Wrap. When true, any non-ErrUserNotFound error from UserStore.GetByID clears the cookie and continues anonymously. When false (default), the cookie is kept on transient errors so a brief DB outage does not log every user out.
func (*SessionMiddleware) SetLogger ¶
func (m *SessionMiddleware) SetLogger(l *slog.Logger)
SetLogger installs a logger used to record stale-session and transient-error events. If unset, those events are silent.
func (*SessionMiddleware) SetSessionCookie ¶
func (m *SessionMiddleware) SetSessionCookie(w http.ResponseWriter, userID string)
SetSessionCookie creates and sets an HMAC-signed session cookie.
func (*SessionMiddleware) SetUserStore ¶
func (m *SessionMiddleware) SetUserStore(u output.UserStore)
SetUserStore installs the UserStore consulted by Wrap to reject session cookies whose userID no longer exists in the database. The store is expected to be the same one used elsewhere in the app — typically the cache-fronted version produced by storage.WithUserCache so this lookup does not become a DB query per request.
When nil (the default), Wrap preserves the legacy behavior of accepting any cookie that passes HMAC + expiry. Tests may rely on the nil default.
func (*SessionMiddleware) ValidateCSRF ¶
func (m *SessionMiddleware) ValidateCSRF(cookieValue, token string) bool
ValidateCSRF checks that the CSRF token matches the cookie value.
func (*SessionMiddleware) Wrap ¶
func (m *SessionMiddleware) Wrap(next http.Handler) http.Handler
Wrap returns middleware that extracts the user ID from the session cookie.
When a UserStore is installed, the middleware additionally verifies the userID still resolves in the database. A cookie that points at a deleted user is cleared and the request continues anonymously (downstream handlers see no userID and redirect to /login as if no cookie were present).
On a transient store error, behavior depends on SetFailClosed:
- fail-open (default): the userID is kept on the context and the cookie is preserved, so a DB blip does not log every user out
- fail-closed: the cookie is cleared and the request continues anonymously, so a disabled / deleted user cannot ride out a DB outage with a still-valid cookie. Recommended for admin-adjacent and high-assurance deployments