oauth

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 18 Imported by: 0

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

View Source
const (
	ProviderGoogle    = "Google"
	ProviderMicrosoft = "Microsoft"
	ProviderGitHub    = "GitHub"
)

The provider names, which are also the oauth_provider enum's labels.

View Source
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.

func New

func New(cfg Config) (*Handler, error)

New builds a handler.

func (*Handler) Mount

func (h *Handler) Mount(mux *http.ServeMux)

Mount registers the routes.

func (*Handler) Providers

func (h *Handler) Providers() []string

Providers lists the configured providers, for a sign-in page.

type JoinInput

type JoinInput struct {
	TenantID   uuid.UUID
	IdentityID uuid.UUID
	Profile    Profile
}

JoinInput gives a person an account in a tenant.

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 LinkInput

type LinkInput struct {
	IdentityID uuid.UUID
	Provider   string
	Profile    Profile
}

LinkInput attaches a provider account to a person who already exists.

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.

func GitHub

func GitHub(clientID, clientSecret string) Provider

GitHub signs people in with a GitHub account.

func Google

func Google(clientID, clientSecret string) Provider

Google signs people in with a Google or Tenant account.

func Microsoft

func Microsoft(clientID, clientSecret, tenant string) Provider

Microsoft signs people in with a Microsoft or Entra ID account.

The tenant is Microsoft's, not rig's: "common" accepts any account, "organizations" excludes personal ones, and a directory identifier restricts sign-in to one organization. Leave it empty for "common".

type ProvisionInput

type ProvisionInput struct {
	Provider string
	Profile  Profile
}

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.

Jump to

Keyboard shortcuts

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