shared

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: AGPL-3.0 Imports: 23 Imported by: 0

Documentation

Overview

Package shared provides middleware and error helpers used by both the public and admin HTTP servers.

Index

Constants

This section is empty.

Variables

View Source
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

func ClientIP(r *http.Request) string

ClientIP extracts the client IP from the request. Uses RemoteAddr only -- X-Forwarded-For is ignored to prevent spoofing.

func ExtractAuthToken

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

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

func ExtractBearerToken(r *http.Request) string

ExtractBearerToken returns the token value from an "Authorization: Bearer <token>" header. Returns empty string if the header is missing or malformed.

func ParseSameSite

func ParseSameSite(s string) http.SameSite

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

func RequestURL(r *http.Request) string

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

func SafeRedirect(path, fallback string) string

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

func SecurityHeaders(secure bool) func(http.Handler) http.Handler

SecurityHeaders returns middleware that sets standard HTTP security headers.

func UserIDFromContext

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

UserIDFromContext returns the authenticated user ID from the request context.

func WriteErrorPage

func WriteErrorPage(w http.ResponseWriter, r *http.Request, status int, title, message string)

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

type DPoPJWTConfig struct {
	ProofLifetime time.Duration // max |now - iat| for proof freshness
}

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.

func (*JWTMiddleware) Wrap

func (m *JWTMiddleware) Wrap(next http.Handler) http.Handler

Wrap returns an http.Handler that validates the Bearer or DPoP JWT. On success, injects *crypto.AccessTokenClaims into context. On failure, returns 401 with WWW-Authenticate header.

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

Jump to

Keyboard shortcuts

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