shared

package
v2.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 21, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package shared provides cross-cutting concerns used by all StormEngine plugins.

These are pure functions and middleware that operate at the HTTP layer without knowing anything about OIDC protocol semantics.

Index

Constants

View Source
const (
	ApplicationTypeWeb       = 0 // web
	ApplicationTypeUserAgent = 1 // user_agent
	ApplicationTypeNative    = 2 // native
)

Application type constants (matching common OIDC implementations).

Variables

This section is empty.

Functions

func CertThumbprint

func CertThumbprint(cert *x509.Certificate) string

CertThumbprint computes the SHA-256 thumbprint of a certificate as a base64url-encoded string (RFC 8705 §3.1, x5t#S256).

func ClientCertFromContext

func ClientCertFromContext(ctx context.Context) *x509.Certificate

ClientCertFromContext retrieves the TLS client certificate from the context. Returns nil if no certificate was presented.

func ContextWithAuthenticatedClient

func ContextWithAuthenticatedClient(ctx context.Context, client AuthenticatedClient) context.Context

ContextWithAuthenticatedClient stores a pre-authenticated client in the context.

func ContextWithClientCert

func ContextWithClientCert(ctx context.Context, cert *x509.Certificate) context.Context

ContextWithClientCert stores the TLS client certificate in the request context.

func ContextWithDPoP

func ContextWithDPoP(ctx context.Context, proof DPoPProof) context.Context

ContextWithDPoP stores a DPoP proof in the request context.

func ContextWithIssuer

func ContextWithIssuer(ctx context.Context, issuer string) context.Context

func ContextWithJARMPreferredAlg

func ContextWithJARMPreferredAlg(ctx context.Context, alg string) context.Context

ContextWithJARMPreferredAlg stores the client's preferred signing algorithm in the context so the JARM plugin can use it for signing.

func CopyRequestObjectToAuthRequest

func CopyRequestObjectToAuthRequest(authReq *protocol.AuthRequest, requestObject *protocol.RequestObject)

CopyRequestObjectToAuthRequest copies request object claims into the authorization request, overriding any existing values.

FAPI 2.0 §5.3.1: when a request object is present, the authorization server SHALL NOT use any parameter values from the query string and SHALL only use parameter values from the request object. Most fields are therefore unconditionally assigned from the request object — any query-string-only values (e.g. state) that are absent from the request object are cleared. RequestURI is preserved when the request object does not carry one, because it is needed downstream as a sentinel (e.g. to skip the signed-request-object-required check for request_uri-based JAR).

Per OIDC Core §6.1 and RFC 9101, the request object is a transport envelope for authorization request parameters. Once the signature is verified and the claims are copied, the original RequestParam (the raw JWT string) has no further use — no downstream flow reads it. Clearing it avoids accidentally re-processing the same JWT and keeps the AuthRequest semantically clean.

func EndpointURL

func EndpointURL(ctx context.Context, ep *protocol.Endpoint) string

EndpointURL converts a protocol.Endpoint into an absolute URL using the issuer from context, applying the same TrimRight logic as IssuerURL.

This allows plugins to declare their paths as *protocol.Endpoint and reuse the built-in Absolute() logic instead of hand-joining strings.

Example:

ep := protocol.NewEndpoint("/authorize")
EndpointURL(ctx, ep) → "http://localhost:9998/authorize"

func ExtractBearerToken

func ExtractBearerToken(r *http.Request) string

ExtractBearerToken extracts a Bearer token from the Authorization header. Returns the token string, or empty string if not present or not a Bearer token.

func FetchJWKSFromURI

func FetchJWKSFromURI(uri string, skipTLSVerify, allowPrivateIPs bool) ([]jwk.Key, error)

FetchJWKSFromURI fetches and parses a JWKS from a remote URI. If allowPrivateIPs is false, the URI is validated to prevent SSRF attacks.

func HTTPLoopbackOrLocalhost

func HTTPLoopbackOrLocalhost(rawURL string) (*url.URL, bool)

HTTPLoopbackOrLocalhost parses a URL and returns true if it uses HTTP/HTTPS and points to a loopback address.

func IsLocalhost

func IsLocalhost(host string) bool

IsLocalhost returns true if the hostname is a loopback address. Per RFC 8252 §7.3, only 127.0.0.1 and ::1 are loopback addresses.

func IssuerFromContext

func IssuerFromContext(ctx context.Context) string

func IssuerMiddleware

func IssuerMiddleware(fn IssuerFromRequest) func(http.Handler) http.Handler

IssuerMiddleware injects the issuer into the request context. This is the only middleware that Engine itself needs to install.

func JARMPreferredAlgFromContext

func JARMPreferredAlgFromContext(ctx context.Context) string

JARMPreferredAlgFromContext retrieves the client's preferred signing algorithm.

func JSONResponse

func JSONResponse(w http.ResponseWriter, data any, statusCode int)

JSONResponse writes a JSON response with the given status code. If data is nil, only the status code is written. Delegates to util/http.MarshalJSONWithStatus for the actual encoding. Sets Cache-Control and Pragma headers per RFC 6749 §5.1.

func NewHTTPClient

func NewHTTPClient(skipTLSVerify bool) *http.Client

NewHTTPClient creates an HTTP client for outbound requests (JWKS fetches, request_uri, backchannel_logout, etc.).

Options (all disabled by default):

  • skipTLSVerify: skips TLS certificate verification. Use ONLY for testing with self-signed certificates. NEVER enable in production.

func NoContentResponse

func NoContentResponse(w http.ResponseWriter)

NoContentResponse sends a 204 No Content response.

func ParseAndValidateRequestObject

func ParseAndValidateRequestObject(ctx context.Context, authReq *protocol.AuthRequest, lookupClient RequestObjectClientLookup, skipTLSVerify, allowPrivateIPs bool) error

ParseAndValidateRequestObject parses and validates a JWT request object (OIDC Core §6.1, RFC 9101). It:

  1. Parses the JWT without verification to extract claims
  2. Validates iss == client_id and aud contains the issuer
  3. Looks up the client and verifies the JWT signature
  4. Validates time claims (exp, nbf)
  5. Copies request object claims into the auth request

skipTLSVerify controls whether TLS certificate verification is skipped when fetching the client's JWKS from jwks_uri (testing only). allowPrivateIPs controls whether SSRF validation is skipped for private IPs.

Returns protocol.ErrInvalidRequestObject on any validation failure.

func ParseClientCredentials

func ParseClientCredentials(r *http.Request) (clientID, clientSecret string, err error)

ParseClientCredentials extracts client credentials from the request without verifying them against the store. Returns protocol.ErrInvalidRequest if no credentials are found.

func RedirectResponse

func RedirectResponse(w http.ResponseWriter, r *http.Request, url string)

RedirectResponse sends a 302 Found redirect.

func ResolveCNF

func ResolveCNF(ctx context.Context) map[string]any

ResolveCNF builds the cnf (confirmation) claim from mTLS and DPoP context. mTLS: cnf.x5t#S256 (RFC 8705 §3.1) DPoP: cnf.jkt (RFC 9449 §7.1) If both are present, both keys are included. Returns nil if neither mTLS nor DPoP context is present.

func ResolvePreferredSigningAlg

func ResolvePreferredSigningAlg(client interface{}) string

ResolvePreferredSigningAlg returns the client's preferred signing algorithm for JARM authorization responses. It checks:

  1. The client's id_token_signed_response_alg (explicit configuration)
  2. FAPI 2.0 profile fallback: PS256 (FAPI 2.0 §5.3.2.2)

Returns empty string if the client has no preference (server default will be used).

func SetUserInfoHeaders

func SetUserInfoHeaders(w http.ResponseWriter)

SetUserInfoHeaders sets the headers required by OIDC Core §5.3.2 and RFC 6750.

func TracerSpan

func TracerSpan(ctx context.Context, tracer trace.Tracer, name string) (context.Context, trace.Span)

TracerSpan starts a span from a trace.Tracer. If the tracer is nil, it returns a no-op span from the context. This is the recommended way to start spans in plugins to avoid nil-pointer panics in tests.

func ValidateAuthRequestParams

func ValidateAuthRequestParams(client AuthRequestClient, authReq *protocol.AuthRequest, defaultScopes ...string) error

ValidateAuthRequestParams validates all authorization request parameters. defaultScopes are applied when the client omits the scope parameter (optional).

func ValidateAuthRequestParamsExceptRedirectURI

func ValidateAuthRequestParamsExceptRedirectURI(client AuthRequestClient, authReq *protocol.AuthRequest, defaultScopes ...string) error

ValidateAuthRequestParamsExceptRedirectURI validates all params except redirect_uri. This is called after redirect_uri has been validated separately, so that remaining errors can be safely redirected to the registered URI. defaultScopes are applied when the client omits the scope parameter (optional).

func ValidateDPoPProofATH

func ValidateDPoPProofATH(proof DPoPProof, accessToken string) error

ValidateDPoPProofATH validates the ath (access token hash) claim in a DPoP proof against the actual access token (RFC 9449 §7.1). The ath claim MUST equal base64url(SHA-256(access_token)).

func ValidateNonce

func ValidateNonce(authReq *protocol.AuthRequest) error

ValidateNonce enforces that nonce is present for implicit and hybrid flows (OIDC Core §3.2.2.1, §3.3.2.1).

func ValidatePKCE

func ValidatePKCE(authReq *protocol.AuthRequest) error

ValidatePKCE checks that code_challenge_method is valid when code_challenge is present.

func ValidatePrompt

func ValidatePrompt(authReq *protocol.AuthRequest) error

ValidatePrompt validates the prompt parameter and mutates authReq accordingly. Per OIDC Core §3.1.2.1:

  • "none" MUST NOT be combined with other values.
  • "login" forces re-authentication by setting max_age to 0.

func ValidateRedirectURI

func ValidateRedirectURI(client AuthRequestClient, uri string, responseType protocol.ResponseType) error

ValidateRedirectURI validates the redirect_uri parameter.

func ValidateRemoteURL

func ValidateRemoteURL(rawURL string) error

ValidateRemoteURL validates a user-provided URL to prevent SSRF attacks. Rules:

  • Scheme must be https (or http for localhost only)
  • For non-localhost URLs, DNS is resolved and private/link-local/loopback IPs are blocked

func ValidateResponseType

func ValidateResponseType(client AuthRequestClient, responseType protocol.ResponseType) error

ValidateResponseType validates the response_type parameter.

func ValidateScopes

func ValidateScopes(client AuthRequestClient, authReq *protocol.AuthRequest, defaultScopes ...string) error

ValidateScopes validates the scope parameter. If the client omits the scope parameter, defaultScopes are applied when provided. Per RFC 6749 §4.1.1: "If the client omits the scope parameter when requesting authorization, the authorization server MUST either process the request using a pre-defined default value or fail the request indicating an invalid scope."

func ValidateSenderConstraining

func ValidateSenderConstraining(client interface{}, globalDPoP, globalMtls bool, r *http.Request) error

ValidateSenderConstraining verifies that the client's sender-constraining requirements are met for the incoming token endpoint request.

Per-client configuration takes precedence over global flags. When both DPoP and mTLS are required, the client supports sender-constraining via either mechanism and at least one proof-of-possession MUST be presented (this is the common FAPI 2.0 case where the actual mechanism is chosen by the conformance variant / client request).

func ValidateSignedRequestObjectRequired

func ValidateSignedRequestObjectRequired(client interface{}, hasRequestObject bool) error

ValidateSignedRequestObjectRequired returns an error if the client is configured to require signed request objects but the request does not contain one. It is used by both the PAR and authorization endpoints.

A signed request object is required when the client has request_object_signing_alg configured.

Note: FAPI 2.0 Security Profile does NOT mandate signed request objects. Only FAPI 2.0 Message Signing (a separate profile) does, and that is enforced via the client's request_object_signing_alg setting.

func VerifyTokenBinding

func VerifyTokenBinding(ctx context.Context, cnf map[string]any) error

VerifyTokenBinding verifies that the current request proves possession of the key bound to the token via cnf claim (RFC 8705 §5, RFC 9449 §7.2).

This is a stateless function that checks:

  • cnf.jkt against DPoP proof in context
  • cnf.x5t#S256 against TLS client certificate in context

Returns nil if the token is not sender-constrained (no cnf) or if verification succeeds. Returns a protocol error otherwise.

func WriteError

func WriteError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger)

WriteError writes an OIDC error response to the response writer. It inspects the error chain for StatusError and oidc.Error to determine the correct HTTP status code and JSON body.

This is a shared utility; plugins may choose to use it or implement their own error handling.

Types

type ApplicationTypeClient

type ApplicationTypeClient interface {
	ApplicationType() int
}

ApplicationTypeClient is an optional interface for application type. The return value uses int to allow plugins to define their own constants.

type AuthRequestClient

type AuthRequestClient interface {
	GetID() string
	AuthMethod() protocol.AuthMethod
}

AuthRequestClient is the minimal client interface for auth request validation.

type AuthenticatedClient

type AuthenticatedClient interface {
	GetID() string
}

AuthenticatedClient is the minimal interface for a client that has already been authenticated by an assertion-based method (RFC 7523 §2.2, OIDC Core §9).

func AuthenticatedClientFromContext

func AuthenticatedClientFromContext(ctx context.Context) AuthenticatedClient

AuthenticatedClientFromContext retrieves a pre-authenticated client from the context. Returns nil if no client was pre-authenticated.

type Client

type Client interface {
	GetID() string
	AuthMethod() protocol.AuthMethod
}

Client is the minimal client interface used by ClientAuthHelper.

func AuthenticatePrivateKeyJWT

func AuthenticatePrivateKeyJWT(r *http.Request, getClientByClientID func(ctx context.Context, clientID string) (Client, error), assertion string, getAudiences func(client Client) []string, skipTLSVerify, allowPrivateIPs bool) (Client, error)

AuthenticatePrivateKeyJWT authenticates a client using a JWT assertion signed with the client's private key (RFC 7523 §2.2, OIDC Core §9).

getClientByClientID is a function that looks up a client by ID. getAudiences is called after the client is resolved so the allowed aud values can depend on the client's profile (e.g. FAPI 2.0 vs plain OAuth). If nil, the assertion's audience is not checked (not recommended).

type ClientAuthHelper

type ClientAuthHelper struct {
	// contains filtered or unexported fields
}

ClientAuthHelper extracts and verifies client credentials from HTTP requests. It supports Basic Auth, POST body, private_key_jwt (client_assertion), and tls_client_auth (mTLS certificate).

Plugins that need client authentication can use this helper or implement their own logic for special cases (e.g., JWT Profile grants).

func NewClientAuthHelper

func NewClientAuthHelper(store ClientStore) *ClientAuthHelper

NewClientAuthHelper creates a helper backed by the given store.

func NewClientAuthHelperFromFuncs

func NewClientAuthHelperFromFuncs(
	getFn func(ctx context.Context, clientID string) (Client, error),
	authFn func(ctx context.Context, clientID, clientSecret string) error,
) *ClientAuthHelper

NewClientAuthHelperFromFuncs creates a helper backed by function references. Use this when bridging a store that returns clients with additional methods (e.g., storm.Client with LoginURL) — Go doesn't support covariant return types.

func (*ClientAuthHelper) AuthenticateClient

func (h *ClientAuthHelper) AuthenticateClient(r *http.Request) (Client, error)

AuthenticateClient extracts client credentials from the request and verifies them against the store. Supports three authentication methods in order of precedence:

  1. client_assertion (private_key_jwt) — RFC 7523 §2.2
  2. mTLS certificate (tls_client_auth) — RFC 8705 §2
  3. Basic Auth or form body (client_secret_basic / client_secret_post)

Returns protocol.ErrInvalidClient if authentication fails.

func (*ClientAuthHelper) WithAllowPrivateIPs

func (h *ClientAuthHelper) WithAllowPrivateIPs(allow bool) *ClientAuthHelper

WithAllowPrivateIPs allows private IPs in JWKS URIs. Use ONLY for testing/development environments.

func (*ClientAuthHelper) WithTLSSkipVerify

func (h *ClientAuthHelper) WithTLSSkipVerify(skip bool) *ClientAuthHelper

WithTLSSkipVerify enables skipping TLS certificate verification for JWKS fetches. Use ONLY for testing with self-signed certificates.

type ClientCertBoundAuthenticator

type ClientCertBoundAuthenticator interface {
	ValidateClientCert(cert *x509.Certificate, clientID string) error
}

ClientCertBoundAuthenticator is an optional interface for clients that require the TLS client certificate to match a specific identity (e.g., certificate CN must match client_id). When ValidateClientCert returns a non-nil error, the token/bc-authorize endpoint rejects the request.

This is disabled by default (clients not implementing this interface skip certificate identity validation). Implement this on your Client struct to enable per-client mTLS identity verification per RFC 8705 §2.1.

SECURITY: If you have multiple tls_client_auth clients sharing the same TLS certificate infrastructure, you MUST implement this interface to prevent cross-client access. Without it, any client holding a valid TLS certificate can impersonate another tls_client_auth client by providing a different client_id in the request.

type ClientStore

type ClientStore interface {
	GetClientByClientID(ctx context.Context, clientID string) (Client, error)
	AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
}

ClientStore is the minimal interface needed for client authentication.

Security: AuthorizeClientIDSecret MUST use constant-time comparison (crypto/subtle.ConstantTimeCompare or bcrypt.CompareHashAndPassword) to prevent timing attacks that could reveal secret validity.

type DPoPProof

type DPoPProof interface {
	JWKThumbprint() string
	AccessTokenHash() string // ath claim (base64url(SHA-256(access_token))), empty if absent
}

DPoPProof is the minimal interface for a DPoP proof stored in context. Defined here to avoid import cycles between plugins.

func DPoPFromContext

func DPoPFromContext(ctx context.Context) DPoPProof

DPoPFromContext retrieves the DPoP proof from the context. Returns nil if no DPoP proof was presented.

type DevModeClient

type DevModeClient interface {
	DevMode() bool
}

DevModeClient is an optional interface for dev mode.

type EndpointConfig added in v2.3.0

type EndpointConfig struct {
	// RoutePath is the actual route path for the endpoint.
	// For example: "/oauth2/token", "/api/v1/authorize"
	RoutePath string

	// DiscoveryURL is the URL that appears in the discovery document.
	// If empty, the default discovery URL is used.
	// For example: "https://op.example.com/oauth2/token"
	DiscoveryURL string
}

EndpointConfig defines the configuration for an endpoint. It allows customizing both the route path and the discovery URL.

type EndpointConfigMap added in v2.3.0

type EndpointConfigMap map[string]EndpointConfig

EndpointConfigMap maps endpoint names to their configurations. Endpoint names are short identifiers like "token", "authorize", "userinfo".

func (EndpointConfigMap) GetDiscoveryURL added in v2.3.0

func (m EndpointConfigMap) GetDiscoveryURL(endpointName, defaultURL string) string

GetDiscoveryURL returns the discovery URL for the given endpoint. If the endpoint is not configured or DiscoveryURL is empty, returns defaultURL.

func (EndpointConfigMap) GetRoutePath added in v2.3.0

func (m EndpointConfigMap) GetRoutePath(endpointName, defaultPath string) string

GetRoutePath returns the route path for the given endpoint. If the endpoint is not configured or RoutePath is empty, returns defaultPath.

type FAPIProfileClient

type FAPIProfileClient interface {
	FAPIProfile() bool
}

FAPIProfileClient is an optional interface for FAPI 2.0 profile detection.

type IDTokenSignedResponseAlgProvider

type IDTokenSignedResponseAlgProvider interface {
	IDTokenSignedResponseAlg() string
}

IDTokenSignedResponseAlgProvider is an optional interface for clients that specify a preferred ID token signing algorithm (id_token_signed_response_alg). When implemented and returning a non-empty string, the token endpoint uses this algorithm to sign the ID token instead of the default.

type IssuerFromRequest

type IssuerFromRequest func(*http.Request) string

func IssuerFromHost

func IssuerFromHost(path string) IssuerFromRequest

IssuerFromHost returns an IssuerFromRequest that derives the issuer from the request's Host header, using the given path suffix.

func StaticIssuer

func StaticIssuer(issuer string) IssuerFromRequest

StaticIssuer returns an IssuerFromRequest that always returns the given value.

type JWKSProvider

type JWKSProvider interface {
	ClientJWKS() []jwk.Key
}

JWKSProvider is optionally implemented by Client to provide the client's public JWKS keys for signature verification.

type JWKSURIProvider

type JWKSURIProvider interface {
	ClientJWKSURI() string
}

JWKSURIProvider is optionally implemented by Client to provide the client's jwks_uri for fetching fresh keys.

type RedirectURIClient

type RedirectURIClient interface {
	RedirectURIs() []string
}

RedirectURIClient is an optional interface for redirect_uri validation.

type RedirectURIGlobClient

type RedirectURIGlobClient interface {
	RedirectURIGlobs() []string
}

RedirectURIGlobClient is an optional interface for glob redirect_uri validation.

type RequestObjectClientLookup

type RequestObjectClientLookup func(ctx context.Context, clientID string) (Client, error)

RequestObjectClientLookup is a function that looks up a client by ID for request object verification. The returned client must implement JWKSProvider (and optionally JWKSURIProvider) to provide verification keys.

type RequestObjectSigningAlgProvider

type RequestObjectSigningAlgProvider interface {
	RequestObjectSigningAlg() string
}

RequestObjectSigningAlgProvider is an optional interface for clients that require signed request objects (e.g. FAPI 2.0 signed_non_repudiation). When implemented and returning a non-empty string, the authorization server MUST reject authorization/PAR requests without a signed request object.

type ResponseTypesProvider

type ResponseTypesProvider interface {
	ResponseTypes() []protocol.ResponseType
}

ResponseTypesProvider is an optional interface for allowed response types.

type ScopeValidationClient

type ScopeValidationClient interface {
	StrictScopeValidation() bool
}

ScopeValidationClient is an optional interface to control scope validation strictness.

type SenderConstrainingProvider

type SenderConstrainingProvider interface {
	RequireDPoP() bool
	RequireMtls() bool
}

SenderConstrainingProvider is an optional interface for clients that require sender-constrained tokens (FAPI 2.0 Security Profile). When RequireDPoP returns true, the token endpoint MUST require a DPoP proof. When RequireMtls returns true, the token endpoint MUST require mTLS client auth.

type StatusError

type StatusError struct {
	// contains filtered or unexported fields
}

StatusError wraps an error with an explicit HTTP status code. Plugins may return this to signal non-standard status codes.

func AsStatusError

func AsStatusError(err error, statusCode int) StatusError

AsStatusError unwraps a StatusError from err, or creates a new one.

func NewStatusError

func NewStatusError(parent error, statusCode int) StatusError

NewStatusError creates a StatusError.

func (StatusError) Error

func (e StatusError) Error() string

func (StatusError) Unwrap

func (e StatusError) Unwrap() error

type UserInfoSignedResponseAlgProvider

type UserInfoSignedResponseAlgProvider interface {
	UserInfoSignedResponseAlg() string
}

UserInfoSignedResponseAlgProvider is an optional interface for clients that specify a preferred UserInfo signing algorithm (userinfo_signed_response_alg). When implemented and returning a non-empty string, the userinfo endpoint uses this algorithm to sign the JWT response instead of the default.

Jump to

Keyboard shortcuts

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