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

appauth

package
v0.29.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package appauth assembles identity/relyingparty for one application, so every application that signs people in through SystemAuth does it the same way, with the same defaults:

  • Browsers sign in through the /bff/* cookie session. The session's SystemAuth tokens are bound to the application's DPoP key, which is required, so sessions survive restarts and are shared by instances.
  • Programs call the API with DPoP-bound SystemAuth access tokens; the Bearer scheme is refused unless explicitly allowed.
  • A cookie-authenticated request that changes state must come from an allowed origin with a JSON body (relyingparty's check).
  • Inactive principals are refused at sign-in and on every request (relyingparty's check).
  • Memberships come from the orgs claim, kept to the organizations where the principal holds one of the application's roles.

New returns an Authenticator: mount Handler at the base path, and protect the API with Middleware or call Authenticate. Other protocols, such as AAuth for agents, chain after the built-in ones through Options.Authenticators.

Example

An application reads its sign-in settings from APP_* variables, keeps sessions and principals in Postgres, and protects its API.

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net/http"

	"github.com/plexusone/systemforge/identity/appauth"
	"github.com/plexusone/systemforge/identity/relyingparty/pgstore"
	"github.com/plexusone/systemforge/identity/requestprincipal"
)

func main() {
	ctx := context.Background()
	cfg, labels := appauth.ConfigFromEnv("APP")
	if err := cfg.Validate(labels); err != nil {
		log.Fatal(err)
	}
	db, err := sql.Open("pgx", "postgres://app@db.example.com/app")
	if err != nil {
		log.Fatal(err)
	}
	if err := pgstore.EnsureSchema(ctx, db); err != nil {
		log.Fatal(err)
	}
	stores, err := pgstore.NewStores(db, pgstore.KeysFromEnv("APP"))
	if err != nil {
		log.Fatal(err)
	}
	auth, err := appauth.New(ctx, cfg, appauth.Options{
		Principals:  stores.Principals,
		Sessions:    stores.Sessions,
		LoginStates: stores.LoginStates,
		Application: "notes",
		Labels:      &labels,
	})
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	mux.Handle(auth.BasePath()+"/", auth.Handler())
	mux.Handle(appauth.ResourceMetadataPath, auth.ResourceMetadataHandler())
	mux.Handle("/api/", auth.Middleware(true)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		p, _ := requestprincipal.FromContext(r.Context()) // set by Middleware(true)
		if _, err := fmt.Fprintf(w, "hello %s", p.PrincipalID); err != nil {
			log.Print(err)
		}
	})))
	log.Fatal(http.ListenAndServe(":8080", mux)) //nolint:gosec // G114: an example
}

Index

Examples

Constants

View Source
const (
	EnvIssuer              = "SYSTEMAUTH_ISSUER"
	EnvClientID            = "CLIENT_ID"
	EnvClientSecret        = "CLIENT_SECRET" //nolint:gosec // G101: an environment variable name, not a credential
	EnvPublicURL           = "PUBLIC_URL"
	EnvAllowedOrigins      = "ALLOWED_ORIGINS"
	EnvAccessTokenAudience = "ACCESS_TOKEN_AUDIENCE"
	EnvAcceptedClientIDs   = "ACCEPTED_CLIENT_IDS"
	EnvDPoPKey             = "DPOP_KEY"
	EnvDPoPKeyFile         = "DPOP_KEY_FILE"
)

Environment variable suffixes read by ConfigFromEnv, after the prefix and an underscore.

View Source
const DPoPChallenge = `DPoP algs="ES256 ES384 ES512"`

DPoPChallenge is the WWW-Authenticate value for a request refused without a more specific challenge: a DPoP-bound access token is expected.

View Source
const DPoPKeyCommand = "openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256"

DPoPKeyCommand generates a key for Config.DPoPKey or Config.DPoPKeyFile.

View Source
const DefaultBasePath = "/bff"

DefaultBasePath is where the browser sign-in surface (/bff/*) is mounted when Config.BasePath is empty.

View Source
const ResourceMetadataPath = "/.well-known/oauth-protected-resource"

ResourceMetadataPath is where protected resource metadata (RFC 9728) is served, on the application's own origin.

Variables

View Source
var (
	// ErrUnauthenticated is wrapped by Authenticate's error for a missing,
	// invalid or expired credential (a 401).
	ErrUnauthenticated = errors.New("appauth: unauthenticated")
	// ErrForbidden is wrapped by Authenticate's error for a valid
	// credential on a refused request, such as a cross-origin write with
	// the session cookie (a 403).
	ErrForbidden = errors.New("appauth: forbidden")
	// ErrNoSessionToken is returned by SessionToken for a request without a
	// browser session, or one that presented its own credential.
	ErrNoSessionToken = errors.New("appauth: the request has no session token")
)
View Source
var DefaultLabels = Labels{
	Issuer:              "issuer",
	ClientID:            "client ID",
	PublicURL:           "public URL",
	AllowedOrigins:      "allowed origin",
	AccessTokenAudience: "access token audience",
	AcceptedClientIDs:   "accepted client ID",
	DPoPKey:             "DPoP key",
	DPoPKeyFile:         "DPoP key file",
}

DefaultLabels are plain setting names.

Functions

func Challenges

func Challenges(err error) []string

Challenges returns the WWW-Authenticate values to send with a refusal from Authenticate: the ones the refusing method chose, such as error="invalid_token", which a client acts on, or DPoPChallenge.

func ConfigFromEnv

func ConfigFromEnv(prefix string) (Config, Labels)

ConfigFromEnv reads the configuration from environment variables named prefix + "_" + one of the Env* suffixes, and returns labels naming them. List values are comma-separated.

func IsLoopbackHost

func IsLoopbackHost(host string) bool

IsLoopbackHost reports whether host is localhost, a *.localhost name, or a loopback IP address.

func ParseDPoPKey

func ParseDPoPKey(s string) (*dpop.KeyPair, error)

ParseDPoPKey parses a PEM P-256 private key, PKCS #8 or SEC 1.

func ReadDPoPKeyFile

func ReadDPoPKeyFile(path string) (*dpop.KeyPair, error)

ReadDPoPKeyFile reads and parses a PEM P-256 private key file.

Types

type Authenticator

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

Authenticator authenticates an application's requests and serves its /bff/* surface.

func New

func New(ctx context.Context, cfg Config, opts Options) (*Authenticator, error)

New builds the Authenticator: the relying-party client, from Options.Metadata or by discovery, the BFF, and the request authentication chain.

func (*Authenticator) Authenticate

Authenticate resolves a request to its principal, for applications that resolve it inside a framework rather than in middleware. A refusal wraps ErrUnauthenticated or ErrForbidden and a *requestprincipal.Error, whose Write sends the response the protocol prescribes; Challenges gives its WWW-Authenticate values.

func (*Authenticator) BFF

func (a *Authenticator) BFF() *relyingparty.BFF

BFF is the browser session handler.

func (*Authenticator) BasePath

func (a *Authenticator) BasePath() string

BasePath is where Handler is mounted.

func (*Authenticator) Challenge

func (a *Authenticator) Challenge(h http.Header)

Challenge adds the WWW-Authenticate challenges for a request without credentials: DPoP, Bearer when unbound tokens are accepted, and those of Options.Authenticators.

func (*Authenticator) Client

func (a *Authenticator) Client() *relyingparty.Client

Client is the relying-party client, for calls to SystemAuth.

func (*Authenticator) ClientAddress

func (a *Authenticator) ClientAddress() *clientaddr.Resolver

ClientAddress returns the client-address resolver (Options.ClientAddress), to key the application's rate limits (ratelimit.PolicyConfig.Address). It is never nil: without the option it uses the direct peer.

func (*Authenticator) Handler

func (a *Authenticator) Handler() http.Handler

Handler serves the /bff/* surface; mount it at BasePath.

func (*Authenticator) Middleware

func (a *Authenticator) Middleware(required bool) func(http.Handler) http.Handler

Middleware authenticates each request and puts its requestprincipal.RequestPrincipal in the context (requestprincipal.FromContext; its Native is the *relyingparty.AuthenticatedPrincipal for the built-in methods). With required, a request without credentials gets a 401 with the challenges; without it, it passes with no principal. Invalid credentials are refused either way, and a cross-origin cookie write gets a 403.

With Options.SignedInRateLimit, signed-in callers are then limited per principal or service account.

func (*Authenticator) RequestAuthenticator

func (a *Authenticator) RequestAuthenticator() requestprincipal.Authenticator

RequestAuthenticator is the authentication chain, to combine with other requestprincipal middleware.

func (*Authenticator) ResourceMetadata

func (a *Authenticator) ResourceMetadata() ResourceMetadata

ResourceMetadata returns the application's protected resource metadata.

func (*Authenticator) ResourceMetadataHandler

func (a *Authenticator) ResourceMetadataHandler() http.Handler

ResourceMetadataHandler serves ResourceMetadata as JSON; mount it at "GET " + ResourceMetadataPath. The document changes only with the configuration, so clients may cache it for an hour.

func (*Authenticator) SessionToken

func (a *Authenticator) SessionToken(r *http.Request) (string, error)

SessionToken returns the access token of the request's browser session, refreshed when about to expire, to call SystemAuth's APIs for the person with Transport. A request that presented its own credential gets ErrNoSessionToken: its token is bound to the caller's key, and the application cannot make proofs for it.

func (*Authenticator) Transport

func (a *Authenticator) Transport(base http.RoundTripper) http.RoundTripper

Transport returns a RoundTripper for requests to SystemAuth that carry a session token in Authorization: it presents a bound token with the DPoP scheme and a proof by the application's key. See relyingparty.Client.Transport.

type Config

type Config struct {
	// Issuer is SystemAuth's issuer URL.
	Issuer string
	// ClientID is the application's OAuth client at SystemAuth.
	ClientID string
	// ClientSecret is the confidential client's secret. Empty for a public
	// client.
	ClientSecret string
	// PublicURL is the origin the application is reached at, such as
	// https://app.example.com. The redirect URL and the DPoP proof URL are
	// built from it.
	PublicURL string
	// AllowedOrigins are other origins that may call the BFF and be
	// post-login return targets.
	AllowedOrigins []string
	// AccessTokenAudience, when set, must be in the aud of access tokens
	// the application's API accepts.
	AccessTokenAudience string
	// AcceptedClientIDs are other OAuth clients whose access tokens the API
	// accepts, such as a command-line tool's. ClientID is always accepted.
	AcceptedClientIDs []string
	// Scopes are requested at sign-in in addition to
	// relyingparty.DefaultScopes.
	Scopes []string
	// DPoPKey is the PEM P-256 private key the application proves
	// possession of to SystemAuth. Sessions' tokens are bound to it, so it
	// must survive restarts and be shared by every instance.
	DPoPKey string
	// DPoPKeyFile is a file holding DPoPKey, for deployments that deliver
	// secrets as files. Set one of DPoPKey and DPoPKeyFile.
	DPoPKeyFile string
	// BasePath is where the BFF is mounted. Default: DefaultBasePath.
	BasePath string
}

Config is an application's sign-in configuration: who SystemAuth is, how the application is registered there, and where it is reached.

func (Config) Configured

func (c Config) Configured() bool

Configured reports whether any sign-in setting is present, so a host can tell "not configured" from "configured wrongly".

func (Config) RedirectURL

func (c Config) RedirectURL() string

RedirectURL is the OIDC callback URL to register with SystemAuth: PublicURL + BasePath + "/auth/callback".

func (Config) Validate

func (c Config) Validate(l Labels) error

Validate checks the configuration and reports every problem, each starting with its label. The DPoP key is required.

type Labels

type Labels struct {
	Issuer, ClientID, PublicURL, AllowedOrigins, AccessTokenAudience,
	AcceptedClientIDs, DPoPKey, DPoPKeyFile string
}

Labels name the settings in Validate's errors, so a host can point at its own variable or file keys. ConfigFromEnv returns labels of the form "Issuer (APP_SYSTEMAUTH_ISSUER)"; a host may replace the parenthesized part.

type Options

type Options struct {
	// Principals is the application's principal store (required).
	Principals relyingparty.PrincipalStore
	// Sessions stores browser sessions, and LoginStates in-flight sign-ins.
	// Both are required unless PublicURL is a loopback host, where the
	// in-memory stores are used and sessions end at a restart. Use
	// pgstore.NewStores in production.
	Sessions    relyingparty.SessionStore
	LoginStates relyingparty.LoginStateStore

	// Application is the application's key in an organization's
	// applications (the orgs claim's apps). When set, the default
	// membership source keeps only organizations where the principal holds
	// a role of it that KnownRole accepts (any role when KnownRole is nil).
	Application string
	KnownRole   func(role string) bool
	// Memberships replaces the default membership source, the orgs claim
	// with SystemAuth's memberships endpoint as fallback. It is used as is:
	// Application does not filter it. Pass relyingparty.LocalMemberships for
	// an application that keeps its own membership tables.
	Memberships relyingparty.MembershipSource

	// Apps lists the deployment's application catalog in the browser
	// session, for the product switcher.
	Apps appcatalog.Source
	// Metadata is SystemAuth's provider metadata, when the host already has
	// it (an embedded SystemAuth). Without it, New discovers it.
	Metadata *relyingparty.Metadata
	// HTTPClient makes the calls to SystemAuth. Default: http.DefaultClient.
	HTTPClient *http.Client
	// Revocation, when set, applies recorded access changes to sessions and
	// tokens at once (ADR-010).
	Revocation *relyingparty.RevocationCache
	// LoginErrorPath, when set, is the app's sign-in page: a browser whose
	// sign-in fails is redirected there with ?error=<CODE> instead of shown
	// a JSON error. See relyingparty.BFFConfig.LoginErrorPath.
	LoginErrorPath string
	// ReplayCache rejects reused DPoP proofs. Default: in memory, per
	// instance; instances that share an API need a shared cache. Unless
	// the public URL is a loopback host or SingleInstance is set, New
	// logs a warning when it uses the default.
	ReplayCache relyingparty.DPoPReplayCache
	// SingleInstance declares that one instance serves the API, so the
	// default in-memory replay cache sees every proof.
	SingleInstance bool
	// Authenticators are tried after the session and token, for requests
	// they do not recognize, such as AAuth-signed agent requests.
	Authenticators []requestprincipal.Authenticator

	// ServiceAccounts accepts SystemAuth service accounts' tokens: their
	// client (relyingparty.ServiceAccountClientID) is accepted, and an
	// account's local principal is created on its first request
	// (relyingparty.BearerConfig.ServicePrincipals). Principals must support
	// service identities, as pgstore.PrincipalStore does.
	ServiceAccounts bool

	// EphemeralDPoPKey lets the application start without a DPoP key: one
	// is generated per process, so people sign in again after every
	// restart. For local development only.
	EphemeralDPoPKey bool
	// AllowBearerTokens also accepts unbound access tokens with the Bearer
	// scheme, from clients SystemAuth exempts from DPoP.
	AllowBearerTokens bool

	// ResourceName names the application in its protected resource
	// metadata (RFC 9728).
	ResourceName string
	// Labels name the settings in New's configuration errors. Default:
	// DefaultLabels.
	Labels *Labels
	// Logger receives warnings and operational errors. Default:
	// slog.Default().
	Logger *slog.Logger

	// ClientAddress finds the client address behind the application's
	// reverse proxies (see clientaddr), for the application's rate limits
	// (ClientAddress returns it). Default: the direct peer.
	ClientAddress *clientaddr.Resolver
	// SignedInRateLimit, when set, limits each signed-in principal or
	// service account after Middleware authenticates it, such as
	// ratelimit.NewSignedInLimiter. Anonymous requests are not limited by
	// it.
	SignedInRateLimit *ratelimit.Limiter
}

Options are the storage ports and choices of an Authenticator. Every option that loosens a default is off unless set, and logs a warning when set.

type ResourceMetadata

type ResourceMetadata struct {
	// Resource is the application's resource identifier, its public URL.
	Resource string `json:"resource"`
	// AuthorizationServers are the issuers whose tokens are accepted.
	AuthorizationServers []string `json:"authorization_servers"`
	// BearerMethodsSupported is how tokens are presented.
	BearerMethodsSupported []string `json:"bearer_methods_supported"`
	// DPoPSigningAlgValuesSupported are the proof algorithms accepted.
	DPoPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported"`
	// DPoPBoundAccessTokensRequired is true unless unbound tokens are
	// accepted.
	DPoPBoundAccessTokensRequired bool `json:"dpop_bound_access_tokens_required"`
	// ResourceName is Options.ResourceName, for display.
	ResourceName string `json:"resource_name,omitempty"`
}

ResourceMetadata is the protected resource metadata document (RFC 9728 section 2). It tells a client given only the application's URL where to obtain access tokens for it, and whether they must be DPoP-bound.

Jump to

Keyboard shortcuts

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