storm

package
v2.2.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: 23 Imported by: 0

Documentation

Overview

Package storm provides the StormEngine plugin-based OIDC server framework.

StormEngine separates routing, middleware, and plugin discovery from OIDC protocol logic. Each OIDC feature (authorize, token, discovery, etc.) is implemented as an independent plugin that registers its own routes.

StormEngine storage interfaces.

Storage is the data layer of StormEngine. You implement these interfaces to connect the OIDC protocol engine to your database, LDAP, or any other backend. The SDK never binds to a specific storage technology.

How it works

Engine discovers your storage's capabilities via Go type assertions. Each plugin declares which interfaces it needs (via Requires()), and Engine checks at Build time that your storage satisfies them. If a required interface is missing, Build() panics with a clear error telling you exactly which methods to add.

Implementation guide

The interfaces below are grouped by how much OIDC functionality they unlock. Start with Core, add Standard/Extended as needed.

## Core — required for a working OIDC authorization code flow

  • Storage (base) — ClientStore + KeyStore + Health
  • ClientStore (always) — client lookup + secret verification
  • KeyStore (always) — JWKS publication + signing key access
  • AuthStore (authorization + token plugins) — auth request CRUD + code management
  • TokenStore (token plugin) — access/refresh token creation + lookup
  • UserinfoStore (userinfo plugin) — populate UserInfo from token

These 6 interfaces are the minimum for a working authorization code flow with UserInfo endpoint. The example at example/storm-server/storage/ provides a reference implementation (~800 lines for Core).

## Standard — enable common OIDC features

## Extended — advanced features, implement as needed

## Sender-constraining extensions (FAPI 2.0 / RFC 9449)

These interfaces bind tokens to the client's cryptographic key or certificate, preventing stolen tokens from being replayed by a different party.

The cnf claim is now passed directly to TokenStore.CreateAccessToken and TokenStore.CreateAccessAndRefreshTokens at issuance time, so storage can bind refresh tokens (via RefreshTokenRequest.GetDPoPJKT) without a separate post-creation store step.

## Optional extensions on Client

Clients can implement additional interfaces for per-client behavior:

See example/storm-server/storage/ for a complete reference implementation.

Index

Constants

View Source
const (
	PriorityAuthorization = 100
	PriorityToken         = 200
	PriorityKeys          = 300
	PriorityDiscovery     = 400
	PriorityUserinfo      = 500
	PriorityIntrospection = 600
	PriorityRevocation    = 700
	PriorityEndSession    = 800
	PriorityBackChannel   = 850
	PriorityDevice        = 900
	PriorityPAR           = 950
	PriorityDPoP          = 960
	PriorityJARM          = 975
	PriorityDCR           = 1000
	PriorityCIBA          = 1050
	PriorityMTLS          = 1060
	PriorityWebFinger     = 1100
)

PluginPriority defines registration ordering. Core plugins use lower numbers; optional plugins use higher numbers.

Variables

This section is empty.

Functions

func NewClientAuthHelper

func NewClientAuthHelper(cs ClientStore) *shared.ClientAuthHelper

NewClientAuthHelper creates a shared.ClientAuthHelper from a storm.ClientStore. This bridges the type gap between storm.Client (superset with LoginURL) and shared.Client (minimal interface) that Go's type system doesn't allow via covariant return types.

func ParseTokenParts

func ParseTokenParts(plaintext []byte) (tokenID, subject string, ok bool)

ParseTokenParts splits "tokenID:subject" plaintext into its components.

func RegisterPlugin

func RegisterPlugin(name string, priority int, factory PluginFactory)

RegisterPlugin registers a plugin factory in the global registry. Call this in init() or at package level. Plugins are auto-discovered by New() and sorted by priority.

func RegisterStorageInterface

func RegisterStorageInterface(name string, t reflect.Type)

RegisterStorageInterface registers a third-party storage interface so that Validate() can provide method hints when it is missing.

Usage in a third-party plugin:

func init() {
    storm.RegisterStorageInterface("MyCustomStore", reflect.TypeOf((*MyCustomStore)(nil)).Elem())
}

func ResolveToken

func ResolveToken(ctx context.Context, crypto UniCrypto, keyStore protocol.KeyStore, issuer, token string) (tokenID, subject string, ok bool)

ResolveToken resolves an opaque token to its tokenID and subject. Supports standard decrypted tokens, GM/T JWE tokens, and JWT access tokens.

This is the shared implementation used by both introspection and userinfo plugins.

Types

type AccessTokenFormatClient added in v2.1.0

type AccessTokenFormatClient interface {
	AccessTokenFormat() AccessTokenType
}

AccessTokenFormatClient is optionally implemented by Client to specify the access token format (opaque bearer vs JWT).

This is the kexcore-oidc equivalent of Zitadel's Client.AccessTokenType().

When not implemented, the token format is determined by the storage layer. JWT access tokens require the storage to implement JWTAccessTokenStore.

type AccessTokenLifetimeProvider

type AccessTokenLifetimeProvider interface {
	AccessTokenLifetime() time.Duration
}

AccessTokenLifetimeProvider is optionally implemented by Client to control the lifetime of issued access tokens.

When not implemented, the TokenStore decides the default lifetime. This is useful for multi-tenant IAM where different clients need different token lifetimes (e.g., high-security clients with short-lived tokens).

type AccessTokenType added in v2.1.0

type AccessTokenType int

AccessTokenType is an enum for the access token format.

const (
	// AccessTokenTypeBearer uses opaque bearer tokens (default).
	AccessTokenTypeBearer AccessTokenType = iota
	// AccessTokenTypeJWT uses JWT-format access tokens (RFC 9068).
	AccessTokenTypeJWT
)

type AuditEvent

type AuditEvent struct {
	Timestamp  time.Time      `json:"timestamp"`
	EventType  string         `json:"event_type"` // e.g. "token.issued", "auth.failed", "auth.success"
	ClientID   string         `json:"client_id"`
	Subject    string         `json:"subject"`
	GrantType  string         `json:"grant_type"`
	RemoteAddr string         `json:"remote_addr"`
	Detail     map[string]any `json:"detail,omitempty"`
}

AuditEvent represents a security-relevant event for audit logging. Storage implementations that support audit logging should implement AuditLogger.

type AuditLogger

type AuditLogger interface {
	WriteAuditEvent(ctx context.Context, event AuditEvent) error
}

AuditLogger is optionally implemented by Storage to receive structured audit events for security-relevant operations (token issuance, authentication failures, etc.). Implementations should persist these events for compliance and incident response.

When not implemented, audit events are logged via slog at INFO/WARN level as a fallback.

type AuthRequest

type AuthRequest interface {
	GetID() string
	GetACR() string
	GetAMR() []string
	GetAudience() []string
	GetAuthTime() time.Time
	GetClientID() string
	GetCodeChallenge() *protocol.CodeChallenge
	GetNonce() string
	GetRedirectURI() string
	GetResponseType() protocol.ResponseType
	GetResponseMode() protocol.ResponseMode
	GetScopes() []string
	GetState() string
	GetSubject() string
	GetClaims() *protocol.ClaimsRequest
	GetSID() string
	Done() bool
}

AuthRequest represents an in-flight authorization request.

type AuthStore

type AuthStore interface {
	CreateAuthRequest(ctx context.Context, req *protocol.AuthRequest, userID string) (AuthRequest, error)
	AuthRequestByID(ctx context.Context, id string) (AuthRequest, error)
	AuthRequestByCode(ctx context.Context, code string) (AuthRequest, error)
	SaveAuthCode(ctx context.Context, id, code string) error
	DeleteAuthRequest(ctx context.Context, id string) error
}

AuthStore is required by the Authorization and Token plugins. It manages the lifecycle of authorization requests and authorization codes.

Implementation notes:

  • CreateAuthRequest: store the auth request, return a handle (AuthRequest) with a unique ID. The returned AuthRequest must satisfy the AuthRequest interface (16 getters).
  • AuthRequestByID: look up by the handle ID (used during login UI flow).
  • AuthRequestByCode: look up by authorization code (used during token exchange). After a successful lookup, the code should be invalidated (one-time use).
  • SaveAuthCode: associate an authorization code string with an auth request ID.
  • DeleteAuthRequest: clean up after the auth request is fully processed.

type AutoCompleteAuthRequest

type AutoCompleteAuthRequest interface {
	CompleteAuthRequest(ctx context.Context, id string, subject string, authTime time.Time, sid string) error
}

AutoCompleteAuthRequest is an optional interface that AuthStore can implement to support auto-completing auth requests without going through the login UI. This is used when prompt=none is requested and an active session exists (OIDC Core §3.1.2.6).

When implemented, the Authorization plugin calls CompleteAuthRequest instead of redirecting to the login UI, ensuring the auth_time claim reflects the original authentication time.

type BackChannelStore

type BackChannelStore interface {
	ClientsForSession(ctx context.Context, sub, sid string) ([]Client, error)
}

BackChannelStore is required by the Back Channel Logout plugin.

type CIBANotificationCallback

type CIBANotificationCallback interface {
	OnCIBAStatusChange(ctx context.Context, req *CIBARequest) error
}

CIBANotificationCallback is optionally implemented by the storage to receive notifications when a CIBA request status changes (approved/denied).

CIBA Core 1.0 §10 — Ping and Push Notification Modes When the delivery mode is "ping", the OP MUST notify the client's client_notification_endpoint when the authentication completes. The actual HTTP POST to the client's endpoint is the storage/implementation's responsibility — the SDK only provides this callback hook.

Implementations should:

  1. Send an HTTP POST to the client's client_notification_endpoint
  2. Include the client_notification_token as a Bearer token
  3. Include the auth_req_id in the request body
  4. Handle retries with exponential backoff

Example usage:

func (s *MyStorage) OnCIBAStatusChange(ctx context.Context, req *CIBARequest) error {
    if req.DeliveryMode != protocol.CIBAModePing {
        return nil
    }
    client, _ := s.GetClientByClientID(ctx, req.ClientID)
    endpoint := client.NotificationEndpoint()
    return httpPost(ctx, endpoint, req.ClientNotificationToken, req.AuthReqID)
}

type CIBARequest

type CIBARequest struct {
	AuthReqID               string
	ClientID                string
	Scope                   string
	Subject                 string
	BindingMessage          string
	UserCode                string
	RequestedScopes         []string
	ExpiresAt               time.Time
	Status                  protocol.CIBAStatus
	DeliveryMode            protocol.CIBADeliveryMode
	ClientNotificationToken string
	ApprovedScopes          []string
	LastPoll                time.Time // last poll time for slow_down detection (CIBA Core 1.0 §10.1)
	Interval                int       // current polling interval in seconds
}

CIBARequest represents a stored CIBA backchannel authentication request.

type CIBAStore

type CIBAStore interface {
	// StoreCIBARequest persists a new CIBA authentication request.
	StoreCIBARequest(ctx context.Context, req *CIBARequest) error

	// GetCIBARequestByAuthReqID retrieves a CIBA request by its auth_req_id.
	// Returns protocol.ErrAuthorizationPending if not found.
	GetCIBARequestByAuthReqID(ctx context.Context, authReqID string) (*CIBARequest, error)

	// UpdateCIBARequestStatus updates the status of a CIBA request.
	UpdateCIBARequestStatus(ctx context.Context, authReqID string, status protocol.CIBAStatus, approvedScopes []string) error

	// GetPendingCIBARequests returns all pending CIBA requests for a given subject.
	// This is used by the approval page to show pending requests.
	GetPendingCIBARequests(ctx context.Context, subject string) ([]*CIBARequest, error)

	// UpdateCIBAPoll records the last poll time for slow_down detection.
	// Called by the token endpoint when a client polls for the CIBA grant.
	UpdateCIBAPoll(ctx context.Context, authReqID string, lastPoll time.Time) error

	// UpdateCIBAInterval increases the polling interval for slow_down compliance.
	// CIBA Core 1.0 §10.1: the interval MUST be increased by 5 seconds on slow_down.
	UpdateCIBAInterval(ctx context.Context, authReqID string, increment int) error
}

CIBAStore is required by the CIBA plugin. It manages CIBA backchannel authentication request lifecycle.

type CategorizablePlugin

type CategorizablePlugin interface {
	Plugin

	// Category returns the plugin's registration category.
	Category() PluginCategory

	// Requires returns the set of storage interface names this plugin needs.
	// The Engine checks whether the Storage implementation satisfies these
	// before registering the plugin. Return nil if no storage dependencies.
	// Example: []string{"UserinfoStore", "IntrospectStore"}
	Requires() []string
}

CategorizablePlugin is optionally implemented by plugins to declare their category and storage dependency requirements. The Engine uses this for automatic registration and ordering.

type Client

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

Client is the minimal client interface.

type ClientCredentialsStore

type ClientCredentialsStore interface {
	ClientCredentials(ctx context.Context, clientID, clientSecret string) (Client, error)
	ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error)
}

ClientCredentialsStore is required by the Client Credentials grant.

type ClientJWKSProvider

type ClientJWKSProvider interface {
	// ClientJWKS returns the client's public keys for signature verification.
	ClientJWKS() []jwk.Key
}

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

Used by the JWT Bearer Grant (RFC 7523 §2.1) to verify the client's signed assertion. The returned keys should include use=sig keys.

When not implemented, the client cannot use the JWT Bearer Grant.

type ClientJWKSURIProvider

type ClientJWKSURIProvider interface {
	// ClientJWKSURI returns the client's jwks_uri endpoint.
	ClientJWKSURI() string
}

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

When the RP rotates its keys, the OP should fetch fresh keys from the jwks_uri instead of using cached keys from registration.

type ClientKeyProvider

type ClientKeyProvider interface {
	// ClientEncryptionKey returns the key used to encrypt ID tokens.
	// Preferred type: jwk.Key (includes kid for JWE header).
	// Also accepted: *rsa.PublicKey, *ecdh.PublicKey, []byte.
	ClientEncryptionKey() interface{}
}

ClientKeyProvider is optionally implemented by Client to provide the client's public key for ID token encryption (JWE).

When implemented, the Token plugin uses the returned key to encrypt the ID token using the algorithm specified by IDTokenEncryptionAlg/Enc.

Returning a jwk.Key ensures the JWE header includes the correct "kid". Raw key types (*rsa.PublicKey, *ecdh.PublicKey, []byte) are also accepted but will not produce a "kid" in the JWE header.

When not implemented, ID token encryption is only available for algorithms that use the server's UniCrypto (dir, SM2, SM9).

type ClientRegistration

type ClientRegistration struct {
	ClientID                     string          `json:"client_id"`
	ClientSecret                 string          `json:"client_secret,omitempty"`
	RegistrationAccessToken      string          `json:"registration_access_token,omitempty"`
	RegistrationClientURI        string          `json:"registration_client_uri,omitempty"`
	ClientIDIssuedAt             int64           `json:"client_id_issued_at"`
	ClientSecretExpiresAt        int64           `json:"client_secret_expires_at"`
	ApplicationType              string          `json:"application_type,omitempty"`
	ClientName                   string          `json:"client_name,omitempty"`
	ClientURI                    string          `json:"client_uri,omitempty"`
	LogoURI                      string          `json:"logo_uri,omitempty"`
	RedirectURIs                 []string        `json:"redirect_uris"`
	ResponseTypes                []string        `json:"response_types,omitempty"`
	GrantTypes                   []string        `json:"grant_types,omitempty"`
	TokenEndpointAuthMethod      string          `json:"token_endpoint_auth_method,omitempty"`
	Scope                        string          `json:"scope,omitempty"`
	Contacts                     []string        `json:"contacts,omitempty"`
	JWKSURI                      string          `json:"jwks_uri,omitempty"`
	JWKS                         json.RawMessage `json:"jwks,omitempty"`
	PolicyURI                    string          `json:"policy_uri,omitempty"`
	TOSURI                       string          `json:"tos_uri,omitempty"`
	SoftwareID                   string          `json:"software_id,omitempty"`
	SoftwareVersion              string          `json:"software_version,omitempty"`
	PostLogoutRedirectURIs       []string        `json:"post_logout_redirect_uris,omitempty"`
	BackChannelLogoutURI         string          `json:"backchannel_logout_uri,omitempty"`
	SectorIdentifierURI          string          `json:"sector_identifier_uri,omitempty"`
	InitiateLoginURI             string          `json:"initiate_login_uri,omitempty"`
	IDTokenSignedResponseAlg     string          `json:"id_token_signed_response_alg,omitempty"`
	IDTokenEncryptedResponseAlg  string          `json:"id_token_encrypted_response_alg,omitempty"`
	IDTokenEncryptedResponseEnc  string          `json:"id_token_encrypted_response_enc,omitempty"`
	UserInfoSignedResponseAlg    string          `json:"userinfo_signed_response_alg,omitempty"`
	UserInfoEncryptedResponseAlg string          `json:"userinfo_encrypted_response_alg,omitempty"`
	UserInfoEncryptedResponseEnc string          `json:"userinfo_encrypted_response_enc,omitempty"`
	RequestObjectSigningAlg      string          `json:"request_object_signing_alg,omitempty"`
	RequireDPoP                  bool            `json:"require_dpop,omitempty"`
	RequireMtls                  bool            `json:"require_mtls,omitempty"`
}

ClientRegistration represents a registered client.

type ClientSessionRecorder

type ClientSessionRecorder interface {
	RecordClientSession(subject, clientID, sid string)
}

ClientSessionRecorder is an optional interface for tracking client sessions per subject. When implemented by the storage, the token plugin records each client session on token issuance so that BackChannelStore.ClientsForSession can find the relevant RPs to notify on logout.

type ClientStore

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

ClientStore provides client lookup and credential verification. Required by: authorization, token, userinfo, DCR, device, PAR plugins.

Implementation notes:

  • GetClientByClientID: return a Client that implements at least the base Client interface. For additional behavior, return a Client that also implements optional interfaces like ScopeValidationClient, RedirectURIClient, etc.
  • AuthorizeClientIDSecret: verify client_secret for confidential clients. Return nil if valid, error if invalid. Used for client_secret_basic and client_secret_post.

Security considerations:

  • AuthorizeClientIDSecret: secret comparison MUST use constant-time comparison (e.g., crypto/subtle.ConstantTimeCompare or bcrypt.CompareHashAndPassword) to prevent timing-based side-channel attacks that could leak secret validity. NEVER use == or strings.Compare for secret verification.

type CodeReuseDetector

type CodeReuseDetector interface {
	// TrackTokenForAuthRequest records that a token was issued for an auth request.
	TrackTokenForAuthRequest(authRequestID, tokenID string)
	// RevokeTokensForUsedCode revokes all tokens issued for a used code.
	// Returns the auth request ID if found, or empty string if the code was not used.
	RevokeTokensForUsedCode(code string) string
}

CodeReuseDetector is an optional interface that storage can implement to detect authorization code reuse and revoke associated tokens. Per RFC 6749 §4.1.2: "If an authorization code is used more than once, the authorization server MUST revoke all tokens issued based on that authorization code."

type DCRStore

type DCRStore interface {
	CreateClient(ctx context.Context, client *RegistrationRequest, clientID, clientSecret, accessToken, uri string) (*ClientRegistration, error)
	GetClientRegistration(ctx context.Context, clientID string) (*ClientRegistration, error)
	GetClientRegistrationByToken(ctx context.Context, token string) (*ClientRegistration, error)
	UpdateClientRegistration(ctx context.Context, clientID string, update *RegistrationRequest) (*ClientRegistration, error)
	DeleteClientRegistration(ctx context.Context, clientID string) error
}

DCRStore is required by the Dynamic Client Registration plugin.

type DPoPCodeBindingStore

type DPoPCodeBindingStore interface {
	// SetAuthRequestDPoPJKT stores the DPoP JWK thumbprint for an authorization request.
	SetAuthRequestDPoPJKT(ctx context.Context, authRequestID string, jkt string) error
	// GetAuthRequestDPoPJKT retrieves the DPoP JWK thumbprint for an authorization request.
	GetAuthRequestDPoPJKT(ctx context.Context, authRequestID string) (string, error)
}

DPoPCodeBindingStore is optionally implemented by AuthStore to support DPoP authorization code binding (RFC 9449 §7.1).

When the authorization request includes a DPoP proof (dpop_jkt parameter), the authorization plugin stores the JWK thumbprint. The token plugin then verifies that the DPoP proof presented during token exchange matches the stored thumbprint.

type DeviceAuthStore

type DeviceAuthStore interface {
	StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error
	GetDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string) (*DeviceAuthorizationState, error)
	// GetDeviceAuthorizationByUserCode looks up a device authorization by user code.
	// Used by the verification page (GET /device) to display authorization details.
	GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*DeviceAuthorizationState, error)
	// ApproveDeviceAuthorization marks a device authorization as approved by the end-user.
	// subject is the authenticated user's identifier.
	ApproveDeviceAuthorization(ctx context.Context, userCode, subject string) error
	// DenyDeviceAuthorization marks a device authorization as denied by the end-user.
	DenyDeviceAuthorization(ctx context.Context, userCode string) error
	// UpdateDeviceAuthorizationPoll records the last poll time for slow_down detection.
	// Called by the token endpoint when a client polls for the device code grant.
	UpdateDeviceAuthorizationPoll(ctx context.Context, clientID, deviceCode string, lastPoll time.Time) error
	// UpdateDeviceAuthorizationInterval increases the polling interval for slow_down compliance.
	// RFC 8628 §3.4: the interval MUST be increased by 5 seconds on slow_down.
	UpdateDeviceAuthorizationInterval(ctx context.Context, clientID, deviceCode string, increment int) error
}

DeviceAuthStore is required by the Device Authorization plugin.

type DeviceAuthorizationState

type DeviceAuthorizationState struct {
	DeviceCode string
	ClientID   string
	UserCode   string
	Subject    string
	Scopes     []string
	Done       bool
	Denied     bool
	Expires    time.Time
	LastPoll   time.Time // last poll time for slow_down detection (RFC 8628 §3.4)
	Interval   int       // current polling interval in seconds
}

DeviceAuthorizationState represents the current state of a device auth flow.

type DevicePollIntervalClient

type DevicePollIntervalClient interface {
	DevicePollInterval() time.Duration
}

DevicePollIntervalClient is optionally implemented by Client to override the device code / CIBA polling interval. When not implemented, the plugin uses its configured default (typically 5 seconds).

type DiscoveryConfig

type DiscoveryConfig struct {
	ExtraFields map[string]any
}

DiscoveryConfig holds extra fields injected into the discovery document.

type DiscoveryContributor

type DiscoveryContributor interface {
	Plugin

	// Contribute populates discovery fields on the given configuration.
	// Plugins should only set the fields they own.
	// ctx contains the issuer via shared.IssuerFromContext.
	Contribute(ctx context.Context, cfg *protocol.DiscoveryConfiguration)
}

DiscoveryContributor is an optional interface that plugins may implement to contribute fields to the openid-configuration discovery document.

The Engine creates a protocol.DiscoveryConfiguration, calls each contributor's Contribute method in registration order, then serializes the result. Plugins set fields directly on the typed struct — no magic strings, no map round-trips.

Rules for plugins:

  1. Only set fields owned by your plugin. Do not overwrite fields set by other plugins.

  2. Custom/extension fields that don't map to DiscoveryConfiguration struct fields should be placed in cfg.Extra.

  3. The ctx parameter contains the issuer via shared.IssuerFromContext(ctx), allowing plugins to build absolute endpoint URLs.

type EndSessionRequest

type EndSessionRequest struct {
	UserID             string
	ClientID           string
	IDTokenHintClaims  *protocol.IDTokenClaims
	RedirectURI        string
	State              string
	LogoutHint         string
	UILocales          []language.Tag
	InvalidRedirectURI bool // true when post_logout_redirect_uri was provided but not registered
}

EndSessionRequest represents a parsed RP-initiated logout request.

type Engine

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

Engine is the core orchestrator for StormEngine. It manages plugin registration, route assembly, middleware application, and discovery document collision detection.

Engine does not know anything about OIDC. It is a pure HTTP framework that delegates all protocol logic to registered plugins.

func New

func New(storage Storage, issuerFn shared.IssuerFromRequest, opts ...EngineOption) *Engine

New creates a new Engine with the given storage and issuer function.

The issuerFn is used to inject the issuer into the request context and into the discovery document.

Plugins are registered from two sources:

  1. Global registry (via RegisterPlugin) — auto-discovered and sorted by priority
  2. WithPlugin option factories — registered after global plugins

Use Disable() to prevent specific Standard/Optional plugins from loading.

func (*Engine) Build

func (e *Engine) Build() http.Handler

Build finalizes the engine by:

  1. Installing the built-in /healthz and /ready handlers
  2. Installing the discovery aggregator
  3. Detecting discovery key collisions across all DiscoveryContributor plugins
  4. Applying CORS and user middleware

If discovery key collisions are detected, Build panics. This is intentional: collisions indicate a configuration error that must be fixed before the server can start.

func (*Engine) Handler

func (e *Engine) Handler() http.Handler

Handler returns the built handler. Must be called after Build.

func (*Engine) Plugins

func (e *Engine) Plugins() []Plugin

Plugins returns a snapshot of registered plugins.

func (*Engine) Register

func (e *Engine) Register(p Plugin)

Register adds a plugin to the engine. The plugin's routes are immediately installed on the internal router.

Name collisions are not checked here; they are detected at Build time via discovery document collision detection.

func (*Engine) Storage

func (e *Engine) Storage() Storage

Storage returns the engine's storage.

func (*Engine) Validate

func (e *Engine) Validate() error

Validate checks plugin registration consistency and storage compatibility. It validates:

  • Each plugin's declared storage dependencies are satisfied
  • Core plugin combinations satisfy RFC constraints
  • No duplicate route registrations

Returns the first error encountered, enabling fail-fast behavior.

type EngineOption

type EngineOption func(*Engine)

EngineOption configures an Engine.

func Disable

func Disable(names ...string) EngineOption

Disable prevents the named plugins from being registered. Only affects Standard and Optional plugins; Core plugins cannot be disabled.

func Enable

func Enable(names ...string) EngineOption

Enable explicitly enables named plugins that would otherwise not be registered. This is primarily used for Optional plugins (CategoryOptional) which are skipped by default unless explicitly enabled.

func WithAllowPrivateIPs

func WithAllowPrivateIPs() EngineOption

WithAllowPrivateIPs disables SSRF protection in DCR (jwks_uri, sector_identifier_uri). Use ONLY for testing with self-hosted conformance suites on private networks. NEVER enable in production.

For production deployments that need to allow specific private CIDRs, handle this at the network layer (firewall / reverse proxy) instead.

func WithCORS

func WithCORS(opts *cors.Options) EngineOption

WithCORS sets the CORS policy for the engine.

func WithCrypto

func WithCrypto(c UniCrypto) EngineOption

WithCrypto sets the UniCrypto implementation for token encryption/signing. Plugins that need Crypto (authorization, token, introspection, userinfo, revocation) will receive it via PluginContext.

func WithDecoder

func WithDecoder(d *protocol.Decoder) EngineOption

WithDecoder sets the protocol decoder for form parsing. If not set, a default decoder is created.

func WithDiscoveryConfig

func WithDiscoveryConfig(cfg DiscoveryConfig) EngineOption

WithDiscoveryConfig sets extra fields injected into the discovery document.

func WithEndpointResolver added in v2.2.0

func WithEndpointResolver(resolver shared.EndpointResolver) EngineOption

WithEndpointResolver sets a custom endpoint resolver for the engine. This allows overriding the URL for specific endpoints in the discovery document.

Example:

engine := storm.New(storage, issuerFn, storm.WithEndpointResolver(
    &shared.OverrideEndpointResolver{
        Base: &shared.DefaultEndpointResolver{},
        Overrides: map[string]string{
            "token": "https://token-service.example.com/token",
        },
    },
))

func WithImplicit

func WithImplicit() EngineOption

WithImplicit enables the Implicit and Hybrid flows. Disabled by default per OAuth 2.1.

func WithLogger

func WithLogger(logger *slog.Logger) EngineOption

WithLogger sets the logger used by the engine.

func WithMiddleware

func WithMiddleware(mw ...func(http.Handler) http.Handler) EngineOption

WithMiddleware appends middleware to the engine's middleware chain. Middleware is applied in the order given, wrapping the final handler.

func WithPARLifetime

func WithPARLifetime(d time.Duration) EngineOption

WithPARLifetime sets the lifetime for pushed authorization request URIs. If not set, the PAR plugin uses its own default (typically 90s).

func WithPlainPKCE

func WithPlainPKCE() EngineOption

WithPlainPKCE enables the "plain" code_challenge_method (RFC 7636). Disabled by default per OAuth 2.1 §4.1.1. Call this to allow clients to use code_challenge_method=plain when they explicitly opt in.

func WithPlugin

func WithPlugin(factory PluginFactory) EngineOption

WithPlugin adds a plugin factory to the Engine. The factory is called during Build() with the Engine's Storage.

func WithRateLimiter

func WithRateLimiter(rl *RateLimiter) EngineOption

WithRateLimiter enables rate limiting on the engine using an in-memory token bucket limiter. The rate limiter applies to all routes. For production IAM, this prevents brute-force attacks on the token endpoint.

Example:

limiter := storm.NewRateLimiter(100, time.Minute) // 100 req/min per IP
engine := storm.New(storage, issuerFn, storm.WithRateLimiter(limiter))

func WithRequireDPoP

func WithRequireDPoP() EngineOption

WithRequireDPoP enables FAPI 2.0 DPoP sender-constrained tokens.

func WithRequireMtls

func WithRequireMtls() EngineOption

WithRequireMtls enables FAPI 2.0 mTLS sender-constrained tokens.

func WithSkipTLSCertVerify

func WithSkipTLSCertVerify() EngineOption

WithSkipTLSCertVerify disables TLS certificate verification on outbound HTTP requests (JWKS fetches, request_uri, backchannel_logout, etc.). Use ONLY for testing with self-signed certificates. NEVER enable in production.

type FAPIProfileProvider

type FAPIProfileProvider interface {
	FAPIProfile() bool
}

FAPIProfileProvider is an optional interface that clients may implement to indicate they are configured for FAPI (Financial-grade API) compliance. Plugins that enforce FAPI-specific requirements (e.g., signed request objects for FAPI-CIBA) check for this interface via type assertion.

type GMDecryptor

type GMDecryptor interface {
	SM2DecryptJWE(ctx context.Context, compact string) ([]byte, error)
}

GMDecryptor is an optional interface for GM/T JWE decryption. Implementations that support GM/T can implement this interface.

type GMSigningKey

type GMSigningKey interface {
	SigningKey

	// GMSigner returns the crypto.Signer for GM/T signing operations.
	// The returned Signer can produce JWS signatures using SGD_SM3_SM2 or SGD_SM3_SM9.
	GMSigner() *GMTokenSigner
}

GMSigningKey extends SigningKey for GM/T signing keys (SM2, SM9). Plugins that need to sign with GM/T algorithms should check for this interface.

type GMTokenSigner

type GMTokenSigner struct {
	// Algorithm is the signing algorithm (e.g. "SGD_SM3_SM2", "SGD_SM3_SM9").
	Algorithm string
	// KeyID is the JWK key ID.
	KeyID string
	// SignFunc signs the payload and returns compact JWS serialization.
	SignFunc func(payload []byte) (string, error)
}

GMTokenSigner provides GM/T token signing capability. It wraps pkg/crypto.Signer for use in storm plugins.

func (*GMTokenSigner) Sign

func (s *GMTokenSigner) Sign(payload []byte) (string, error)

Sign signs the payload and returns the compact JWS serialization.

type GrantTypeClient added in v2.1.0

type GrantTypeClient interface {
	AllowedGrantTypes() []string
}

GrantTypeClient is optionally implemented by Client to restrict which OAuth 2.0 grant types are allowed for this specific client.

This is the kexcore-oidc equivalent of Casdoor's "OAuth授权类型" setting and Keycloak's per-client "Grant Types" configuration.

When implemented, the Token plugin checks the requested grant_type against this list BEFORE processing. If the grant type is not in the list, the request is rejected with unsupported_grant_type.

When not implemented, all grant types enabled at the server level are allowed.

Security considerations:

  • Public clients should only allow authorization_code (with PKCE)
  • Confidential clients can additionally use client_credentials
  • Machine-to-machine clients can use client_credentials and jwt-bearer
  • Device flow clients can use device_code

type IDTokenLifetimeClient

type IDTokenLifetimeClient interface {
	IDTokenLifetime() time.Duration
}

IDTokenLifetimeClient is optionally implemented by Client to control the lifetime of issued ID tokens. When not implemented, the plugin uses its default (typically 1 hour).

type IntrospectStore

type IntrospectStore interface {
	SetIntrospectionFromToken(ctx context.Context, resp *protocol.IntrospectionResponse, tokenID, subject, clientID string) error
}

IntrospectStore is required by the Introspection plugin.

type JARMSigner

type JARMSigner interface {
	// SignAuthResponse signs the authorization response parameters
	// as a JWT. The ctx is used to derive the issuer URL. The params
	// map contains the response fields (code, state, etc.). The
	// clientID is used for audience validation. The signingAlg is the
	// client's preferred signing algorithm (e.g. "PS256" for FAPI);
	// if empty, the server's default is used.
	// Returns the compact JWT string.
	SignAuthResponse(ctx context.Context, params map[string]string, clientID string, signingAlg string) (string, error)
}

JARMSigner is optionally implemented by a plugin to provide JARM (JWT Secured Authorization Response Mode) support per RFC 9101.

When implemented, the authorization response is signed as a JWT and returned using the requested JARM response mode (query.jwt, fragment.jwt, or form_post.jwt).

type JWTAccessTokenSigningClient added in v2.1.0

type JWTAccessTokenSigningClient interface {
	AccessTokenSigningAlgorithm() string
}

JWTAccessTokenSigningClient is optionally implemented by Client to specify the signing algorithm for JWT-format access tokens.

This is the kexcore-oidc equivalent of Casdoor's "Token签名算法" setting for access tokens in JWT format.

When not implemented, the signing algorithm is determined by the server's KeyStore (typically RS256 or ES256).

The returned algorithm MUST be one of the server's supported algorithms. If the algorithm is not supported, the token plugin falls back to the server's default.

type JWTAccessTokenStore added in v2.1.0

type JWTAccessTokenStore interface {
	// CreateJWTAccessToken creates a JWT-format access token.
	// The returned tokenID is used for introspection/revocation lookups.
	// The returned token string is the compact JWT serialization.
	CreateJWTAccessToken(ctx context.Context, req TokenRequest, cnf map[string]any, lifetime time.Duration) (tokenID, token string, expiration time.Time, err error)
}

JWTAccessTokenStore is optionally implemented by TokenStore to support JWT-format access tokens (RFC 9068).

When a client requests JWT access tokens (via AccessTokenFormatClient), the token plugin uses this interface instead of the default opaque token flow.

type JWTProfileStore

type JWTProfileStore interface {
	ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error)
}

JWTProfileStore is required by the JWT Profile grant.

type Key

type Key = protocol.Key

Key is an alias for protocol.Key. GM/T keys should additionally satisfy protocol.GMJWKProvider for JWKS publication of SM2/SM9 keys that jwx cannot represent.

type KeyStore

type KeyStore interface {
	KeySet(ctx context.Context) ([]protocol.Key, error)
	SignatureAlgorithms(ctx context.Context) ([]string, error)
	SigningKey(ctx context.Context) (SigningKey, error)
}

KeyStore provides cryptographic key access. It extends protocol.KeyStore with SigningKey for OP-side token signing.

Implementation notes:

  • KeySet: return all public keys as JWKS (for /jwks endpoint and token verification). Each key must implement protocol.Key (jwk.Key with KeyID and Algorithm).
  • SignatureAlgorithms: return the list of supported signing algorithms (e.g. ["RS256", "ES256"]). Used by discovery document.
  • SigningKey: return the current signing key + algorithm for token signing. The SDK uses this to sign ID tokens and access tokens (if JWT). Rotate keys by changing what this returns — old keys stay in KeySet for verification.

Security considerations:

  • Key rotation: when rotating signing keys, KeySet MUST continue to return old keys until all tokens signed with them have expired. Removing old keys prematurely will cause signature verification failures for existing tokens.
  • KeySet should return keys with explicit "kid" and "alg" to prevent algorithm confusion attacks (RFC 7517 §4.4).

type MiddlewareProvider

type MiddlewareProvider interface {
	Plugin

	// Middleware wraps the given handler with plugin-specific middleware.
	Middleware(next http.Handler) http.Handler
}

MiddlewareProvider is optionally implemented by plugins that provide HTTP middleware to be applied to all requests. The engine auto-detects this interface during Build() and applies the middleware to the pipeline.

Example: DPoP plugin provides middleware to parse DPoP proofs from request headers and store them in the request context.

type NotificationEndpointProvider

type NotificationEndpointProvider interface {
	// NotificationEndpoint returns the client's backchannel notification URL.
	NotificationEndpoint() string
}

NotificationEndpointProvider is an optional interface for clients that register a notification endpoint for CIBA ping delivery mode (CIBA Core 1.0 §10). When implemented, the SDK validates the endpoint URL for SSRF protection before dispatching notification callbacks.

type PARLifetimeClient

type PARLifetimeClient interface {
	PARLifetime() time.Duration
}

PARLifetimeClient is optionally implemented by Client to override the pushed authorization request URI lifetime. When not implemented, the plugin uses its configured default (typically 90 seconds).

type PARStore

type PARStore interface {
	StorePushedAuthRequest(ctx context.Context, clientID string, req *protocol.AuthRequest, lifetime time.Duration) (requestURI string, err error)
	GetPushedAuthRequest(ctx context.Context, requestURI string) (*protocol.AuthRequest, error)
}

PARStore is required by the Pushed Authorization Request plugin.

type PairwiseTransformer

type PairwiseTransformer interface {
	// Transform converts a real subject into a pairwise subject for a given client.
	// The same (clientID, subject) pair must always produce the same result.
	Transform(clientID, subject string) string

	// IsPairwiseClient returns true if the client uses pairwise subject identifiers.
	IsPairwiseClient(clientID string) bool
}

PairwiseTransformer is an optional interface that transforms subjects into pairwise identifiers (OIDC Core §8.1). If the storage implements this interface, the token plugin will automatically apply pairwise transformation when creating tokens for clients that require it.

type Plugin

type Plugin interface {
	// Name returns the plugin's unique identifier.
	// Used for logging and conflict detection.
	Name() string

	// Register installs the plugin's HTTP handlers on the given router.
	// The plugin has full control over routing and handler behavior.
	Register(r chi.Router)
}

Plugin is the fundamental unit of a StormEngine server. Each plugin owns its routes, request parsing, business logic, and error handling. The Engine does not interpret OIDC semantics.

Plugin.Name() must be unique across all registered plugins.

type PluginCategory

type PluginCategory int

PluginCategory defines the lifecycle and registration behavior of a plugin.

const (
	// CategoryCore plugins are always registered and cannot be disabled.
	// They form the minimum viable OIDC server (authorization, token, keys, discovery).
	CategoryCore PluginCategory = iota

	// CategoryStandard plugins are registered by default when their Storage
	// dependencies are satisfied. Users can disable them via Disable().
	CategoryStandard

	// CategoryOptional plugins are only registered when explicitly enabled
	// by the user via WithPlugin() or specific option functions.
	CategoryOptional
)

type PluginContext

type PluginContext struct {
	Storage         Storage
	Crypto          UniCrypto // may be nil if not configured
	Decoder         *protocol.Decoder
	EnableImplicit  bool // enable implicit/hybrid flows (disabled by default per OAuth 2.1)
	AllowPlainPKCE  bool // allow plain code_challenge_method (disabled by default per OAuth 2.1)
	AllowPrivateIPs bool // WARNING: disables SSRF protection in DCR (jwks_uri, sector_identifier_uri).
	// Only for testing with private-network conformance suites.
	// NEVER enable in production — use network-level controls instead.
	SkipTLSCertVerify bool // WARNING: disables TLS certificate verification on outbound HTTP requests.
	// Only for testing with self-signed certificates.
	// NEVER enable in production.
	RequireDPoP      bool                     // FAPI 2.0: require DPoP proof for all token requests
	RequireMtls      bool                     // FAPI 2.0: require mTLS client certificate for all token requests
	PARLifetime      time.Duration            // PAR request_uri lifetime (default: 0 means use plugin default, usually 90s)
	IssuerFn         shared.IssuerFromRequest // issuer URL function
	EndpointResolver shared.EndpointResolver  // endpoint URL resolver (optional, defaults to DefaultEndpointResolver)
	Tracer           trace.Tracer             // otel tracer for plugins
}

PluginContext provides dependencies to plugin factories. It wraps Storage with additional engine-level services.

type PluginFactory

type PluginFactory func(ctx *PluginContext) Plugin

PluginFactory creates a plugin from a PluginContext. Used by WithPlugin and RegisterPlugin for deferred plugin construction.

type PostLogoutRedirectURIClient added in v2.1.0

type PostLogoutRedirectURIClient interface {
	PostLogoutRedirectURIs() []string
}

PostLogoutRedirectURIClient is optionally implemented by Client to define the allowed post-logout redirect URIs (OIDC RP-Initiated Logout §2.1).

When the EndSession plugin receives a post_logout_redirect_uri parameter, it validates against this list. If the client does not implement this interface, post-logout redirect is not allowed for that client.

type RateLimitClient

type RateLimitClient interface {
	MaxTokenRequests() int
	TokenRequestWindow() time.Duration
}

RateLimitClient is optionally implemented by Client to enforce per-client rate limiting on the token endpoint. When implemented, the token plugin checks the client's request rate before processing.

MaxRequests: maximum number of token requests allowed within Window. Window: the sliding window duration (e.g. 1*time.Minute). If Window is zero, a default of 1 minute is used. If MaxRequests is zero or negative, no per-client rate limiting is applied.

type RateLimiter

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

RateLimiter provides a simple in-memory token bucket rate limiter for production IAM deployments. It implements http.Handler middleware and can be applied to specific routes or the entire engine.

For distributed deployments, replace with a Redis-backed limiter (e.g. go-redis/redis_rate) by implementing the MiddlewareProvider interface.

func NewRateLimiter

func NewRateLimiter(rate int, window time.Duration) *RateLimiter

NewRateLimiter creates a rate limiter that allows `rate` requests per `window` per client IP. Expired entries are cleaned up every `window`.

Example:

limiter := storm.NewRateLimiter(100, time.Minute) // 100 req/min per IP
engine.Use(limiter.Middleware)

func (*RateLimiter) Allow

func (rl *RateLimiter) Allow(key string) bool

Allow checks if a request from the given key is allowed. Returns true if the request is within the rate limit.

func (*RateLimiter) Middleware

func (rl *RateLimiter) Middleware(next http.Handler) http.Handler

Middleware returns an http.Handler middleware that rate-limits by client IP.

func (*RateLimiter) Stop

func (rl *RateLimiter) Stop()

Stop terminates the background cleanup goroutine.

type RefreshTokenLifetimeClient added in v2.1.0

type RefreshTokenLifetimeClient interface {
	RefreshTokenLifetime() time.Duration
}

RefreshTokenLifetimeClient is optionally implemented by Client to control the lifetime of issued refresh tokens.

This is the kexcore-oidc equivalent of Casdoor's "Refresh Token过期" setting and Keycloak's "Client Session Max" (refresh token is tied to session lifetime).

When not implemented, the refresh token lifetime is determined by the storage layer (typically 24h-30d depending on the implementation).

type RefreshTokenRequest

type RefreshTokenRequest interface {
	TokenRequest
	GetAMR() []string
	GetAuthTime() time.Time
	GetCodeChallenge() *protocol.CodeChallenge
	GetNonce() string
	GetID() string
	SetCurrentScopes(scopes []string)
	// GetDPoPJKT returns the DPoP JWK thumbprint bound to this refresh token
	// (RFC 9449 §7.2). Returns empty string if the refresh token is not
	// DPoP-bound.
	GetDPoPJKT() string
}

RefreshTokenRequest extends TokenRequest for refresh token operations.

type RegistrationRequest

type RegistrationRequest struct {
	ApplicationType              string          `json:"application_type"`
	ClientName                   string          `json:"client_name"`
	ClientURI                    string          `json:"client_uri"`
	LogoURI                      string          `json:"logo_uri"`
	RedirectURIs                 []string        `json:"redirect_uris"`
	ResponseTypes                []string        `json:"response_types"`
	GrantTypes                   []string        `json:"grant_types"`
	TokenEndpointAuthMethod      string          `json:"token_endpoint_auth_method"`
	Scope                        string          `json:"scope"`
	Contacts                     []string        `json:"contacts"`
	JWKSURI                      string          `json:"jwks_uri"`
	JWKS                         json.RawMessage `json:"jwks"`
	PolicyURI                    string          `json:"policy_uri"`
	TOSURI                       string          `json:"tos_uri"`
	SoftwareID                   string          `json:"software_id"`
	SoftwareVersion              string          `json:"software_version"`
	PostLogoutRedirectURIs       []string        `json:"post_logout_redirect_uris"`
	BackChannelLogoutURI         string          `json:"backchannel_logout_uri"`
	SectorIdentifierURI          string          `json:"sector_identifier_uri,omitempty"`
	InitiateLoginURI             string          `json:"initiate_login_uri"`
	IDTokenSignedResponseAlg     string          `json:"id_token_signed_response_alg,omitempty"`
	IDTokenEncryptedResponseAlg  string          `json:"id_token_encrypted_response_alg,omitempty"`
	IDTokenEncryptedResponseEnc  string          `json:"id_token_encrypted_response_enc,omitempty"`
	UserInfoSignedResponseAlg    string          `json:"userinfo_signed_response_alg,omitempty"`
	UserInfoEncryptedResponseAlg string          `json:"userinfo_encrypted_response_alg,omitempty"`
	UserInfoEncryptedResponseEnc string          `json:"userinfo_encrypted_response_enc,omitempty"`
	RequestObjectSigningAlg      string          `json:"request_object_signing_alg,omitempty"`
	RequireDPoP                  bool            `json:"require_dpop,omitempty"`
	RequireMtls                  bool            `json:"require_mtls,omitempty"`
}

RegistrationRequest represents a dynamic client registration request.

type ResourceIndicator

type ResourceIndicator interface {
	GetResources() []string
}

ResourceIndicator is optionally implemented by AuthRequest to expose the resource indicator values from the authorization request (RFC 8707). When present, the Token plugin merges these into the access token's audience.

type RevocationStore

type RevocationStore interface {
	RevokeToken(ctx context.Context, tokenOrTokenID, userID, clientID string) *protocol.Error
	GetRefreshTokenInfo(ctx context.Context, clientID, token string) (userID, tokenID string, err error)
}

RevocationStore is required by the Revocation plugin.

Security considerations:

  • RevokeToken: when revoking a refresh token, you MUST also revoke all associated access tokens (RFC 7009 §2.1). Implementations should perform cascade revocation within a single transaction to avoid partial revocation states.
  • GetRefreshTokenInfo: the returned tokenID should be the same identifier used in TokenStore, so that Revocation can invalidate all related tokens atomically.

type ScopeValidationClient

type ScopeValidationClient interface {
	Client
	// StrictScopeValidation returns true to reject unsupported scopes with
	// an error, or false (or not implemented) to silently strip them.
	StrictScopeValidation() bool
}

ScopeValidationClient is an optional interface that clients may implement to control whether scope validation is strict (error on unsupported scopes) or lenient (silently strip unsupported scopes).

When not implemented, the default behavior is lenient (strip).

type ScopeWhitelistClient added in v2.1.0

type ScopeWhitelistClient interface {
	// AllowedScopes returns the complete list of scopes this client is
	// allowed to request. Scopes not in this list will be filtered out.
	AllowedScopes() []string
}

ScopeWhitelistClient is optionally implemented by Client to define an explicit whitelist of allowed scopes.

This is the kexcore-oidc equivalent of Zitadel's IsScopeAllowed. When implemented, any scope requested by the client that is NOT in this whitelist will be silently dropped (not rejected with an error).

When not implemented, all requested scopes are accepted (subject to server-level scope validation).

type SessionStore

type SessionStore interface {
	TerminateSession(ctx context.Context, userID, clientID string) error
}

SessionStore is required by the EndSession plugin.

type Signer

type Signer interface {
	// Sign signs the payload and returns the compact JWS serialization.
	Sign(ctx context.Context, keyID string, payload []byte) (string, error)
}

Signer provides JWT signing capability. This is a simpler alternative to UniCrypto for plugins that only need signing.

type SigningKey

type SigningKey interface {
	ID() string
	Algorithm() string
	Key() jwk.Key
}

SigningKey represents a key used for token signing.

type SigningKeyByAlgProvider

type SigningKeyByAlgProvider interface {
	SigningKeyByAlg(ctx context.Context, alg string) (SigningKey, error)
}

SigningKeyByAlgProvider is an optional interface that a KeyStore can implement to support retrieving a signing key by algorithm. This enables per-client ID token signing algorithms.

type Storage

type Storage interface {
	// ClientStore provides client lookup and credential verification.
	// Required by: authorization, token, userinfo, DCR, device, PAR plugins.
	ClientStore

	// KeyStore provides JWKS and signing key access.
	// Required by: token, keys, authorization (Request Object verification) plugins.
	KeyStore

	// Health is used by the /ready probe.
	// Return nil when the storage backend is reachable.
	Health(ctx context.Context) error
}

Storage is the minimal storage contract required by StormEngine. Users implement this interface and pass it to Engine.

Engine automatically detects which optional capability interfaces the storage implements, and each plugin consumes only the interfaces it needs. This eliminates the giant monolithic Storage interface.

At minimum, your Storage must embed ClientStore and KeyStore, and provide a Health() method. All other interfaces are discovered via type assertion — implement them to enable additional plugins.

type TokenCNFLookup

type TokenCNFLookup interface {
	TokenCNF(ctx context.Context, tokenID string) (map[string]any, error)
}

TokenCNFLookup is an optional extension that allows querying the stored cnf (confirmation) claim for a token. Used by the UserInfo and Introspection endpoints to verify sender-constrained tokens.

If your storage implements TokenCNFStore, you should also implement TokenCNFLookup so that protected resource endpoints can verify the binding.

type TokenCNFStore

type TokenCNFStore interface {
	SetTokenCNF(ctx context.Context, tokenID string, cnf map[string]any) error
}

TokenCNFStore is an optional extension of TokenStore that supports storing the cnf (confirmation) claim for certificate-bound or DPoP-bound access tokens (RFC 8705 §3.1, RFC 9449 §7.1).

type TokenClaimsRestrictor added in v2.1.0

type TokenClaimsRestrictor interface {
	// RestrictIDTokenScopes filters the scopes that will be included
	// in the ID Token claims. Return a subset of the requested scopes.
	RestrictIDTokenScopes(scopes []string) []string

	// RestrictAccessTokenScopes filters the scopes that will be included
	// in the Access Token claims. Return a subset of the requested scopes.
	RestrictAccessTokenScopes(scopes []string) []string
}

TokenClaimsRestrictor is optionally implemented by Client to control which scopes are allowed in ID Token and Access Token responses.

This is the kexcore-oidc equivalent of Zitadel's RestrictAdditionalIdTokenScopes / RestrictAdditionalAccessTokenScopes and Keycloak's Client Scope restrictions.

Use cases:

  • High-security clients: only allow minimal scopes in tokens
  • Public clients: restrict to read-only scopes
  • Service-to-service: allow broad scopes

When not implemented, all requested scopes are included (subject to normal scope validation).

type TokenClientProvider

type TokenClientProvider interface {
	TokenClientID(ctx context.Context, tokenID string) (string, error)
}

TokenClientProvider is optionally implemented by storage to return the client_id that a token was issued to. Used by the UserInfo endpoint to populate the "aud" claim in JWT responses (OIDC Core §5.3.2).

type TokenExchangeExternalVerifierStorage added in v2.1.0

type TokenExchangeExternalVerifierStorage interface {
	// VerifyExchangeSubjectToken verifies an external subject token.
	// Returns the token identifier (or the token itself), the subject, and
	// optional private claims extracted from the token.
	VerifyExchangeSubjectToken(ctx context.Context, token string, tokenType protocol.TokenType) (tokenIDOrToken, subject string, claims map[string]any, err error)

	// VerifyExchangeActorToken verifies an external actor token.
	// Returns the token identifier (or the token itself), the actor subject,
	// and optional private claims extracted from the token.
	VerifyExchangeActorToken(ctx context.Context, token string, tokenType protocol.TokenType) (tokenIDOrToken, actor string, claims map[string]any, err error)
}

TokenExchangeExternalVerifierStorage is optionally implemented by storage to verify third-party (external) tokens during Token Exchange (RFC 8693 §2.1).

When the subject_token or actor_token is not a token issued by this server (i.e., not an opaque access token, refresh token, or ID token), the SDK falls back to this interface for verification. This enables scenarios like:

  • Exchanging a SAML assertion for an access token
  • Exchanging a third-party OIDC ID token for a local token
  • Exchanging a JWT from an external identity provider

When not implemented, only tokens issued by this server are accepted.

type TokenExchangeRequest

type TokenExchangeRequest interface {
	TokenRequest
	GetRequestedTokenType() protocol.TokenType
	GetSubjectTokenType() protocol.TokenType
	GetActorTokenType() protocol.TokenType
	// GetActor returns the actor's subject identifier (RFC 8693 §2.1).
	// Empty if no actor_token was provided.
	GetActor() string
	// GetActorTokenIDOrToken returns the actor token's storage ID or the raw token.
	GetActorTokenIDOrToken() string
	// GetSubjectTokenIDOrToken returns the subject token's storage ID or the raw token.
	GetSubjectTokenIDOrToken() string
	// GetSubjectTokenClaims returns private claims extracted from the subject token.
	// May be nil for opaque tokens.
	GetSubjectTokenClaims() map[string]any
	// GetActorTokenClaims returns private claims extracted from the actor token.
	// May be nil if no actor_token was provided.
	GetActorTokenClaims() map[string]any
	SetCurrentScopes(scopes []string)
	SetRequestedTokenType(tokenType protocol.TokenType)
}

TokenExchangeRequest represents a validated token exchange request.

Storage implementations receive this in CreateAccessToken (as TokenRequest). Type-assert to TokenExchangeRequest to detect token exchange and access exchange-specific fields (actor, claims, token types).

type TokenExchangeStore

type TokenExchangeStore interface {
	ValidateTokenExchangeRequest(ctx context.Context, req TokenExchangeRequest) error
	CreateTokenExchangeRequest(ctx context.Context, req TokenExchangeRequest) error
}

TokenExchangeStore is required by the Token Exchange plugin.

type TokenRequest

type TokenRequest interface {
	GetSubject() string
	GetAudience() []string
	GetClientID() string
	GetScopes() []string
}

TokenRequest is the common interface for all token creation requests.

type TokenStore

type TokenStore interface {
	// CreateAccessToken creates an access token. The cnf map contains the
	// token's confirmation claim (e.g. jkt for DPoP, x5t#S256 for mTLS).
	// It may be nil if the token is not sender-constrained.
	CreateAccessToken(ctx context.Context, req TokenRequest, cnf map[string]any) (tokenID string, expiration time.Time, err error)

	// CreateAccessAndRefreshTokens creates an access token and a refresh token.
	// The cnf map contains the access token's confirmation claim and is also
	// used to bind the refresh token (e.g. RFC 9449 §7.2 DPoP binding).
	// currentRefreshToken is non-empty when rotating an existing refresh token.
	CreateAccessAndRefreshTokens(ctx context.Context, req TokenRequest, currentRefreshToken string, cnf map[string]any) (accessTokenID, newRefreshToken string, expiration time.Time, err error)

	TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (RefreshTokenRequest, error)
}

TokenStore is required by the Token, UserInfo, Introspection, and Revocation plugins. It manages access token and refresh token creation and lookup.

Implementation notes:

  • CreateAccessToken: store the token with subject, clientID, scopes, audience. Return a unique tokenID (opaque string, used as key for other stores) and expiration time.
  • CreateAccessAndRefreshTokens: same as CreateAccessToken but also issue a refresh token. currentRefreshToken is empty for first issuance; non-empty when rotating (delete old RT first).
  • TokenRequestByRefreshToken: look up the original token request by refresh token string. The returned RefreshTokenRequest carries the original subject, scopes, audience, etc.

The tokenID you return is the key used by TokenCNFStore, IntrospectStore, and UserinfoStore to look up token metadata. It can be a UUID, database primary key, or any opaque string — the SDK never inspects it.

type UniCrypto

type UniCrypto interface {
	// --- Hash ---
	// Hash computes the hash of data using the algorithm identified by sigAlgorithm.
	// The sigAlgorithm parameter follows JWA naming conventions:
	//   - "RS256", "ES256", "PS256" → SHA-256
	//   - "RS384", "ES384", "PS384" → SHA-384
	//   - "RS512", "ES512", "PS512", "EdDSA" → SHA-512
	//   - "SGD_SM3_SM2", "SGD_SM3_SM9" → SM3
	// Returns the raw hash bytes (not base64 encoded).
	Hash(ctx context.Context, sigAlgorithm string, data []byte) ([]byte, error)

	// --- Signing ---
	// Sign signs the payload using the algorithm associated with the given key ID.
	// Returns the compact JWS serialization.
	Sign(ctx context.Context, keyID string, payload []byte) (string, error)

	// --- Symmetric Encryption ---
	// Encrypt encrypts plaintext using the configured algorithm.
	// For standard: AES-GCM. For GM: SM4-GCM.
	Encrypt(ctx context.Context, plaintext []byte) ([]byte, error)

	// Decrypt decrypts ciphertext using the configured algorithm.
	Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error)

	// --- Algorithm Query ---
	// AlgorithmSuite returns the current algorithm suite identifier.
	// Examples: "RSA+SHA256+AES", "SM2+SM3+SM4", "ECDSA+SHA256+AES"
	AlgorithmSuite() string
}

UniCrypto provides a unified cryptographic interface for all algorithm families.

This interface abstracts away the differences between standard algorithms (RSA, ECDSA, AES, SHA) and Chinese national standards (SM2, SM3, SM4). Implementations can be backed by software libraries or hardware (HSM/KMS).

Usage:

crypto.Hash(ctx, "RS256", data)       // SHA-256
crypto.Hash(ctx, "SGD_SM3_SM2", data) // SM3
crypto.Encrypt(ctx, plaintext)         // default encryption
crypto.Sign(ctx, keyID, payload)       // sign with algorithm from key

type UserInfoResponseAlgProvider

type UserInfoResponseAlgProvider interface {
	UserInfoResponseAlg(ctx context.Context, clientID string) (string, error)
}

UserInfoResponseAlgProvider is an optional interface that returns the client's registered userinfo_signed_response_alg value. When the storage implements this interface and returns a non-empty algorithm, the UserInfo endpoint MUST return a signed JWT (OIDC Core §5.3.2).

type UserinfoFromRequestProvider added in v2.1.0

type UserinfoFromRequestProvider interface {
	SetUserinfoFromRequest(ctx context.Context, userinfo *protocol.UserInfo, tokenID, subject, origin string, scopes []string) error
}

UserinfoFromRequestProvider is optionally implemented by UserinfoStore to allow per-request customization of userinfo claims.

This is the kexcore-oidc equivalent of Zitadel's CanSetUserinfoFromRequest. The request parameter carries the full token request context (scopes, audience, client info) which allows dynamic claims based on the specific authorization context.

When implemented, the UserInfo plugin calls this instead of SetUserinfoFromToken for requests where richer context is needed.

type UserinfoStore

type UserinfoStore interface {
	SetUserinfoFromToken(ctx context.Context, userinfo *protocol.UserInfo, tokenID, subject, origin string) error
}

UserinfoStore is required by the UserInfo plugin (OIDC Core §5.3).

Implementation notes:

  • SetUserinfoFromToken receives a tokenID (from TokenStore) and the subject claim. Look up the user by subject, then populate the UserInfo fields based on the token's granted scopes. If you want scope-based filtering, call protocol.UserInfo.FilterByScopes before returning; otherwise all claims set on the UserInfo will be included in the response.
  • origin is the HTTP Origin header value (for CORS), may be empty.
  • Set the UserInfo.Subject field — this is enforced by the plugin.

The SDK calls this after validating the access token. You do NOT need to verify the token — just populate the response.

Directories

Path Synopsis
plugins
authorization
Package authorization implements the OIDC Authorization endpoint plugin.
Package authorization implements the OIDC Authorization endpoint plugin.
backchannel
Package backchannel implements the OIDC Back-Channel Logout plugin.
Package backchannel implements the OIDC Back-Channel Logout plugin.
ciba
Package ciba implements the OpenID Connect Client-Initiated Backchannel Authentication (CIBA) plugin.
Package ciba implements the OpenID Connect Client-Initiated Backchannel Authentication (CIBA) plugin.
dcr
Package dcr implements the OAuth 2.0 Dynamic Client Registration plugin.
Package dcr implements the OAuth 2.0 Dynamic Client Registration plugin.
device
Package device implements the OAuth 2.0 Device Authorization Grant plugin.
Package device implements the OAuth 2.0 Device Authorization Grant plugin.
discovery
Package discovery implements the OIDC Discovery capability contributor plugin.
Package discovery implements the OIDC Discovery capability contributor plugin.
dpop
Package dpop implements Demonstrating Proof-of-Possession (DPoP) at the application layer (RFC 9449).
Package dpop implements Demonstrating Proof-of-Possession (DPoP) at the application layer (RFC 9449).
endsession
Package endsession implements the OIDC RP-Initiated Logout endpoint plugin.
Package endsession implements the OIDC RP-Initiated Logout endpoint plugin.
introspection
Package introspection implements the OAuth 2.0 Token Introspection endpoint plugin.
Package introspection implements the OAuth 2.0 Token Introspection endpoint plugin.
jarm
Package jarm implements JWT Secured Authorization Response Mode (RFC 9101).
Package jarm implements JWT Secured Authorization Response Mode (RFC 9101).
keys
Package keys implements the JWKS (JSON Web Key Set) endpoint plugin.
Package keys implements the JWKS (JSON Web Key Set) endpoint plugin.
mtls
Package mtls implements OAuth 2.0 Mutual-TLS client authentication and certificate-bound access tokens (RFC 8705).
Package mtls implements OAuth 2.0 Mutual-TLS client authentication and certificate-bound access tokens (RFC 8705).
pairwise
Package pairwise implements Pairwise Subject Identifiers (OIDC Core §8.1).
Package pairwise implements Pairwise Subject Identifiers (OIDC Core §8.1).
par
Package par implements the OAuth 2.0 Pushed Authorization Requests plugin.
Package par implements the OAuth 2.0 Pushed Authorization Requests plugin.
revocation
Package revocation implements the OAuth 2.0 Token Revocation endpoint plugin.
Package revocation implements the OAuth 2.0 Token Revocation endpoint plugin.
token
Package token implements the OIDC Token endpoint plugin.
Package token implements the OIDC Token endpoint plugin.
userinfo
Package userinfo implements the OIDC UserInfo endpoint plugin.
Package userinfo implements the OIDC UserInfo endpoint plugin.
webfinger
Package webfinger implements the WebFinger endpoint (RFC 7033).
Package webfinger implements the WebFinger endpoint (RFC 7033).
Package shared provides cross-cutting concerns used by all StormEngine plugins.
Package shared provides cross-cutting concerns used by all StormEngine plugins.

Jump to

Keyboard shortcuts

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