Documentation
¶
Overview ¶
Package auth is the authentication foundation, assembled.
The packages under it are the parts: session issue and rotation, API keys, permission resolution, the endpoints, and the Postgres stores behind all of them. Each is separately usable and separately testable, and putting them together is the same fifteen lines in every project.
This package is those fifteen lines. Hand it a pool:
front, err := auth.New(auth.Config{Pool: pool})
if err != nil {
return nil, err
}
mux := api.Register(api.Handlers{
Server: api.Server{Auth: front},
…
})
One field, because the two halves cannot be wired separately without one of them eventually being forgotten: Register takes its GetClaims from it and mounts its routes on the mux it returns. It is typed as an interface the generated package declares, so a project with no authentication does not depend on this module to serve a table.
That is a working sign-in, logout, refresh, email verification, session list and API key endpoint, over the tables `rig setup-project` wrote, with lockout and rate limits counted in the database. Nothing about it is generated and nothing about it is yours to maintain.
The fields below are the choices an application actually has to make — where the tenant comes from, how long a session lasts, who sends the mail. Anything further is a reason to build the parts yourself: every one of them is exported, this package holds no logic of its own, and Parts returns what it made so that a project can go halfway rather than starting over.
Index ¶
- Constants
- func TenantFromHeader(r *http.Request) (uuid.UUID, error)
- type Auth
- func (a *Auth) Claims(r *http.Request) (tenancy.Claims, error)
- func (a *Auth) Close(ctx context.Context) error
- func (a *Auth) DispatchMail(ctx context.Context) (account.MailReport, error)
- func (a *Auth) Mount(mux httpx.Router)
- func (a *Auth) Parts() Parts
- func (a *Auth) Providers() []string
- func (a *Auth) PruneLog(ctx context.Context) (int, error)
- func (a *Auth) PruneMail(ctx context.Context) (int, error)
- func (a *Auth) ReleaseMailClaims(ctx context.Context) (int, error)
- func (a *Auth) Session(r *http.Request) (*session.Token, error)
- func (a *Auth) StopClaimingMail()
- type CacheOptions
- type Config
- type EmailCodeOptions
- type GrantsCache
- func (c *GrantsCache) Forget(tenantID, accountID uuid.UUID)
- func (c *GrantsCache) Invalidate(ctx context.Context, db dbx.Conn, tenantID, accountID uuid.UUID) error
- func (c *GrantsCache) InvalidateAll(ctx context.Context, db dbx.Conn) error
- func (c *GrantsCache) Serve(bus *cache.Bus)
- func (c *GrantsCache) ServeLocally()
- func (c *GrantsCache) Wrap(g authhttp.Grants) authhttp.Grants
- type GrantsCacheConfig
- type MailOptions
- type OAuth
- type Parts
Constants ¶
const DefaultBasePath = "/auth"
DefaultBasePath is where the endpoints mount when nothing says otherwise.
const DefaultCacheTTL = 30 * time.Second
DefaultCacheTTL is thirty seconds.
Long enough that somebody clicking through an application is served from memory throughout, and short enough that the window it actually covers — a replica that lost the channel between a revocation and its reconnect — is measured in seconds.
const GrantsTopic = "grants"
GrantsTopic is the name grant invalidations travel under.
Exported for the reason session.TokenTopic and apikey.KeyTopic are: it is half of an agreement, and somebody reading the channel with psql to find out whether anything is being published needs to know what they are looking at.
const TenantHeader = "X-Tenant-Id"
TenantHeader is where the default tenant resolver looks.
A request has to say which tenant it is for before there is a session to read it from: the same address may exist in two of them, and which one is being signed into is part of the request. A header is the least surprising default and the easiest to try; a subdomain or a path segment is a Config.Tenant away.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Auth ¶
type Auth struct {
// contains filtered or unexported fields
}
Auth is the assembled foundation.
func (*Auth) Claims ¶
Claims identifies the caller of a request.
Together with Mount this satisfies the Authenticator interface the generated API package declares, which is how api.Server.Auth takes a *Auth without the generated code importing this module.
This is what to give the generated server as GetClaims. It accepts a session token or an API key, resolves the caller's permissions, and refuses anything else — so a token, a key, and the roles attached to either mean the same thing in a generated handler as they do in the endpoints here.
func (*Auth) Close ¶
Close stops the invalidation channel and waits for it.
Register it with the shutdown, within a timeout — there is nothing in flight worth finishing, so this only needs long enough to close a connection:
app.CloseWithin("auth", 5*time.Second, front.Close)
A project that configured no cache has nothing to close and this returns at once, which is what makes registering it unconditionally the right thing to write.
Forgetting it is not a correctness problem, and that is deliberate. A bus that is not running reports itself as not live, and a cache that is not live reads through and stores nothing — so the failure mode of every mistake around this lifecycle is a slower server, never a server answering out of a cache nobody can withdraw from.
func (*Auth) DispatchMail ¶
DispatchMail is one pass of the link queue: claim what is due, send it, mark it.
**This is the guarantee behind every link rig mints, and nothing runs it for you.** Register it as a task an operator's cron invokes — a subcommand rather than a goroutine, so it is a cron job rather than something racing itself in every replica. With Mail.Queue set and no such entry, links are queued and never sent.
An empty report and no error for a project that did not turn the queue on, so registering the task before deciding costs nothing.
func (*Auth) Mount ¶
Mount registers the endpoints on a router.
It takes the same one the generated API is registered through, so that one server answers both and the paths cannot collide silently: /auth is separate from the API's base path, and Go's router refuses a duplicate pattern. A net/http.ServeMux satisfies it, and a generated server passes something that wraps each route in a span before handing it on.
Setting api.Server.Auth calls this for you, which is the ordinary way. Call it directly only when the API layer is not rig's — mounting these endpoints beside a hand-written router, for instance.
func (*Auth) Parts ¶
Parts are the pieces this assembled, for the things an endpoint cannot do for you.
func (*Auth) Providers ¶
Providers are the configured sign-in providers, for a sign-in page to draw a button per name. Empty when none are wired up.
The names are what the routes take: a provider called Google is reached at <BasePath>/oauth/google/start, lowercased.
func (*Auth) PruneLog ¶
PruneLog deletes authentication log entries older than Config.LogRetention and reports how many went. Zero retention keeps everything and prunes nothing.
A task somebody runs, not a goroutine this starts. Housekeeping that schedules itself inside the server is housekeeping every replica does at once, and this one takes a lock on the table the whole authentication path writes to. `serve.Config.Tasks` makes it a subcommand for a cron job; the generated wiring registers it for a project whose rig.yaml set a window.
func (*Auth) PruneMail ¶
PruneMail removes deliveries that are done and older than Mail.Retention, in the same task that dispatches them — the way the file sweeper's two rules share one.
Never a pending row, and never a link row: those are the record of who was invited and when.
func (*Auth) ReleaseMailClaims ¶
ReleaseMailClaims gives back every lease this process holds, for the close half.
A lease given back is a row the next dispatcher takes immediately, rather than one that waits out the TTL. The attempts are not given back: the sends this process made were made.
func (*Auth) StopClaimingMail ¶
func (a *Auth) StopClaimingMail()
StopClaimingMail stops taking new deliveries while the pass in flight finishes, for the drain half of a shutdown.
type CacheOptions ¶
type CacheOptions struct {
// Enabled turns it on. The zero value reads a row per authenticated
// request, which is what rig did before this existed and is still the
// correct choice for a deployment that is not paying for it.
Enabled bool
// TTL is how long an answer may be reused when no invalidation arrives.
//
// It is the backstop and not the guarantee. The guarantee is the channel: a
// revocation publishes on the transaction that performed it, so every
// replica that is listening has forgotten by the time that transaction
// commits. This is what remains for a replica that was not.
//
// Zero takes [DefaultCacheTTL].
TTL time.Duration
// Channel is the Postgres channel invalidations travel on. Zero takes
// [cache.DefaultChannel].
//
// It has to be an identifier — it reaches Postgres both quoted in a LISTEN
// and as a parameter to pg_notify — and [cache.NewBus] panics on one that is
// not, at startup, rather than failing at the first notification.
Channel string
// MaxEntries bounds each cache. Zero takes [cache.DefaultMaxEntries].
MaxEntries int
// Logger receives one line when invalidations stop arriving and one when
// they resume. Zero uses [slog.Default].
//
// Worth setting, and worth reading. It is the only way anybody learns that
// this module has gone back to reading a row per request: the fallback is
// correct and silent, so nothing else about the server will say so.
Logger *slog.Logger
}
CacheOptions is whether rig holds its own per-request reads in memory, and what that costs.
The reads are the two this module makes on behalf of every caller: resolving a session token, and resolving an API key. Both are rig's end to end — it performs the read and it performs every write that invalidates it — which is the whole reason they are the ones cached. An application supplies no invalidation, registers no hook and has no write path it can forget, because there is none belonging to it.
The read this deliberately does not cover is Config.Grants, which is an application's own function over an application's own tables. rig cannot see those writes, so caching that answer would mean an application publishing its own invalidations — and one forgotten write path there is a withdrawn permission that goes on working. Parts.Cache is the bus for a project that decides to take that on knowingly.
type Config ¶
type Config struct {
// Pool is the database holding the foundation tables.
Pool *pgxpool.Pool
// Tenant resolves which tenant a request belongs to. The default reads the
// X-Tenant-Id header.
//
// Replace it with whatever your deployment actually uses — a subdomain, a
// path segment, a claim in a proxy's header. It is consulted only where a
// tenant cannot be known some other way: a sign-in, a code request, the OAuth
// start. Once there is a session the tenant comes from the token.
//
// uuid.Nil is an ordinary answer at every one of those places and means
// "unspecified" rather than "none": a single sign-in page cannot know which
// tenants an address belongs to before somebody has proved they own it, so
// the sign-in resolves it afterwards. That is true of a provider sign-in as
// well as a code one — a header is not there when a provider sends the
// browser back, so a header-based deployment answers Nil at the OAuth start
// and the callback settles it. Which is why the default answers Nil for a
// missing header rather than refusing; see [TenantFromHeader].
Tenant func(*http.Request) (uuid.UUID, error)
// BasePath prefixes the endpoints. Default [DefaultBasePath].
//
// The provider routes sit under it: BasePath + "/oauth/{provider}/start".
BasePath string
// OnError renders a failure. The default is this package's own; pass your
// API's error mapper so that an authentication failure looks like every
// other failure.
OnError func(w http.ResponseWriter, r *http.Request, err error)
// AccessTTL, RefreshTTL and RememberTTL are how long the two kinds of token
// last, and how long "remember me" extends the refresh token to. Zero means
// the session package's defaults: 10 minutes, 12 hours, 30 days.
AccessTTL time.Duration
RefreshTTL time.Duration
RememberTTL time.Duration
// RotationLeeway is how long a refresh token stays usable after it has been
// exchanged. Zero is [session.DefaultRotationLeeway], thirty seconds.
//
// Without one, a dropped response is a logout: the client never received the
// new pair, retries with the old one, and has its whole family revoked for a
// network blip. See [session.Config.RotationLeeway].
RotationLeeway time.Duration
// Cache holds the reads this package makes on every request — verifying a
// session, verifying an API key — and withdraws them when they stop being
// true.
//
// The zero value caches nothing, which is a row read per authenticated
// request and the behaviour rig had before this existed. Setting
// [CacheOptions.Enabled] is not a time-to-live over authentication: it opens
// a Postgres channel, and every revocation this package performs publishes
// on the transaction that performed it, so a permission taken away stops
// working at the moment the change commits rather than when a timer expires.
//
// It is [New]'s to own. The bus is built, registered and started here and
// closed by [Auth.Close], so there is no cache to wire, no publish to
// remember, and no write path that can be left out.
Cache CacheOptions
// EmailCode is the mailed-code sign-in: rig sends a short numeric code to an
// address and somebody types it back.
//
// Off by default, and it is the way in for anybody with no account at a
// configured provider — which in most deployments is most people. [New]
// refuses it with no Notifier, because a code nobody receives is a door
// nobody can open.
//
// See [github.com/simonjanss/rig/auth/account.EmailCodeOptions].
EmailCode EmailCodeOptions
// Notifier sends the mail a flow needs — a sign-in code, a verification
// link, an invitation.
//
// Nil means none is sent, and it means that silently: account.NoNotifier is
// substituted and returns success from every method, so in production
// nobody can confirm an address and nothing anywhere says so. Set one. (Two
// combinations are refused rather than dropped: Mail.Queue with no
// notifier, which would write rows nothing could ever send, and EmailCode
// with no notifier, which would be the only door minting codes into a
// void.)
Notifier account.Notifier
// Mail is the queue for those links: off by default, and turning it on means
// running a job.
//
// Off, a link is minted in the request that asked for it and handed straight
// to the Notifier, so a provider having a bad minute fails that request,
// spends the caller's rate-limit budget, and kills a token that was already
// minted. On, the link is written to the database in the same transaction and
// sent by [Auth.DispatchMail] — which nothing runs on its own. Turning it on
// without a cron entry is turning mail off.
//
// The trade in the other direction is latency: a queued reset mail arrives up
// to one dispatch interval late.
Mail MailOptions
// RequireVerifiedEmail refuses a sign-in until the address is verified —
// a code and a provider alike, since linking a provider account
// requires a verified address and records it as one.
//
// It does not refuse a registration. The address is one request old there
// and the mail that would confirm it has not been opened.
RequireVerifiedEmail bool
// Grants answers what an account may do. Nil means nobody holds anything,
// which makes every endpoint carrying a permission answer 403.
//
// This is where an application's authorization model plugs in, and rig ships
// none. It derives the permission *keys* from the schema — api.Permissions()
// in the generated package is the whole catalogue — and generates the check
// that reads them off the claims. Deciding who holds which is yours: a table
// of roles, a switch on the account's level, a claim on a federated token, a
// map in a constant. Every product answers it differently and a framework
// that picked one would be wrong for most of them.
//
// examples/auth has a worked implementation over role tables of its own, if a
// starting point is more useful than a blank page.
Grants authhttp.Grants
// OnSessionRefresh replaces a session's payload every time it is refreshed.
//
// The payload is the application's own context for one session — which device
// it is, when step-up authentication was last satisfied, which mode somebody
// picked. It is set at sign-in through session.IssueInput.Payload, carried
// forward by every rotation, and reaches a handler as tenancy.Claims.Extra,
// where [github.com/simonjanss/rig/runtime/tenancy.Extra] decodes it into a
// type of your own.
//
// Nil, the default, carries the previous payload forward unchanged. Set this
// only to recompute it — the hook is handed the token being exchanged, so the
// current payload is prev.Payload.
//
// Not for permissions. It is written at sign-in and again only at refresh, so
// anything here outlives its own revocation by up to the refresh lifetime.
// Grants answers that question, per request.
OnSessionRefresh func(ctx context.Context, prev *session.Token) (json.RawMessage, error)
// IdentitySessionTTL is how long the tenant-less credential lasts. Zero is
// [session.DefaultIdentityTTL], half an hour, which is what picking a
// tenant takes.
IdentitySessionTTL time.Duration
// AllowTenantCreation mounts POST /auth/tenants, where somebody signed in
// makes one and becomes its Owner.
//
// Off by default. rig owns the mechanism — the tenant row, the first account,
// the slug, the transaction — because that is the same everywhere and getting
// it half-right leaves a tenant nobody can reach. The policy is Tenants below:
// who may, what a name is allowed to be, and what else a new one needs.
AllowTenantCreation bool
// Tenants is what this application decides about making them. Every field is
// optional; the zero value lets anybody signed in make one called anything.
Tenants account.TenantOptions
// AllowIdentity decides whether somebody nobody here has ever heard of may
// become an identity, and it is the one gate on that question: every path
// that would write a rig_identity asks it. A provider sign-in, a mailed
// code, an invitation and a direct provision alike — so a deployment cannot
// acquire an ungated door by turning on a sign-in method it did not have
// before.
//
// Nil keeps today's behaviour exactly: OAuth.AllowProvisioning for the
// provider path, EmailCode.AllowProvisioning for the code one, and the
// account.provision permission for the other two. A gate is asked *as well
// as* those and never instead of them, so it cannot open a door a bool has
// shut.
//
// It is asked about strangers and nobody else. By the time it runs rig has
// established that the address has no provider link and no identity of its
// own, so an existing member — and somebody adding a second provider to an
// account they already have — have both been admitted without consulting it.
// An application writes one rule, not a chain of them.
//
// The ordinary body is a domain rule, and
// [github.com/simonjanss/rig/auth/identity.AllowDomains] is that written
// out — which auth.allowed_identity_domains in rig.yaml generates for you.
// It allows an invitation whatever the list says, because somebody here
// typed that address in.
//
// Returning an error refuses, and the error's own sentence is what the
// refusal says — except on the mailed-code path, which answers 204 to every
// address so that holding one cannot be used to ask whether somebody here
// has it. There the refusal goes to rig_auth_log instead.
//
// See [github.com/simonjanss/rig/auth/identity.Gate], including the one
// thing it is not: an identity that already exists at a domain a gate would
// now refuse goes on signing in, because nobody is asking about them any
// more. This gates becoming somebody, not being somebody.
AllowIdentity identity.Gate
// OnRegistered runs inside the transaction that creates somebody rig has
// never seen — asking for a sign-in code with a new address, where
// EmailCode.AllowProvisioning allows it — and an error rolls the whole thing
// back. The ordinary body is accounts.Provision into a starter tenant, or
// accounts.Invite to leave one waiting in the picker they land in. Nil
// creates the person and nothing else.
//
// See [github.com/simonjanss/rig/auth/account.Config.OnRegistered].
OnRegistered func(ctx context.Context, accounts *account.Service, in account.Registered) error
// OnJoined runs inside the transaction that accepts an invitation, after the
// account exists and before the session is issued, and an error rolls the
// acceptance back. It is where a new member's roles and rows are seeded:
// accepting is called by rig's own handler, so this is the caller's line.
//
// See [github.com/simonjanss/rig/auth/account.Config.OnJoined].
OnJoined func(ctx context.Context, in account.Joined) error
// Limits override the rate limits. The zero value is the documented set: 5
// failed sign-ins per address per 15 minutes, 50 per address range, 5 code
// requests an hour, and so on.
Limits throttle.Defaults
// LogRetention is how long an entry in rig_auth_log is kept, and it is only
// read by [Auth.PruneLog]. Zero, the default, keeps everything and makes that
// method a no-op.
//
// It cannot be shorter than the longest window in Limits, and [New] refuses a
// configuration where it is rather than clamping it. Those limits are counted
// from this table: pruning inside a window deletes the failures a lockout is
// adding up to, so the limit stops limiting and nothing says so. Refusing at
// startup is the last place that mistake can be caught cheaply.
LogRetention time.Duration
// TrustedProxies are the networks whose X-Forwarded-For may be believed.
//
// Empty means none, and that is deliberate: an address read from a header a
// client controls is an address a client chooses, and a rate limit keyed on
// one is a rate limit an attacker walks around.
TrustedProxies []netip.Prefix
// OAuth wires the provider endpoints. Leave Providers empty and none are
// mounted.
OAuth OAuth
// Now is the clock, for tests.
Now func() time.Time
}
Config is what an application chooses. Only Pool is required.
type EmailCodeOptions ¶ added in v0.6.0
type EmailCodeOptions struct {
// Enabled turns the flow on and mounts its two routes. Off means they do not
// exist rather than answering 403.
Enabled bool
// Length is how many digits, and defaults to six. Six to ten are accepted:
// shorter is guessable whatever the attempt ceiling, and longer is a token,
// which belongs in a link.
Length int
// TTL is how long a code lasts, and defaults to ten minutes.
TTL time.Duration
// MaxAttempts is how many wrong guesses kill a code, and defaults to three
// — deliberately below the five wrong sign-ins that lock an address, so
// that mistyping a code is recoverable. It is a ceiling on one code rather
// than a rate limit on an address, and the two are not substitutes.
MaxAttempts int
// AllowProvisioning sends a code to an address rig has never seen, creating
// the person when the code is asked for. Off by default, which makes the
// deployment invite-only; on, it is self-registration, and OnRegistered is
// the hook that decides where a newcomer lands.
AllowProvisioning bool
}
EmailCodeOptions is the mailed-code sign-in.
A local mirror of github.com/simonjanss/rig/auth/account.EmailCodeOptions, the way MailOptions is of the queue's — this file is the one struct an application fills in, and reaching two packages down to configure a sign-in would make it two.
type GrantsCache ¶
type GrantsCache struct {
// contains filtered or unexported fields
}
GrantsCache is the map NewGrantsCache builds and the publisher that withdraws from it.
A nil *GrantsCache is usable and does nothing: GrantsCache.Wrap hands back the function it was given and the rest are no-ops. So is one that was never served. Both mean a project that decided against this, or has no `cache:` block, and neither needs a condition at the call sites that withdraw grants — the writes look the same either way.
func NewGrantsCache ¶
func NewGrantsCache(cfg GrantsCacheConfig) *GrantsCache
NewGrantsCache builds a cache for an application's authhttp.Grants function, not yet attached to anything.
**rig does not turn this on and cannot.** CacheOptions covers the two reads rig owns on both sides; this one is over an application's own tables, written to by an application's own code, and a cache whose invalidations somebody has to remember to publish is only as correct as the write path they forgot. That is why this is a call rather than a key in rig.yaml: the decision belongs next to the writes it is a promise about.
What it is for is the part that is the same for everybody. Grants runs on every authenticated request — that is what makes a role taken away stop working on the next call rather than at the next sign-in — and it is usually the most expensive read on that path. Caching it is six decisions, all of them already made next door in session.TokenCache, and all of them easy to get wrong once:
- the key is the tenant *and* the account, because permissions are per tenant and one account can be in several;
- the two slices are copied on the way out, because they reach a handler as tenancy.Claims fields and one caller appending to them would widen what every other request in the window may do;
- an error is never held, so a role table that was briefly unreachable does not answer "no permissions" for a lifetime;
- an empty answer *is* held, because somebody holding nothing is a real answer and not a miss;
- a replica that has lost the channel stops answering from memory altogether, rather than serving permissions it can no longer withdraw;
- and an invalidation is published on the caller's transaction, so it is delivered when the change commits and discarded if it rolls back.
Three calls, in this order, because the generated wiring needs the Grants function before it can hand back the bus it built:
grants := auth.NewGrantsCache(auth.GrantsCacheConfig{})
front, err := api.New(pool, api.Hooks{Grants: grants.Wrap(authz.Grants(pool))})
grants.Serve(front.Parts().Cache)
Every step of that is fail-safe. A cache that was never served holds nothing, a bus that is not running reports itself as not live, and a map that is not live reads through and stores nothing — so forgetting the third line, or leaving the `cache:` block out of rig.yaml entirely, costs latency rather than correctness.
func (*GrantsCache) Forget ¶
func (c *GrantsCache) Forget(tenantID, accountID uuid.UUID)
Forget withdraws one answer in this process only.
For a caller with no database handle to publish on. It leaves every other replica holding what it had, so it is the wrong tool for a revocation and the right one for a test.
func (*GrantsCache) Invalidate ¶
func (c *GrantsCache) Invalidate( ctx context.Context, db dbx.Conn, tenantID, accountID uuid.UUID, ) error
Invalidate withdraws one account's grants in one tenant, everywhere.
Call it from the transaction that changed the roles, passing that transaction — `dbx.Tx(ctx)` inside a hook, or the pool for a write that has none. Postgres delivers a notification when the transaction issuing it commits and discards it when that transaction rolls back, so an invalidation published this way is atomic with the change causing it and reaches every replica.
Published rather than dropped locally, and that distinction is the whole feature: a role removed on one replica has to stop working on all of them.
func (*GrantsCache) InvalidateAll ¶
InvalidateAll withdraws every held answer, everywhere.
For the writes that change what a role *means* rather than who holds it — seeding a tenant's roles, syncing the permission catalogue after a deploy, editing the grants on a role somebody else holds. Working out which accounts those reach is a query, and the answer is "clear it" for a map that refills itself on the next request.
func (*GrantsCache) Serve ¶
func (c *GrantsCache) Serve(bus *cache.Bus)
Serve attaches the cache to the invalidation channel, which is Parts.Cache.
Nothing is held until this runs, and nothing is held at all when bus is nil — a project with no `cache:` block. Both are the same rule: an answer that cannot be withdrawn is not one this will keep.
It panics if the bus already serves GrantsTopic, which is the wiring mistake of attaching two caches to one bus.
func (*GrantsCache) ServeLocally ¶
func (c *GrantsCache) ServeLocally()
ServeLocally attaches the cache to nothing, so it holds answers and forgets them in this process alone.
One process, no replicas, and staleness is the caller's problem — which is a real posture for a single-instance deployment, and the state a test wants without a database behind it. GrantsCache.Serve is what a deployment with replicas uses.
func (*GrantsCache) Wrap ¶
func (c *GrantsCache) Wrap(g authhttp.Grants) authhttp.Grants
Wrap returns the function to hand to Config.Grants.
Safe to call on a nil *GrantsCache, which returns g unchanged — so a project that decided against this needs no condition at the call site.
type GrantsCacheConfig ¶
type GrantsCacheConfig struct {
// TTL is how long an answer may be reused with no invalidation arriving.
// Zero takes [DefaultCacheTTL].
//
// The backstop and not the guarantee, exactly as [CacheOptions.TTL] is —
// with one difference worth being honest about. rig publishes every
// invalidation for the reads it caches itself, so that TTL covers only a
// replica that was not listening. Here the invalidations are yours, so it
// also covers every write path you did not publish from. It is the cost of
// a mistake as much as the cost of an outage.
TTL time.Duration
// MaxEntries bounds the map. Zero takes [cache.DefaultMaxEntries].
MaxEntries int
// Now is the clock, for tests.
Now func() time.Time
}
GrantsCacheConfig tunes a GrantsCache.
type MailOptions ¶
type MailOptions struct {
// Queue writes links to rig_identity_verification_delivery and sends them
// from [Auth.DispatchMail] instead of from the request.
Queue bool
// ClaimTTL is how long one dispatcher's claim is honoured, and SendTimeout
// bounds one call into the Notifier. SendTimeout has to be the shorter of
// the two, and rig refuses the pair rather than explaining it.
ClaimTTL time.Duration
SendTimeout time.Duration
// MaxAttempts, BackoffBase and BackoffCap are the retry arithmetic. The
// defaults span about eight hours.
MaxAttempts int
BackoffBase time.Duration
BackoffCap time.Duration
// Retention is how long a sent, failed or skipped delivery is kept before
// [Auth.PruneMail] removes it. Zero keeps them forever, which is what every
// other table in rig currently does and is not a recommendation.
//
// The link rows themselves are never pruned by this: they are the record of
// who was invited and when.
Retention time.Duration
}
MailOptions is the mail queue's configuration.
Queue is the switch; everything else has a default, and the defaults are notify's, so an operator tuning one dispatcher does not have to learn a second set of arithmetic for the other. See account.MailOptions for what each number does and which pairs are refused.
type OAuth ¶
type OAuth struct {
// Providers are the identity providers to offer, from the oauth package:
// oauth.Google(id, secret), oauth.Microsoft(id, secret), and so on.
Providers []oauth.Provider
// BaseURL is this application's own address, because the callback URL it
// registers with a provider has to be absolute.
BaseURL string
// Origin overrides BaseURL for one request, for an application served at
// several origins — a tenant per subdomain, most often. See
// [oauth.Config.Origin], including what a provider will and will not
// register.
Origin func(r *http.Request) string
// SigningKey signs the state parameter. Required, and at least 32 bytes —
// [oauth.New] refuses anything shorter.
//
// It has to be the same key in every replica: a callback may arrive at a
// different one than the one that started the flow, and a key invented per
// process is a sign-in that fails whenever a load balancer is doing its job.
SigningKey []byte
// StateTTL bounds how long a sign-in round trip may take. Zero is
// [oauth.DefaultStateTTL], ten minutes: generous for a redirect, short
// enough that a stolen state parameter is useless.
StateTTL time.Duration
// AllowProvisioning creates a person the first time somebody signs in with
// a provider. Without it a provider sign-in only works for an address that
// already has an identity.
AllowProvisioning bool
// AllowJoining creates an account in the tenant a sign-in named, for
// somebody not in it yet. Nil follows AllowProvisioning, which is what one
// switch did when it gated both doors. See
// [github.com/simonjanss/rig/auth/oauth.Config.AllowJoining].
AllowJoining *bool
// AllowedReturnTo are the origins a sign-in may return to, each as
// scheme://host with nothing after the host. An open redirect is the
// classic mistake here.
//
// A relative path is always allowed and is never matched against this list,
// so listing one does nothing. Which origin such a path resolves against is
// the ending's decision: the JSON one hands it back untouched, and the
// browser one resolves it against [OAuth.Browser]'s origin — which is also
// added to this list for you, since that is where its redirects land.
AllowedReturnTo []string
// Insecure allows the state cookie over plain HTTP, for local development
// where a browser would refuse the __Host- prefixed one. Never anywhere real.
Insecure bool
// Browser says where the front end is, and selecting it is what makes a
// sign-in end in a cookie and a redirect rather than in a JSON body. Nil
// keeps the JSON ending.
//
// Setting [OAuth.OnSignIn] as well replaces the ending and keeps the rest:
// the failure redirect and the allowed return-to entry still come from
// here, because a sign-in cancelled at the consent screen never reaches an
// ending and is rig's to answer either way.
//
// [handoff.Config.APIHost] is filled in from BaseURL when it is empty,
// because the two hosts have to be compared before anything can be written
// and only one of them is named here.
Browser *handoff.Config
// OnSignIn finishes a provider sign-in, and it is the escape hatch rather
// than the only way: nil selects one of two built-in endings.
//
// Without [OAuth.Browser] that is [authhttp.Handler.SignIn], which issues a
// session and answers with the same body a sign-in does — right for curl and
// for a native client, and nothing a browser that has just followed a
// redirect can act on. With one it is
// [authhttp.Handler.SignInToBrowser], which leaves the tokens in a
// short-lived cookie and redirects to the front end.
//
// Setting this beside a Browser is not a conflict and this wins. They are
// two questions — where the front end is, which CORS wants to know as well,
// and how a sign-in ends — and an application that answers the second one
// itself still has the first one worth answering.
//
// Whichever it is, it has to handle a sign-in with no tenant to land in:
// [oauth.SignIn.TenantID] is uuid.Nil whenever [Config.Tenant] answered Nil
// at the start, and issuing a session straight from that field would issue
// one into a tenant that does not exist. The default handles it by answering
// the way a sign-in does — an identity token and the tenant list, so the
// picker can take over.
OnSignIn func(w http.ResponseWriter, r *http.Request, in oauth.SignIn) error
// OnError renders a provider sign-in that did not finish, and it is a
// different question from [Config.OnError] rather than a duplicate of it.
//
// That one is the shape your API answers failures in, and it answers them in
// JSON, which is right for every route a script calls. These two are the
// routes a person reaches with their address bar, so what a hook here
// replaces is not an envelope but a dead end: rig's default is a bare
// text/plain page on the API's own origin, and an error envelope rendered
// into an address bar is no more readable than one. That is why
// [Config.OnError] is not used for this and why setting one does not set the
// other.
//
// What it gets is an [oauth.Failure], which says which of the ways this was
// — cancelled at the consent screen, an expired state cookie, an address the
// provider has not verified — so an application can redirect to its own
// sign-in page carrying a code it has copy for, rather than infer one from a
// status. Nil falls through to [OAuth.Fail], and then to the default.
OnError func(w http.ResponseWriter, r *http.Request, f *oauth.Failure)
// Fail is the API's own error writer, used when [OAuth.OnError] is nil and
// no front end has been configured to redirect to.
//
// It is what gets a refused provider sign-in into the log. The two callback
// routes were the only ones rig serves that wrote no line at any level, so a
// failure existed in the authentication log and nowhere else; handing them
// the same writer every other route has closes that. A generated server sets
// it for you.
//
// It does not change what a browser sees when a front end is configured: a
// `web:` block installs a redirect into [OAuth.OnError], and that still wins.
Fail func(w http.ResponseWriter, r *http.Request, err error)
}
OAuth is what a provider sign-in needs beyond a provider list.
type Parts ¶
type Parts struct {
Accounts *account.Service
Sessions *session.Manager
// Identities issues and verifies the credential somebody holds between
// signing in and choosing a tenant.
Identities *session.IdentityManager
APIKeys *apikey.Manager
Limiter *throttle.Limiter
Stores *authpg.Stores
// Cache is the invalidation channel rig's own caches run over, or nil when
// the project configured none.
//
// Here for the one thing rig cannot do on an application's behalf. rig
// caches what it owns end to end — it makes the read and it makes every
// write that withdraws it — and the read it does not own is Grants, over
// role tables belonging to the application. A project that wants that answer
// cached serves a topic of its own on this bus and publishes on it wherever
// its roles change; see runtime/cache. Nothing in rig will do it for you,
// because nothing in rig can see those writes.
Cache *cache.Bus
}
Parts are the pieces New built, for a project that needs to reach past the endpoints: issuing a session after somebody signs in another way, minting a key from an admin screen, checking a permission in a service rule.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package account implements the sign-in flows.
|
Package account implements the sign-in flows. |
|
Package apikey mints and verifies machine-to-machine credentials.
|
Package apikey mints and verifies machine-to-machine credentials. |
|
Package authhttp mounts the authentication endpoints.
|
Package authhttp mounts the authentication endpoints. |
|
Package authlog records what happened to an authentication attempt.
|
Package authlog records what happened to an authentication attempt. |
|
Package authpg implements the auth stores over the tables `rig setup-project` creates.
|
Package authpg implements the auth stores over the tables `rig setup-project` creates. |
|
Package foundation carries the schema the auth module's tables need.
|
Package foundation carries the schema the auth module's tables need. |
|
Package handoff hands a browser its tokens when a provider sign-in ends somewhere else.
|
Package handoff hands a browser its tokens when a provider sign-in ends somewhere else. |
|
Package identity is the one gate on who may become a person in a deployment.
|
Package identity is the one gate on who may become a person in a deployment. |
|
Package oauth signs people in through an external provider.
|
Package oauth signs people in through an external provider. |
|
Package oauthtest is a stand-in identity provider, so a provider sign-in can be driven in a test or a demonstration without registering an application with anybody.
|
Package oauthtest is a stand-in identity provider, so a provider sign-in can be driven in a test or a demonstration without registering an application with anybody. |
|
Package session issues, verifies, and rotates opaque session tokens.
|
Package session issues, verifies, and rotates opaque session tokens. |