Documentation
¶
Overview ¶
Package authhttp mounts the authentication endpoints.
It is deliberately thin. Every decision worth arguing about was made in github.com/simonjanss/rig/auth/account, github.com/simonjanss/rig/auth/session, and github.com/simonjanss/rig/auth/apikey; what is here is JSON, status codes, and the one piece of real logic an HTTP layer owns — deciding what a request's credential is and turning it into claims.
That last part, Handler.Claims, is the integration point: it is what a generated server's Server.GetClaims is set to, and it is why an application never writes authentication middleware of its own.
Index ¶
- Constants
- func Permissions() []tenancy.Permission
- type Config
- type Grants
- type Handler
- func (h *Handler) Claims(r *http.Request) (tenancy.Claims, error)
- func (h *Handler) Mount(mux httpx.Router)
- func (h *Handler) Session(r *http.Request) (*session.Token, error)
- func (h *Handler) SignIn(w http.ResponseWriter, r *http.Request, in oauth.SignIn) error
- func (h *Handler) SignInToBrowser(to *handoff.Handoff) func(w http.ResponseWriter, r *http.Request, in oauth.SignIn) error
Constants ¶
const ( // PermissionManageAPIKeys is required for a key that acts as a service // account — a separate identity with scopes of its own — and for seeing or // revoking anybody's key but your own. // // This is the administrative half, because minting a service key is creating // authority: the key acts as an account nobody signs in to, and what it may do // is whatever scopes it was given. PermissionManageAPIKeys = "apikey.manage" // PermissionOwnAPIKey is required to mint a key that acts as yourself. // // A separate, much cheaper permission, and the reason is in the verification // path: a personal key's permissions are intersected with its owner's every // time it is used, so it can never do anything its owner cannot. Minting one // grants nothing new — it is a second way to present authority somebody already // holds, like a password manager entry. Ordinary members can have this without // being able to create a service account. PermissionOwnAPIKey = "apikey.own" // PermissionImpersonate is required to act as somebody else. PermissionImpersonate = "account.impersonate" // PermissionReadAuthLogAll is required to read the tenant's authentication // trail rather than only your own events. PermissionReadAuthLogAll = "authlog.read.all" // PermissionReadSessionsAll is required to see every session open in the // tenant rather than only your own. PermissionReadSessionsAll = "session.read.all" // PermissionRevokeSessionsAll is required to end somebody else's session. PermissionRevokeSessionsAll = "session.revoke.all" )
Permissions the endpoints check.
Variables ¶
This section is empty.
Functions ¶
func Permissions ¶
func Permissions() []tenancy.Permission
Permissions are the keys this package's own endpoints check.
They belong beside the endpoints that check them rather than in a list an application maintains: minting a key and inviting somebody are things the auth package does, so the keys they require are its to declare. A reconcile unions them with the application's own, which is why a project passes only its generated catalogue and still ends up with a complete table.
Types ¶
type Config ¶
type Config struct {
Accounts *account.Service
Sessions *session.Manager
// APIKeys is optional. Without it the key endpoints are not mounted and a
// key presented as a credential is refused, which is the right behavior for
// a project that skipped that part of the foundation.
APIKeys *apikey.Manager
// AuditLog reads what was recorded. Nil leaves GET <base>/audit unmounted —
// absent rather than answering 403, so there is nothing to probe, which is
// the same choice registration and tenant creation make.
//
// A reader and not the [github.com/simonjanss/rig/auth/authlog.Log] every
// other part of this package writes to. The write path discards its failures
// on purpose and a read cannot, so they are two interfaces; one
// implementation usually satisfies both.
AuditLog authlog.Reader
// Tenant resolves which tenant a request belongs to.
//
// There is no default, because there is no sensible one: a subdomain, a
// header, and a path segment are all reasonable and only the application
// knows which it uses. Guessing would mean guessing wrong for somebody.
Tenant func(*http.Request) (uuid.UUID, error)
// Identities verifies the tenant-less credential the tenant picker runs
// on. Nil leaves the picker endpoints unmounted and refuses an identity
// token, which is right for a project where every session names a tenant.
Identities *session.IdentityManager
// AllowTenantCreation mounts POST /auth/tenants, where somebody holding an
// identity session makes one and is signed into it as its Owner.
//
// Off by default. What a new tenant consists of beyond the row and its first
// account is [account.TenantOptions.OnCreated]; whether anybody may make one
// at all is [account.TenantOptions.Allow]. This only decides whether the route
// exists — off means absent rather than 403, so there is nothing to probe.
AllowTenantCreation bool
// AllowEmailCode mounts POST /auth/email-code and
// POST /auth/email-code/verify, the sign-in for somebody with no account at
// a configured provider.
//
// Off by default. Off means the endpoints do not exist rather than
// answering 403, so nothing is there to probe — and a deployment with no
// provider and this off has no way in at all, which rig does not refuse
// because a service that only provisions and invites is a legitimate thing
// to build.
//
// Whether a code may go to an address rig has never seen is a separate
// decision, and it lives on the account service:
// [github.com/simonjanss/rig/auth/account.EmailCodeOptions.AllowProvisioning].
AllowEmailCode bool
// Grants fills in a caller's roles and permissions. Nil means every
// authenticated caller has none, which makes any endpoint with a permission
// requirement answer 403 — correct, and confusing enough that it is worth
// saying here.
Grants Grants
// BasePath prefixes every route. It defaults to /auth.
BasePath string
// OnError renders a failure. Nil uses a small default; an application that
// already has an error shape should pass its own so that authentication
// failures look like every other failure.
OnError func(w http.ResponseWriter, r *http.Request, err error)
// TrustedProxies are the networks whose X-Forwarded-For headers may be
// believed.
//
// Empty means none, and that is the safe default: an address read out of a
// header a client controls is an address a client chooses, and rate limits
// keyed on it are rate limits an attacker can walk straight around.
TrustedProxies []netip.Prefix
}
Config builds a handler.
type Grants ¶
type Grants func(ctx context.Context, tenantID, accountID uuid.UUID) (roles, permissions []string, err error)
Grants answers what an account may do, in the tenant the request is for.
A function, and the only thing rig asks of an application's authorization model. Where the answer comes from is not rig's business: a table of roles, a directory, a claim on a federated token, a switch on the account's level, or a constant while a project is young. rig derives the permission *keys* from the schema and generates the check that reads them; deciding who holds which is the application's, because no two products agree on it and a framework that picked one would be wrong for everybody else.
It runs when a request arrives rather than at sign-in, which is what makes revoking access take effect on the next call instead of whenever the session happens to refresh. That also puts it on the hot path, and usually makes it the expensive read there — `auth.NewGrantsCache` wraps one of these in a cache a revocation can reach, in exchange for publishing an invalidation wherever roles change. A bare time-to-live over this is a permission somebody took away that goes on working, so rig offers the channel rather than the timer.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler serves the authentication endpoints.
func (*Handler) Claims ¶
Claims resolves a request's credential.
This is what a generated server's Server.GetClaims is set to. It accepts an access token or an API key in the Authorization header and tells them apart by prefix, which is why the prefixes exist.
Roles are resolved here, per request, rather than baked into the token: a role taken away should stop working now, not whenever the session next happens to refresh.
func (*Handler) Session ¶
Session returns the token behind a request, for a handler that needs the session itself rather than the claims — ending an impersonation, for one.
func (*Handler) SignIn ¶
SignIn finishes a provider sign-in with the same last step, and the same body, a password login gets.
It is the default github.com/simonjanss/rig/auth/oauth.Config.OnSignIn, so that a project which only wants "sign in with Google" to work does not have to write the last step itself. A browser flow usually wants a cookie and a redirect instead, which is exactly the sort of decision that stays with the application.
The work is github.com/simonjanss/rig/auth/account.Service.SignInIdentity rather than a session issue of its own, and that is what lets a provider sign-in reach the cases a login already answers: a tenant it named, one of several the person belongs to, or none at all — in which case the answer is 200 with an identity token, an empty tenant list and no session, and the picker takes it from there.
It issues through the manager the account service holds rather than Config.Sessions directly. Through github.com/simonjanss/rig/auth.New those are the same one.
func (*Handler) SignInToBrowser ¶ added in v0.5.1
func (h *Handler) SignInToBrowser( to *handoff.Handoff, ) func(w http.ResponseWriter, r *http.Request, in oauth.SignIn) error
SignInToBrowser finishes a provider sign-in for a front end that is not this API, by leaving the tokens in a cookie and redirecting to it.
It is the other built-in ending, and it is what a single-page application actually needs: Handler.SignIn's body is right for curl and useless to a browser that has just followed a redirect, which can neither read it nor do anything with what it says. github.com/simonjanss/rig/auth.New selects this one when a project names a web origin and writes no github.com/simonjanss/rig/auth/oauth.Config.OnSignIn of its own.
The last step is the same github.com/simonjanss/rig/auth/account.Service.SignInIdentity the JSON ending runs — deliberately, and through the same unexported constructor, so that the two endings cannot drift into disagreeing about what a provider sign-in is. Only the answer differs.
A refusal is returned rather than redirected here, even though the redirect is what the browser needs. Returning it reaches github.com/simonjanss/rig/auth/oauth.Config.OnError through the same path every other failure in the flow takes — and that path writes the OAuthSignIn/Failed entry first. Rendering the redirect here instead would answer the person correctly and cost the audit trail the one record that a provider authenticated somebody this application then turned away. auth.New wires that hook to handoff.Handoff.Fail.