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
}
Output:
Index ¶
- Constants
- Variables
- func Challenges(err error) []string
- func ConfigFromEnv(prefix string) (Config, Labels)
- func IsLoopbackHost(host string) bool
- func ParseDPoPKey(s string) (*dpop.KeyPair, error)
- func ReadDPoPKeyFile(path string) (*dpop.KeyPair, error)
- type Authenticator
- func (a *Authenticator) Authenticate(r *http.Request) (*requestprincipal.RequestPrincipal, error)
- func (a *Authenticator) BFF() *relyingparty.BFF
- func (a *Authenticator) BasePath() string
- func (a *Authenticator) Challenge(h http.Header)
- func (a *Authenticator) Client() *relyingparty.Client
- func (a *Authenticator) ClientAddress() *clientaddr.Resolver
- func (a *Authenticator) Handler() http.Handler
- func (a *Authenticator) Middleware(required bool) func(http.Handler) http.Handler
- func (a *Authenticator) RequestAuthenticator() requestprincipal.Authenticator
- func (a *Authenticator) ResourceMetadata() ResourceMetadata
- func (a *Authenticator) ResourceMetadataHandler() http.Handler
- func (a *Authenticator) SessionToken(r *http.Request) (string, error)
- func (a *Authenticator) Transport(base http.RoundTripper) http.RoundTripper
- type Config
- type Labels
- type Options
- type ResourceMetadata
Examples ¶
Constants ¶
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.
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.
const DPoPKeyCommand = "openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256"
DPoPKeyCommand generates a key for Config.DPoPKey or Config.DPoPKeyFile.
const DefaultBasePath = "/bff"
DefaultBasePath is where the browser sign-in surface (/bff/*) is mounted when Config.BasePath is empty.
const ResourceMetadataPath = "/.well-known/oauth-protected-resource"
ResourceMetadataPath is where protected resource metadata (RFC 9728) is served, on the application's own origin.
Variables ¶
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") )
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 ¶
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 ¶
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 ¶
IsLoopbackHost reports whether host is localhost, a *.localhost name, or a loopback IP address.
func ParseDPoPKey ¶
ParseDPoPKey parses a PEM P-256 private key, PKCS #8 or SEC 1.
Types ¶
type Authenticator ¶
type Authenticator struct {
// contains filtered or unexported fields
}
Authenticator authenticates an application's requests and serves its /bff/* surface.
func New ¶
New builds the Authenticator: the relying-party client, from Options.Metadata or by discovery, the BFF, and the request authentication chain.
func (*Authenticator) Authenticate ¶
func (a *Authenticator) Authenticate(r *http.Request) (*requestprincipal.RequestPrincipal, error)
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 ¶
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 ¶
Configured reports whether any sign-in setting is present, so a host can tell "not configured" from "configured wrongly".
func (Config) RedirectURL ¶
RedirectURL is the OIDC callback URL to register with SystemAuth: PublicURL + BasePath + "/auth/callback".
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.