admin

package
v0.0.81 Latest Latest
Warning

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

Go to latest
Published: May 7, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrAdminUnauthorized = fmt.Errorf("admin authentication required")
	ErrAdminForbidden    = fmt.Errorf("admin access denied")
)

Common errors for admin auth

View Source
var ErrAppNotFound = errors.New("app registration not found")

ErrAppNotFound is returned by AppRegistrationStore.GetApp and DeleteApp when the requested client_id does not exist.

View Source
var ErrInvalidClientMetadata = errors.New("invalid client metadata")

ErrInvalidClientMetadata signals that a management request was authenticated successfully but the request body fails RFC 7591 / 7592 client-metadata validation. HTTP wrappers map this to 400 Bad Request. Distinct from ErrUnauthorized because, by the time we get here, the caller has already proven possession of the registration access token — refusing to distinguish would be needlessly cryptic.

View Source
var ErrUnauthorized = errors.New("unauthorized registration management request")

ErrUnauthorized is the single failure mode returned by ClientRegistrationManager for any authentication problem on the management protocol. The uniform error is intentional: distinguishing "unknown client_id" from "wrong token" would turn /apps/dcr/{client_id} into a probe for valid identifiers.

Functions

func MintResourceToken

func MintResourceToken(userID, appClientID, appSecret string, quota AppQuota, scopes []string, authzDetails []core.AuthorizationDetail) (string, error)

MintResourceToken creates a resource-scoped JWT for a user on behalf of a registered App, signed with the app's shared secret (HS256). This is the backwards-compatible API.

func MintResourceTokenWithKey

func MintResourceTokenWithKey(userID, appClientID string, signingKey any, quota AppQuota, scopes []string, authzDetails []core.AuthorizationDetail) (string, error)

MintResourceTokenWithKey creates a resource-scoped JWT signed with the provided key. The signing algorithm is auto-detected from the key type:

  • []byte → HS256
  • *rsa.PrivateKey → RS256
  • *ecdsa.PrivateKey → ES256

Types

type APIKeyAuth

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

APIKeyAuth authenticates requests using a shared API key passed in the X-Admin-Key header.

func NewAPIKeyAuth

func NewAPIKeyAuth(key string) *APIKeyAuth

NewAPIKeyAuth creates an AdminAuth that validates the X-Admin-Key header.

func (*APIKeyAuth) Authenticate

func (a *APIKeyAuth) Authenticate(r *http.Request) error

type AdminAuth

type AdminAuth interface {
	// Authenticate checks whether the request is authorized.
	// Returns nil if authorized, or an error describing why not.
	Authenticate(r *http.Request) error
}

AdminAuth authenticates admin requests to protected endpoints (e.g., Host registration, key rotation).

type AppQuota

type AppQuota struct {
	MaxRooms   int     `json:"max_rooms,omitempty"`
	MaxMsgRate float64 `json:"max_msg_rate,omitempty"`
}

AppQuota contains per-app quota limits embedded as custom claims in resource-scoped JWTs.

type AppRegistrar

type AppRegistrar struct {
	KeyStore keys.KeyStorage
	Auth     AdminAuth

	// KidStore retains old keys during rotation grace periods so that
	// in-flight tokens signed with the previous key remain verifiable.
	// If nil, rotation replaces the key immediately with no grace period.
	KidStore *keys.KidStore

	// DefaultGracePeriod is the default grace period for key rotation
	// when not specified in the request. Defaults to 24h.
	DefaultGracePeriod time.Duration

	// Store persists app registration metadata. It is the source of truth;
	// the apps map below is a hot-path cache hydrated on construction and
	// updated synchronously on every write.
	Store AppRegistrationStore
	// contains filtered or unexported fields
}

AppRegistrar is an embeddable HTTP handler for App registration CRUD. Mount it on any admin service's mux to let apps register and obtain signing credentials. Create with NewAppRegistrar() (in-memory store) or NewAppRegistrarWithStore.

func NewAppRegistrar added in v0.0.53

func NewAppRegistrar(keyStore keys.KeyStorage, auth AdminAuth) *AppRegistrar

NewAppRegistrar creates an AppRegistrar backed by an in-memory store. Equivalent to NewAppRegistrarWithStore(keyStore, auth, NewInMemoryAppStore()).

For deployments that need registrations to survive restart, use NewAppRegistrarWithStore with a persistent backend (FSAppStore, GORMAppStore).

func NewAppRegistrarWithStore added in v0.0.81

func NewAppRegistrarWithStore(keyStore keys.KeyStorage, auth AdminAuth, store AppRegistrationStore) *AppRegistrar

NewAppRegistrarWithStore creates an AppRegistrar backed by the given store. Existing registrations in the store are loaded into the in-memory cache so that subsequent reads (RLockApps, GET /apps) reflect the persisted state immediately after construction.

If store.ListApps returns an error during hydration, the AppRegistrar is returned with an empty cache; subsequent writes proceed normally. (We do not panic — a transient store error at startup should not crash the host process. The error is intentionally swallowed here since there is no caller-visible context to report it through; callers wanting strict startup semantics should call store.ListApps themselves first.)

func (*AppRegistrar) DeleteRegistration added in v0.0.81

DeleteRegistration implements ClientRegistrationManager (RFC 7592 §2.3). On success it removes the registration from the AppRegistrationStore (and the in-memory cache), and deletes the client's signing key from KeyStore so any tokens already issued under this client_id fail subsequent signature-validation — satisfying the spec requirement that "the authorization server MUST invalidate" all tokens for a deleted client.

Failure ordering mirrors handleDeleteApp: we persist the deletion in the store first; only on success do we drop the cache entry and the KeyStore key. If the store write fails we return early without touching the in-memory cache or the credentials, so the registration remains authoritatively present (deletion can be retried).

ctx is currently unused but threaded through for cancellation / deadline propagation once stores grow async ops.

func (*AppRegistrar) GetRegistration added in v0.0.81

GetRegistration implements ClientRegistrationManager. It returns the RFC 7591 registration for req.ClientID iff req.AccessToken matches the stored registration_access_token. Returns ErrUnauthorized for every failure mode — wrong/missing token, unknown client_id, or a registration that lacks a management token (e.g., a legacy /apps/register entry) — so the management endpoint cannot be used to probe for valid client_ids.

The returned DCRResponse intentionally omits client_secret. RFC 7592 §3 permits but does not require echoing the secret on read; re-emitting symmetric credentials on every read enlarges the disclosure window if the registration access token is ever logged or proxied. Clients that lose the secret can rotate via PUT (#169).

ctx is currently unused but threaded through for cancellation / deadline propagation once stores grow async ops, and for parity with the rest of the manager interface.

func (*AppRegistrar) Handler

func (h *AppRegistrar) Handler() http.Handler

Handler returns an http.Handler for app registration endpoints. Includes both custom OneAuth API, RFC 7591 DCR, and RFC 7592 DCR Management:

POST   /apps/register         — OneAuth custom registration
POST   /apps/dcr              — RFC 7591 Dynamic Client Registration
GET    /apps/dcr/{client_id}  — RFC 7592 DCR Management read (issue #168)
GET    /apps                  — List all apps
GET    /apps/{id}             — Get app metadata
DELETE /apps/{id}             — Delete app
POST   /apps/{id}/rotate      — Rotate secret/key

Routing precedence note: Go's ServeMux uses longest-prefix matching, so the "/apps/dcr/" prefix below wins over the "/apps/" catch-all without further fiddling. The exact-match "/apps/dcr" route handles RFC 7591 registration.

func (*AppRegistrar) RLockApps

func (h *AppRegistrar) RLockApps(fn func(map[string]*AppRegistration))

RLockApps calls fn with a read-locked view of all registered apps.

func (*AppRegistrar) SaveRegistration added in v0.0.81

func (h *AppRegistrar) SaveRegistration(reg *AppRegistration) error

SaveRegistration persists the registration to the store and updates the in-memory cache. Used by handleRegister, DCRHandler, and (in #157) the RFC 7592 management endpoints. If the store write fails, the cache is not updated and the error is returned.

func (*AppRegistrar) UpdateRegistration added in v0.0.81

UpdateRegistration implements ClientRegistrationManager (RFC 7592 §2.2). It performs a full replacement of the editable metadata fields and rotates the registration_access_token; the new token is returned in the response. The old token becomes invalid as soon as SaveRegistration succeeds.

Editable fields (overwritten from req.Metadata on success): ClientName, ClientURI, RedirectURIs, GrantTypes, Scope, AuthorizationDetailsTypes, ClientDomain (derived from ClientURI / ClientName, mirroring DCRHandler's logic).

Locked fields (return ErrInvalidClientMetadata on attempted change):

  • TokenEndpointAuthMethod — would require re-keying; out of scope for #169.

Locked fields (silently retained):

  • SigningAlg, ClientID, CreatedAt, RegistrationClientURI, the key material in KeyStore.

req.Metadata is treated as RFC 7591 client metadata; req.Metadata.JWKS is currently ignored (auth-method changes are rejected, so the JWKS cannot usefully change either).

ctx is currently unused but threaded through for cancellation / deadline propagation once stores grow async ops, and for parity with the rest of the manager interface.

type AppRegistration

type AppRegistration struct {
	ClientID                  string    `json:"client_id"`
	ClientDomain              string    `json:"client_domain"`
	SigningAlg                string    `json:"signing_alg"`
	MaxRooms                  int       `json:"max_rooms,omitempty"`
	MaxMsgRate                float64   `json:"max_msg_rate,omitempty"`
	AuthorizationDetailsTypes []string  `json:"authorization_details_types,omitempty"` // RFC 9396
	CreatedAt                 time.Time `json:"created_at"`
	Revoked                   bool      `json:"revoked"`

	// RFC 7591 / 7592 client metadata (populated by DCR; see #157).
	ClientName              string   `json:"client_name,omitempty"`
	ClientURI               string   `json:"client_uri,omitempty"`
	RedirectURIs            []string `json:"redirect_uris,omitempty"`
	GrantTypes              []string `json:"grant_types,omitempty"`
	Scope                   string   `json:"scope,omitempty"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method,omitempty"`

	// RFC 7592 management credentials. Issued when the management protocol
	// is implemented (#168); empty for legacy registrations.
	RegistrationAccessToken string `json:"registration_access_token,omitempty"`
	RegistrationClientURI   string `json:"registration_client_uri,omitempty"`
}

AppRegistration holds metadata about a registered App.

The DCR / RFC 7592 fields below (ClientName, ClientURI, RedirectURIs, GrantTypes, Scope, TokenEndpointAuthMethod, RegistrationAccessToken, RegistrationClientURI) are persisted starting in issue #165 so that the schema is stable when issue #157 (RFC 7592 Management) populates them. They may be empty for apps registered via the legacy /apps/register endpoint.

type AppRegistrationStore added in v0.0.81

type AppRegistrationStore interface {
	// SaveApp inserts or replaces the registration for app.ClientID.
	SaveApp(app *AppRegistration) error

	// GetApp returns the registration for clientID, or ErrAppNotFound.
	GetApp(clientID string) (*AppRegistration, error)

	// ListApps returns every registration in the store. Order is unspecified.
	ListApps() ([]*AppRegistration, error)

	// DeleteApp removes the registration for clientID. Returns ErrAppNotFound
	// if no such registration exists.
	DeleteApp(clientID string) error
}

AppRegistrationStore persists app registration metadata.

It is the source of truth for registered apps; AppRegistrar holds a hot-path in-memory cache that is hydrated from the store on construction and updated on every write.

Backends: InMemoryAppStore (admin/), FSAppStore (stores/fs/, see issue #166), GORMAppStore (stores/gorm/, see issue #167).

type ClientRegistrationManager added in v0.0.81

type ClientRegistrationManager interface {
	// GetRegistration returns the registration for req.ClientID iff
	// req.AccessToken matches its stored registration_access_token. Any
	// auth failure — wrong token, missing token, unknown client_id — yields
	// ErrUnauthorized so callers (and attackers) cannot distinguish them.
	GetRegistration(ctx context.Context, req *GetRegistrationRequest) (*GetRegistrationResponse, error)

	// UpdateRegistration replaces the metadata for req.ClientID per RFC 7592
	// §2.2 (full replacement, not PATCH-style merge). The request's body-level
	// client_id (req.Metadata.ClientID) MUST equal req.ClientID — mismatch
	// returns ErrInvalidClientMetadata, mapped to HTTP 400 by the wrapper.
	// Auth failures return ErrUnauthorized as with GetRegistration.
	//
	// On success the registration_access_token is rotated (RFC 7592 §2.2
	// recommended) and the new token is returned in the response; the old
	// token is invalid for any subsequent management request.
	//
	// Out of scope for #169: changing token_endpoint_auth_method (which would
	// require re-keying / new secret). Such requests return
	// ErrInvalidClientMetadata. Clients that need to change auth method
	// DELETE and re-register.
	UpdateRegistration(ctx context.Context, req *UpdateRegistrationRequest) (*UpdateRegistrationResponse, error)

	// DeleteRegistration removes the registration for req.ClientID iff
	// req.AccessToken matches the stored registration_access_token, and
	// invalidates the client's signing credentials so already-issued tokens
	// fail subsequent validation (RFC 7592 §2.3 — "the authorization server
	// MUST invalidate" all tokens for the deleted client). The same uniform
	// ErrUnauthorized envelope as the other methods covers every auth failure.
	//
	// Idempotency is intentional but limited: a second DELETE on the same
	// client_id returns ErrUnauthorized (the registration is gone, so the
	// token check fails at lookup) rather than a special "already deleted"
	// error — preserves the no-enumeration guard.
	DeleteRegistration(ctx context.Context, req *DeleteRegistrationRequest) (*DeleteRegistrationResponse, error)
}

ClientRegistrationManager is the transport-agnostic core of OAuth 2.0 Dynamic Client Registration Management (RFC 7592). HTTP handlers in admin/ (and any future gRPC / in-process callers) are thin wrappers around this interface — they parse the request, call into the manager, then format the response. Mirrors the apiauth/ pattern (TokenIssuer / TokenValidator / TokenIntrospector / TokenRevoker — see issue #110).

Method shape — every method follows the convention:

MethodName(ctx context.Context, req *XRequest) (*XResponse, error)

Two-arg / two-return signatures map cleanly to gRPC codegen if we ever generate stubs from these interfaces. The first parameter is plain context.Context for now; a typed library context (carrying stores, loggers, request-scoped deps) can replace it later without changing the shape — that's the point of standardizing on a single ctx parameter rather than expanding the parameter list ad hoc.

#168 ships GetRegistration. #169 ships UpdateRegistration. #170 ships DeleteRegistration — completing the verb trio.

type DCRHandler added in v0.0.62

type DCRHandler struct {
	// KeyStore stores client credentials (same KeyStore as AppRegistrar).
	KeyStore keys.KeyStorage

	// Auth authenticates the caller. Supports both:
	//   - Initial access token via Authorization: Bearer header (RFC 7591 §3)
	//   - X-Admin-Key header (OneAuth custom, backward-compatible)
	// If nil, registration is open (not recommended for production).
	Auth AdminAuth

	// Registrar is the underlying AppRegistrar for metadata storage.
	// If nil, client metadata is not persisted (only KeyStore is used).
	Registrar *AppRegistrar

	// IssuerBaseURL is the public base URL used to construct
	// registration_client_uri values returned in DCR responses
	// (e.g. "https://auth.example.com"). When empty, the URI is
	// built from the incoming request's scheme + Host header — fine
	// for tests but unreliable behind proxies, so production
	// deployments should set this explicitly.
	IssuerBaseURL string
}

DCRHandler implements OAuth 2.0 Dynamic Client Registration (RFC 7591) as a conformance wrapper around AppRegistrar. It accepts standard DCR request format and maps to/from the internal AppRegistrar model.

Automatically mounted by AppRegistrar.Handler() at POST /apps/dcr. Existing /apps/* endpoints continue working unchanged.

On successful registration the response includes a registration access token + management URI (RFC 7592 §3) which the client uses to read, update, or delete its own registration via /apps/dcr/{client_id}.

See: https://www.rfc-editor.org/rfc/rfc7591 See: https://www.rfc-editor.org/rfc/rfc7592

func (*DCRHandler) ServeHTTP added in v0.0.62

func (h *DCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP handles POST /register per RFC 7591.

type DCRManagementHandler added in v0.0.81

type DCRManagementHandler struct {
	// Manager is the transport-agnostic core. Required.
	Manager ClientRegistrationManager
}

DCRManagementHandler is the HTTP transport adapter for the RFC 7592 management protocol — it parses the request, hands off to a ClientRegistrationManager, then formats the response. All protocol / authorization decisions live behind the interface so the same logic is usable from gRPC, in-process callers, and tests without HTTP machinery.

#168 ships GET; #169 ships PUT; #170 ships DELETE — completing the verb trio. Other HTTP methods return 405 with an Allow header advertising the supported set.

See: https://www.rfc-editor.org/rfc/rfc7592

func (*DCRManagementHandler) ServeHTTP added in v0.0.81

func (h *DCRManagementHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP routes /apps/dcr/{client_id} requests by HTTP method.

type DCRRequest added in v0.0.62

type DCRRequest struct {
	// ClientID is required for PUT (RFC 7592 §2.2), ignored for register.
	ClientID string `json:"client_id,omitempty"`

	// Client metadata
	ClientName   string   `json:"client_name,omitempty"`
	ClientURI    string   `json:"client_uri,omitempty"`
	RedirectURIs []string `json:"redirect_uris,omitempty"`
	GrantTypes   []string `json:"grant_types,omitempty"`
	Scope        string   `json:"scope,omitempty"`

	// Authentication method
	TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`

	// RFC 9396 — authorization details types this client intends to use
	AuthorizationDetailsTypes []string `json:"authorization_details_types,omitempty"`

	// Keys — for asymmetric auth methods (private_key_jwt)
	JWKS *utils.JWKSet `json:"jwks,omitempty"`
}

DCRRequest is the RFC 7591 client registration request, also reused as the RFC 7592 §2.2 update request body.

On RFC 7591 registration the ClientID field is unused — the server assigns the value. On RFC 7592 PUT the client MUST include its existing client_id; the HTTP wrapper validates that it matches the URL path before invoking ClientRegistrationManager.UpdateRegistration.

See: https://www.rfc-editor.org/rfc/rfc7591#section-2 See: https://www.rfc-editor.org/rfc/rfc7592#section-2.2

type DCRResponse added in v0.0.62

type DCRResponse struct {
	ClientID                  string   `json:"client_id"`
	ClientSecret              string   `json:"client_secret,omitempty"`
	ClientIDIssuedAt          int64    `json:"client_id_issued_at"`
	ClientSecretExpiresAt     int64    `json:"client_secret_expires_at"`
	ClientName                string   `json:"client_name,omitempty"`
	ClientURI                 string   `json:"client_uri,omitempty"`
	RedirectURIs              []string `json:"redirect_uris,omitempty"`
	GrantTypes                []string `json:"grant_types,omitempty"`
	TokenEndpointAuthMethod   string   `json:"token_endpoint_auth_method,omitempty"`
	Scope                     string   `json:"scope,omitempty"`
	AuthorizationDetailsTypes []string `json:"authorization_details_types,omitempty"` // RFC 9396

	// RFC 7592 §3 — management credentials.
	RegistrationAccessToken string `json:"registration_access_token,omitempty"`
	RegistrationClientURI   string `json:"registration_client_uri,omitempty"`
}

DCRResponse is the RFC 7591 client registration response, extended with the RFC 7592 §3 management credentials (registration_access_token + registration_client_uri) so that registered clients can subsequently call /apps/dcr/{client_id} to read, update, or delete their own registration. See: https://www.rfc-editor.org/rfc/rfc7591#section-3.2.1 See: https://www.rfc-editor.org/rfc/rfc7592#section-3

type DeleteRegistrationRequest added in v0.0.81

type DeleteRegistrationRequest struct {
	// ClientID identifies the registration to remove.
	ClientID string

	// AccessToken is the registration_access_token currently on the
	// registration — must match for the deletion to be authorized.
	AccessToken string
}

DeleteRegistrationRequest is the input to ClientRegistrationManager.DeleteRegistration.

type DeleteRegistrationResponse added in v0.0.81

type DeleteRegistrationResponse struct{}

DeleteRegistrationResponse is intentionally empty today: RFC 7592 §2.3 returns 204 No Content with no body. The struct exists so future forward-compat fields (e.g., a deletion confirmation token) can be added without changing the method signature — same rationale as the other response types.

type GetRegistrationRequest added in v0.0.81

type GetRegistrationRequest struct {
	// ClientID identifies the registration to read.
	ClientID string

	// AccessToken is the registration_access_token issued at registration time
	// (RFC 7592 §3). Must match the value stored on the registration.
	AccessToken string
}

GetRegistrationRequest is the input to ClientRegistrationManager.GetRegistration.

type GetRegistrationResponse added in v0.0.81

type GetRegistrationResponse struct {
	Registration *DCRResponse
}

GetRegistrationResponse wraps the registration metadata. Wrapped (rather than returning *DCRResponse directly) so that future forward-compat fields can be added without changing the method signature — same reason gRPC requires dedicated response messages per method.

type InMemoryAppStore added in v0.0.81

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

InMemoryAppStore is a process-local AppRegistrationStore. State is lost on restart — suitable for tests and dev. Production deployments should use a persistent backend (FS, GORM).

func NewInMemoryAppStore added in v0.0.81

func NewInMemoryAppStore() *InMemoryAppStore

NewInMemoryAppStore returns an empty InMemoryAppStore.

func (*InMemoryAppStore) DeleteApp added in v0.0.81

func (s *InMemoryAppStore) DeleteApp(clientID string) error

func (*InMemoryAppStore) GetApp added in v0.0.81

func (s *InMemoryAppStore) GetApp(clientID string) (*AppRegistration, error)

func (*InMemoryAppStore) ListApps added in v0.0.81

func (s *InMemoryAppStore) ListApps() ([]*AppRegistration, error)

func (*InMemoryAppStore) SaveApp added in v0.0.81

func (s *InMemoryAppStore) SaveApp(app *AppRegistration) error

type NoAuth

type NoAuth struct{}

NoAuth allows all requests. For development/testing only.

func NewNoAuth

func NewNoAuth() *NoAuth

func (*NoAuth) Authenticate

func (a *NoAuth) Authenticate(r *http.Request) error

type UpdateRegistrationRequest added in v0.0.81

type UpdateRegistrationRequest struct {
	// ClientID identifies the registration to update. The wrapper guarantees
	// this matches Metadata.ClientID before invoking the manager.
	ClientID string

	// AccessToken is the registration_access_token issued at registration time
	// (or the rotated value from a previous UpdateRegistration). Must match
	// the value currently stored on the registration.
	AccessToken string

	// Metadata is the RFC 7591 / 7592 client metadata to replace the existing
	// registration with. Treated as a full-replacement payload per
	// RFC 7592 §2.2 — fields omitted from Metadata are cleared on the server.
	Metadata *DCRRequest
}

UpdateRegistrationRequest is the input to ClientRegistrationManager.UpdateRegistration.

type UpdateRegistrationResponse added in v0.0.81

type UpdateRegistrationResponse struct {
	Registration *DCRResponse
}

UpdateRegistrationResponse wraps the post-update registration. Registration includes the rotated registration_access_token, which supersedes the one passed in via the request. Callers MUST persist the new token before discarding the old one.

Jump to

Keyboard shortcuts

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