Documentation
¶
Overview ¶
Package oauth signs people in through an external provider.
Three decisions here are worth more than the rest of the package put together.
A person is matched on the provider's subject, never on the email address. Subjects are stable for the life of an account; addresses change, and providers hand a released address to somebody else. Matching on the address is how one person ends up signed in as another.
An existing person is only ever linked to a provider account when the provider says the address is verified. Without that check, anybody who can register your address at any supported provider owns your account here.
And who somebody is, is a separate question from whether they belong here. The first is global — one provider link, one identity, however many tenants — and the second is per tenant and answered no by default. A provider will authenticate anybody with a Google account, so joining a tenant is gated by Config.AllowProvisioning and by the tenant's own list of allowed domains.
Index ¶
Constants ¶
const ( ProviderGoogle = "Google" ProviderMicrosoft = "Microsoft" ProviderGitHub = "GitHub" )
The provider names, which are also the oauth_provider enum's labels.
const DefaultStateTTL = 10 * time.Minute
DefaultStateTTL bounds a sign-in round trip.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
Store Store
Providers []Provider
// BaseURL is the origin callbacks come back to, for example
// https://app.example.com. It must match what is registered with the
// provider, exactly.
BaseURL string
// BasePath defaults to /auth/oauth.
BasePath string
// Origin overrides BaseURL for one request, for an application served at more
// than one origin — which is exactly what host-based tenancy is:
// acme.example.com and beta.example.com are the same application, and a
// sign-in that started at one has to come back to it. The state cookie is
// host-only, so a callback that landed on the other host would arrive without
// it and be refused.
//
// Nil means BaseURL, which is right for the single-origin deployment most
// applications are.
//
// The constraint this has to live inside is the provider's, not rig's: a
// redirect URI is registered exactly, and few providers accept a wildcard. So
// every origin this returns has to be registered with every provider. A
// deployment with a tenant per subdomain and a provider that will not take a
// wildcard keeps the callback on one canonical host instead, and hands the
// finished session on to the tenant's own host itself — which is what
// OnSignIn is for.
Origin func(r *http.Request) string
// SigningKey signs the cookie that carries the state and the PKCE verifier
// across the round trip.
//
// Required, and at least 32 bytes. The alternative is a table of pending
// sign-ins to clean up; a signed cookie holds the same three values, needs
// no storage, and cannot be forged.
SigningKey []byte
// Tenant resolves which tenant is signing in.
Tenant func(*http.Request) (uuid.UUID, error)
// OnSignIn finishes the request once an identity is resolved.
//
// It writes the response: set a cookie, redirect with a token, render a
// page. rig does not choose, because the choice depends on whether the
// client is a browser or a native application, and only the application
// knows.
OnSignIn func(w http.ResponseWriter, r *http.Request, in SignIn) error
// AllowedReturnTo are the origins a sign-in may redirect to when it
// finishes. A path on this origin is always allowed; anything else has to
// be listed, because an unchecked returnTo is an open redirect and an open
// redirect on a sign-in endpoint wears your domain in a phishing link.
AllowedReturnTo []string
// AllowProvisioning creates an account for somebody with no existing one.
//
// Off by default. An open sign-in endpoint on a business application is a
// way for anybody with a Google account to appear inside a customer's
// tenant, which is rarely what anyone wants and never what they expect.
AllowProvisioning bool
Log authlog.Log
// StateTTL bounds how long a sign-in may take. Ten minutes is generous for
// a redirect and short enough that a stolen state is useless by the time
// anybody notices it.
StateTTL time.Duration
// Insecure allows the state cookie over plain HTTP. It is for local
// development and nothing else.
Insecure bool
Now func() time.Time
}
Config builds a handler.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler serves the sign-in routes.
type Link ¶
type Link struct {
ID uuid.UUID
IdentityID uuid.UUID
Provider string
Subject string
EmailAddress string
}
Link is a provider account attached to a person.
It hangs off the identity and carries no tenant: one Google account is one Google account, so linking it once means "sign in with Google" works for every tenant that person belongs to.
type Profile ¶
type Profile struct {
// Subject is the provider's stable identifier. It is the only field an
// identity is matched on.
Subject string
EmailAddress string
// EmailVerified is the provider's claim that the address belongs to this
// person. An unverified address is never enough to link an existing
// account, and this is the field that decides it.
EmailVerified bool
DisplayName string
}
Profile is what a provider says about somebody.
type Provider ¶
type Provider struct {
// Name must match a label of the oauth_provider enum, since it is what
// gets stored. The built-in constructors get this right.
Name string
ClientID string
ClientSecret string
Scopes []string
Endpoint oauth2.Endpoint
// UserInfoURL is fetched with the access token to read a profile.
UserInfoURL string
// Parse turns the provider's answer into a profile. Every provider spells
// the same three facts differently, and this is where that ends.
Parse func(body []byte) (Profile, error)
// Extra runs after Parse when a provider needs a second call. GitHub does:
// its user endpoint returns a null address for anybody who kept theirs
// private.
Extra func(ctx context.Context, client *http.Client, p *Profile) error
}
Provider is one place people can sign in from.
It is a struct rather than an interface because the differences between providers are data — three URLs and a way to read a profile — and an interface would make adding one a new type instead of a literal.
type ProvisionInput ¶
ProvisionInput creates a person the application has not seen before, and links them, in one step.
type SignIn ¶
type SignIn struct {
// Link is the provider account that was used, and Link.IdentityID is who it
// belongs to.
Link *Link
// TenantID and AccountID are the session to issue. The account is the
// person's row in this tenant, which is what claims are made of.
TenantID uuid.UUID
AccountID uuid.UUID
Profile Profile
Provider string
// New reports that this sign-in created the account, so an application can
// send a welcome message or run onboarding. It is true for somebody joining
// their second tenant as well as their first: the account is new either
// way, which is what onboarding is about.
New bool
// ReturnTo is where the caller asked to be sent afterwards, already
// checked against the allow-list. Empty when none was asked for.
ReturnTo string
}
SignIn is what a completed sign-in hands to the application.
type Store ¶
type Store interface {
// FindLink returns the link for a provider subject, or nil. It is not
// scoped to a tenant: a provider account belongs to a person.
FindLink(ctx context.Context, provider, subject string) (*Link, error)
// FindIdentityByEmail returns the identifier of the person with that
// address, or uuid.Nil. It is what makes "sign in with Google" reach the
// person who already signed up with a password, rather than making a second
// one beside them.
FindIdentityByEmail(ctx context.Context, lowercased string) (uuid.UUID, error)
// LinkIdentity records the connection.
LinkIdentity(ctx context.Context, in LinkInput) (*Link, error)
// ProvisionIdentity creates the person and links them in one step.
ProvisionIdentity(ctx context.Context, in ProvisionInput) (*Link, error)
// FindAccount returns a person's account in one tenant, or uuid.Nil when
// they do not belong to it.
FindAccount(ctx context.Context, tenantID, identityID uuid.UUID) (uuid.UUID, error)
// JoinTenant creates an account for a person in a tenant. It must honour
// the tenant's allowed email domains: a provider will authenticate anybody,
// so this is the door.
JoinTenant(ctx context.Context, in JoinInput) (uuid.UUID, error)
}
Store is the persistence a sign-in needs.
An application implements it over the generated repositories for identity, rig_identity_oauth and rig_account, all of which `rig setup-project` creates.
The two halves are deliberately separate calls. Who somebody is, is global — one address, one provider link — and whether they belong here is per tenant, so a person signing in to their second tenant goes through the first half unchanged and only the second half decides anything.