Help us shape the future of Go! Take the 2026 Go Developer Survey

authserver

package
v0.1.0-alpha.9 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Overview

Package authserver lets a host operate an OAuth 2.0 authorization server and OpenID Connect provider for its own users, on PostgreSQL and SQLite. It implements authorization server profile 1 of docs/authorization-server.md: the authorization code grant with S256 PKCE, rotating refresh tokens in families that replay revokes, RFC 9207 issuer identification, RFC 8707 single-audience opaque access tokens, ID tokens, UserInfo, discovery, JWKS, RFC 7009 revocation and RFC 7662 introspection.

Sigil is the issuer here, not a relying party: this package shares no state with plugin/oauth or plugin/sso. The host registers clients through Clients, mounts Handler at the issuer origin outside Auth.Handler, and serves login and consent pages that continue an authorization through the /authorization/* routes of Auth.Handler. See docs/authserver.md.

Index

Constants

View Source
const (
	AuthorizeOperation   plugin.Operation = "authserver.authorize"
	TokenOperation       plugin.Operation = "authserver.token"
	RefreshOperation     plugin.Operation = "authserver.refresh"
	GrantRevokeOperation plugin.Operation = "authserver.grant_revoke"
	TokenRevokeOperation plugin.Operation = "authserver.token_revoke"

	ClientCreateOperation       plugin.Operation = "authserver.client_create"
	ClientUpdateOperation       plugin.Operation = "authserver.client_update"
	ClientDisableOperation      plugin.Operation = "authserver.client_disable"
	ClientEnableOperation       plugin.Operation = "authserver.client_enable"
	ClientDeleteOperation       plugin.Operation = "authserver.client_delete"
	ClientSecretRotateOperation plugin.Operation = "authserver.client_secret_rotate"
	ClientSecretRevokeOperation plugin.Operation = "authserver.client_secret_revoke"

	ResourceSecretRotateOperation plugin.Operation = "authserver.resource_secret_rotate"
	ResourceSecretRevokeOperation plugin.Operation = "authserver.resource_secret_revoke"

	// Method names events that concern no known client.
	Method sigil.AuditMethod = "authserver"
)

Operations recorded in the audit trail. Authorization, token and revocation events are attributed to the user, with the client as method ("authserver.<client id>"). Client and resource-secret administration is attributed to the host or to an operator.

View Source
const (
	ScopeOpenID        = "openid"
	ScopeEmail         = "email"
	ScopeProfile       = "profile"
	ScopeOfflineAccess = "offline_access"
)

The reserved OpenID Connect scopes. They belong to no resource.

View Source
const (
	RS256 = "RS256"
	ES256 = "ES256"
)

Signing algorithms. RS256 is always available, as OIDC Discovery requires.

View Source
const (
	InteractionLifetime = 10 * time.Minute
	CodeLifetime        = 60 * time.Second
	MaxTokenLifetime    = time.Hour
	// Refresh families: idle 30 days and absolute 90 days by default, at most
	// 365 days absolute (design §6.4).
	DefaultRefreshIdleLifetime = 30 * 24 * time.Hour
	DefaultRefreshLifetime     = 90 * 24 * time.Hour
	MaxRefreshLifetime         = 365 * 24 * time.Hour

	MaxRedirectURIs = 10
	MaxResources    = 32
	MaxScopes       = 64
)

Fixed lifetimes and limits (design §6.4).

View Source
const ID = "authserver"
View Source
const IntrospectOperation plugin.Operation = "authserver.introspect"

IntrospectOperation records refused resource-server authentication only; introspection itself is a read and records nothing.

View Source
const ReasonReplay sigil.AuditReason = "replay"

ReasonReplay marks a second redemption of a consumed authorization code, or a rotated refresh token presented again.

Variables

View Source
var Paths = []string{"/authorize", "/token", "/userinfo", "/revoke", "/introspect", "/jwks",
	"/.well-known/openid-configuration", "/.well-known/oauth-authorization-server"}

Paths are the protocol endpoints Handler serves, relative to the issuer.

Functions

func ClientMethod

func ClientMethod(clientID string) sigil.AuditMethod

ClientMethod names a client on events: "authserver.<client id>".

func Contract

func Contract() plugin.Definition

Contract is the offline declaration for tooling: routes, schema names and migrations are independent of configuration. Its handlers fail closed.

func PruneExpired

func PruneExpired(ctx context.Context, store storage.Store, before time.Time, limit int) (int64, error)

PruneExpired removes at most limit (1..1000) rows of each kind that expired (or, for interactions, access tokens and refresh families, were consumed or revoked) before the cutoff, clamped to database time: interactions, codes, access tokens, refresh families that are revoked or idle- or absolutely expired (with all their tokens), and expired client and resource secrets. The rotated tokens of a live family are kept, so their replay is still detected. It returns the total removed. The audit trail keeps their history. Grants never expire.

Types

type ClaimsFunc

type ClaimsFunc func(tx storage.Tx, user storage.User, scopes []string) (map[string]any, error)

ClaimsFunc returns profile claims for user. It runs inside the issuing transaction under the user's lock, for ID tokens and UserInfo, only when the profile scope was granted. It may return only claims named in Config.ProfileClaims; any other claim fails the request. It must not perform network I/O or retain tx.

type Client

type Client struct {
	ID   string     `json:"id"`
	Type ClientType `json:"type"`
	// Name and the optional URIs are shown on consent screens as registered;
	// Sigil never fetches them.
	Name      string `json:"name"`
	LogoURI   string `json:"logoUri"`
	PolicyURI string `json:"policyUri"`
	TermsURI  string `json:"termsUri"`
	// RedirectURIs are compared byte for byte, query included.
	RedirectURIs []string `json:"redirectUris"`
	// FirstParty clients never show consent. Only mark clients the host
	// itself operates.
	FirstParty bool `json:"firstParty"`
	// NonceInsteadOfPKCE lets a confidential client omit PKCE on an openid
	// request that carries a nonce (RFC 9700 §2.1.1).
	NonceInsteadOfPKCE bool `json:"nonceInsteadOfPkce"`
	// Scopes and Resources bound every request; Scopes may include openid,
	// email and profile. DefaultResource, one of Resources, is the audience
	// of a request that names none.
	Scopes          []string `json:"scopes"`
	Resources       []string `json:"resources"`
	DefaultResource string   `json:"defaultResource"`
	// RefreshTokens allows the refresh_token grant. It requires
	// offline_access in Scopes, and offline_access requires it: a refresh
	// family is created only when the user also granted offline_access.
	RefreshTokens bool `json:"refreshTokens"`
	// Zero lifetimes select the configured defaults.
	AccessTokenLifetime time.Duration `json:"-"`
	IDTokenLifetime     time.Duration `json:"-"`
	RefreshIdleLifetime time.Duration `json:"-"`
	RefreshLifetime     time.Duration `json:"-"`
	// IDTokenAlgorithm is RS256 or, when an ES256 key is active, ES256.
	IDTokenAlgorithm string `json:"idTokenAlgorithm"`
	// Enabled is false after Disable: every authorization and token use of
	// the client fails until Enable.
	Enabled bool `json:"enabled"`
	// Revision advances on every change; pass it to Update.
	Revision  int64          `json:"revision"`
	Secrets   []ClientSecret `json:"secrets"`
	CreatedAt time.Time      `json:"createdAt"`
	UpdatedAt time.Time      `json:"updatedAt"`
}

Client is a registration. It never contains a secret or its digest.

type ClientInput

type ClientInput struct {
	Type                                 ClientType
	Name, LogoURI, PolicyURI, TermsURI   string
	RedirectURIs                         []string
	FirstParty, NonceInsteadOfPKCE       bool
	Scopes, Resources                    []string
	DefaultResource                      string
	AccessTokenLifetime, IDTokenLifetime time.Duration
	IDTokenAlgorithm                     string
	RefreshTokens                        bool
	RefreshIdleLifetime, RefreshLifetime time.Duration
}

ClientInput is a registration to create or replace. Type is ignored by Update.

type ClientSecret

type ClientSecret struct {
	ID        string     `json:"id"`
	CreatedAt time.Time  `json:"createdAt"`
	ExpiresAt *time.Time `json:"expiresAt"`
}

ClientSecret describes a live secret. The value is returned only once.

type ClientService

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

ClientService is a host-only capability. No browser route creates or changes a client; a host administration page must apply its own authorization, for example through sigil.AdminService.Operator, whose administrator is then the actor of the events.

func (ClientService) Create

Create registers a client, enabled. A confidential client's first secret is returned once; a public client gets none.

func (ClientService) Delete

func (s ClientService) Delete(ctx context.Context, id string) error

Delete disables the client, then removes its grants (with their codes, refresh families and tokens) in bounded batches, then the client. If it fails part way the client stays disabled and Delete can be called again.

func (ClientService) Disable

func (s ClientService) Disable(ctx context.Context, id string) error

Disable stops every new authorization, every token use and every refresh of the client at once, on every instance, including introspection. Grants and refresh families are kept, so Enable restores them; codes and interactions in flight never complete.

func (ClientService) Enable

func (s ClientService) Enable(ctx context.Context, id string) error

Enable re-enables a disabled client.

func (ClientService) Get

func (s ClientService) Get(ctx context.Context, id string) (Client, error)

Get returns one client.

func (ClientService) List

func (s ClientService) List(ctx context.Context) ([]Client, error)

List returns every client, oldest first.

func (ClientService) RevokeSecret

func (s ClientService) RevokeSecret(ctx context.Context, id, secretID string) error

RevokeSecret deletes one of a client's secrets at once. An unknown secret is forbidden.

func (ClientService) RotateSecret

func (s ClientService) RotateSecret(ctx context.Context, id string, overlap time.Duration) (IssuedSecret, error)

RotateSecret issues a new secret for a confidential client and makes every existing secret expire overlap from now (zero expires them at once), so the client can move to the new secret. A client holds at most two live secrets: revoke one before rotating again.

func (ClientService) Update

func (s ClientService) Update(ctx context.Context, id string, revision int64, in ClientInput) (Client, error)

Update replaces a client's registration at the expected revision. The type is fixed. Interactions and codes issued under the old revision fail. Existing grants and refresh families keep their scopes and resources, but every token use and refresh is checked against the client's current registration, so withdrawing refresh tokens, a scope or a resource stops the families that need it.

type ClientType

type ClientType string

ClientType is fixed when a client is created.

const (
	// Confidential clients authenticate with a secret Sigil generates.
	Confidential ClientType = "confidential"
	// Public clients hold no secret: PKCE and the exact redirect URI stand in
	// for authentication.
	Public ClientType = "public"
)

type ClientView

type ClientView struct {
	ID         string `json:"id"`
	Name       string `json:"name"`
	LogoURI    string `json:"logoUri"`
	PolicyURI  string `json:"policyUri"`
	TermsURI   string `json:"termsUri"`
	FirstParty bool   `json:"firstParty"`
}

ClientView is exactly the host-registered display metadata.

type Config

type Config struct {
	// Store is the Auth's store. Interactions are created before any user is
	// known, so the plugin opens its own transactions for them.
	Store storage.Store
	// Issuer is the HTTPS origin, with no path, at which the host mounts
	// Handler. It must be sigil.Config.BaseURL's origin or, with a session
	// cookie domain, an origin inside it, so /authorize sees the session.
	// Changing it is a new issuer.
	Issuer string
	// LoginURL and ConsentURL are the host's pages, on one of the Auth's
	// trusted origins, that receive "interaction" and continue an
	// authorization through the /authorization/* routes. ErrorURL receives
	// "error" when a request names an unknown client or an unregistered
	// redirect URI and can therefore not be answered by redirect.
	LoginURL, ConsentURL, ErrorURL string
	Resources                      []Resource
	// SigningKeys are published in JWKS, active or not. ActiveSigningKeys
	// names the one key per algorithm that signs; an RS256 key must be
	// active. Rotate by publishing a new key before activating it (see
	// docs/authserver.md).
	SigningKeys       []SigningKey
	ActiveSigningKeys []string
	// ProfileClaims lists the standard profile claims Claims may return; they
	// are advertised in discovery. Claims is required when it is not empty.
	ProfileClaims []string
	Claims        ClaimsFunc
	// AccessTokenLifetime and IDTokenLifetime default to ten minutes and are
	// at most an hour. Clients may lower them.
	AccessTokenLifetime, IDTokenLifetime time.Duration
	// RefreshIdleLifetime (30 days) is how long a refresh family survives
	// unused; every rotation extends it. RefreshLifetime (90 days) is the
	// family's absolute lifetime, fixed when the code is redeemed. Both are
	// whole seconds from one hour to 365 days, idle no longer than absolute.
	// Clients may override them within the same bounds.
	RefreshIdleLifetime, RefreshLifetime time.Duration
	// MetadataMaxAge (one hour) is the Cache-Control max-age of discovery and
	// JWKS, and so the delay between publishing and activating a key.
	MetadataMaxAge time.Duration
	// TokenPrefix optionally prefixes access and refresh tokens, for secret
	// scanning.
	TokenPrefix string
}

Config is copied and validated by New.

type Grant

type Grant struct {
	Client    ClientView     `json:"client"`
	Scopes    []string       `json:"scopes"`
	Resources []ResourceView `json:"resources"`
	CreatedAt time.Time      `json:"createdAt"`
	UpdatedAt time.Time      `json:"updatedAt"`
}

Grant is a consent the caller gave a client.

type InteractionView

type InteractionView struct {
	Client ClientView `json:"client"`
	// Next is "login" when the session does not satisfy prompt, max_age or
	// id_token_hint, "consent", or "complete" when RedirectTo is set.
	Next               string         `json:"next"`
	RedirectTo         string         `json:"redirectTo,omitempty"`
	Scopes             []string       `json:"scopes"`
	Resources          []ResourceView `json:"resources"`
	GrantedScopes      []string       `json:"grantedScopes"`
	GrantedResources   []string       `json:"grantedResources"`
	LoginHint          string         `json:"loginHint"`
	UILocales          string         `json:"uiLocales"`
	ExpiresAt          time.Time      `json:"expiresAt"`
	ConsentRequiredFor []string       `json:"consentRequiredFor"`
}

InteractionView is what the host's login and consent pages show.

type IssuedSecret

type IssuedSecret struct {
	ID     string
	Secret sigil.Secret
}

IssuedSecret is the only copy of a secret; sigil.Secret redacts it.

type Plugin

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

Plugin binds to exactly one Auth. Methods are safe for concurrent use.

func New

func New(c Config) (*Plugin, error)

New validates and copies configuration. Topology is checked by Bind.

func (*Plugin) AdminNarrowGrant

func (p *Plugin) AdminNarrowGrant(ctx context.Context, userID, clientID string, scopes, resources []string) error

AdminNarrowGrant withdraws part of userID's grant to a client, as an administrator: the grant keeps only scopes and resources, which must be a non-empty subset of what it holds. Live access tokens and refresh families holding anything withdrawn stop validating at once. It records authserver.grant_revoke.

func (*Plugin) AdminRevokeGrants

func (p *Plugin) AdminRevokeGrants(ctx context.Context, userID string) (int64, error)

AdminRevokeGrants revokes every grant userID holds, with their codes, refresh families and tokens, as an administrator (the host, or an Operator context's administrator). It works while the account is suspended, which is how a suspension for compromise is made permanent for delegated access. It records authserver.grant_revoke, even when there was none, and returns how many grants it revoked.

func (*Plugin) Authorize

func (p *Plugin) Authorize(ctx context.Context, token sigil.Secret, req Requirement) (context.Context, TokenInfo, error)

Authorize validates an access token presented to the host's own API against req, under the owner's lock, and returns a context carrying it for WithToken and FromContext. It never establishes a session. An invalid, expired, revoked or foreign-audience token, a disabled client, a revoked or narrowed grant, a changed security epoch and a suspended account are all unauthenticated; a valid token lacking a scope is forbidden.

func (*Plugin) Bind

func (p *Plugin) Bind(a *sigil.Auth) error

Bind attaches the plugin to its Auth and checks the topology: the issuer must receive the session cookie, and the login, consent and error pages must be on the Auth's trusted origins.

func (*Plugin) Clients

func (p *Plugin) Clients() ClientService

Clients returns the host-only client administration capability.

func (*Plugin) Decide

func (p *Plugin) Decide(ctx context.Context, handle string, binding sigil.Secret, approve bool, scopes []string) (string, error)

Decide completes an authorization with the user's decision, in one transaction under the user's lock: the session is revalidated (and must satisfy prompt, max_age and id_token_hint), the browser binding and handle are checked, the interaction is consumed, the client is checked at the same revision, the grant is widened, a single-use code is created and authserver.authorize is recorded. It returns the URL the host page must navigate to, carrying the code, or access_denied when approve is false or the user withheld openid. scopes, when not nil, is the approved subset of the requested scopes; nil approves all of them. Losing a race with another tab, a client change or a session revocation fails the whole transaction. An impersonation session is forbidden.

func (*Plugin) Definition

func (p *Plugin) Definition() plugin.Definition

func (*Plugin) Grants

func (p *Plugin) Grants(ctx context.Context) ([]Grant, error)

Grants lists the caller's grants, oldest first.

func (*Plugin) Handler

func (p *Plugin) Handler() http.Handler

Handler serves the protocol endpoints in Paths at the issuer origin. Mount it outside Auth.Handler, at the root of the issuer origin, for example:

for _, path := range authserver.Paths {
	mux.Handle(path, server.Handler())
}

It applies the Auth's proxy and host policy, never reads the session cookie except on /authorize, never sets one except the authorization's browser binding, and never allows credentialed CORS. The host owns TLS.

func (*Plugin) Interaction

func (p *Plugin) Interaction(ctx context.Context, handle string, binding sigil.Secret) (InteractionView, error)

Interaction describes an authorization for the host's login or consent page. The caller's live session must be the browser's that started it: the binding is the browser-binding cookie. When nothing more is needed, it completes the authorization as Decide would and returns RedirectTo, the URL the page must navigate to.

func (*Plugin) RequireToken

func (p *Plugin) RequireToken(req Requirement) (func(http.Handler) http.Handler, error)

RequireToken returns middleware for the host's own API: it accepts only one Authorization: Bearer header (never cookies, query strings or bodies), runs Authorize against req and passes a context carrying the token. Use WithToken in the handler for mutations. The host owns rate, body, origin and CORS policy for these routes; bearer authentication is not ambient, so it needs no CSRF proof.

func (*Plugin) ResourceSecrets

func (p *Plugin) ResourceSecrets() ResourceSecretService

ResourceSecrets returns the host-only resource credential capability.

func (*Plugin) RevokeGrant

func (p *Plugin) RevokeGrant(ctx context.Context, clientID string) error

RevokeGrant revokes the caller's grant to a client, with every code, refresh family and access token issued under it, on every instance as soon as it commits. A grant the caller does not hold is forbidden.

func (*Plugin) WithToken

func (p *Plugin) WithToken(ctx context.Context, req Requirement, op plugin.Operation, fn func(storage.Tx, TokenInfo) error) error

WithToken revalidates the token Authorize bound to ctx against req and runs trusted work in the same transaction, holding the owner's lock, so a revocation cannot race the work. A non-empty op records a success event attributed to the token's user, with the client as method. Always scope resource queries to the returned TokenInfo's user.

type Requirement

type Requirement struct {
	Resource string
	Scopes   []string
}

Requirement is what one call of the host's own API needs: Resource is the API's URI, one of Config.Resources, and every scope must be one it defines and be held by the token.

type Resource

type Resource struct {
	URI           string
	Name          string
	Scopes        []string
	Introspection bool
}

Resource is one of the host's resource servers. URI is the audience of its access tokens; Scopes are the scopes it defines. Introspection lets it call the introspection endpoint with secrets from Plugin.ResourceSecrets.

type ResourceSecretService

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

ResourceSecretService is the host-only capability over the introspection credentials of resources declared with Introspection.

func (ResourceSecretService) List

func (s ResourceSecretService) List(ctx context.Context, resourceURI string) ([]ClientSecret, error)

List describes a resource's live introspection secrets.

func (ResourceSecretService) Revoke

func (s ResourceSecretService) Revoke(ctx context.Context, resourceURI, id string) error

Revoke deletes one introspection secret at once.

func (ResourceSecretService) Rotate

func (s ResourceSecretService) Rotate(ctx context.Context, resourceURI string, overlap time.Duration) (IssuedSecret, error)

Rotate issues an introspection secret for a resource, with the overlap rule of ClientService.RotateSecret. The resource authenticates to the introspection endpoint with its URI as client_id and this secret.

type ResourceView

type ResourceView struct {
	URI  string `json:"uri"`
	Name string `json:"name"`
}

ResourceView names a resource for a consent screen.

type Scope

type Scope struct {
	Name string `json:"name"`
	// Resource is the URI of the resource that defines it, empty for the
	// OpenID Connect scopes.
	Resource string `json:"resource"`
}

Scope describes a requested or granted scope for a consent screen.

type SigningKey

type SigningKey struct {
	ID        string
	Algorithm string
	Signer    crypto.Signer
}

SigningKey is a host-custodied ID token signing key. Signer may be backed by a KMS or HSM; Sigil never stores or generates private keys. The public key is derived from Signer. IDs must be stable and never reused.

type TokenInfo

type TokenInfo struct {
	TokenID, ClientID, UserID, Audience string
	Scopes                              []string
	IssuedAt, ExpiresAt                 time.Time
}

TokenInfo is a credential-free view of a validated access token.

func FromContext

func FromContext(ctx context.Context) (TokenInfo, bool)

FromContext returns the token Authorize bound to ctx. It is display and routing data, not a grant: mutations use WithToken.

Jump to

Keyboard shortcuts

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