Documentation
¶
Overview ¶
Package authgate is a login gate, not user accounts: sign in with any OAuth 2.0 or OpenID Connect provider and keep the person's profile in a sealed cookie, with no database, no user table and no auth.New.
It is for an internal tool behind company login, a prototype, a tool that acts on the person's GitHub or Google account, or a serverless function with nowhere to keep rows. Everything the rest of this library stores (users, identities, sessions, devices) does not exist here, and so neither does anything built on it: no per-device sign-out, no passwords, no bans, no admin, no linking of two providers to one person. Revocation is coarse on purpose: every cookie carries an absolute expiry and the SessionVersion it was issued under, so bumping the version (or rotating the secret without keeping the old one) signs everyone out at once. An app that needs any of the missing parts uses auth.New with plugin/oauth instead, and the providers it configured here carry over unchanged.
The providers are plugin/oauth's own (oauth.Google, oauth.GitHub, the presets, oauth.OIDC with discovery), so PKCE, state, the RFC 9207 issuer check and ID-token verification are the code the full library runs. The state is bound to the browser by a sealed __Host- flow cookie, the same login-CSRF defence authhttp's flow cookie gives, without a stored row.
Index ¶
- Constants
- Variables
- type Gate
- type Option
- func Allow(rule func(Profile) bool) Option
- func AllowAnyone() Option
- func AllowEmailDomain(domains ...string) Option
- func AllowEmails(addrs ...string) Option
- func ErrorHandler(fn func(w http.ResponseWriter, r *http.Request, err error)) Option
- func KeepTokens() Option
- func Prefix(path string) Option
- func SessionLifetime(d time.Duration) Option
- func SessionVersion(n uint64) Option
- func SignInPage(path string) Option
- func WithBaseURL(origin string) Option
- func WithClock(now func() time.Time) Option
- func WithHTTPClient(hc *http.Client) Option
- func WithLogger(l *slog.Logger) Option
- func WithProviders(ps ...oauth.Provider) Option
- func WithSecret(current []byte, previous ...[]byte) Option
- type Profile
Constants ¶
const ( // SessionCookieName holds the sealed profile. SessionCookieName = "__Host-authgate_session" // FlowCookieName holds the sealed state, PKCE verifier and return path // of one sign-in between start and callback. FlowCookieName = "__Host-authgate_flow" )
Cookie names. __Host- makes the browser refuse the cookie unless it is Secure, has Path=/ and no Domain, so a sibling subdomain cannot plant or overwrite it. They differ from authhttp's so a gate and a full Auth can run side by side on one host.
const ( // DefaultPrefix is where Handler expects to be mounted. DefaultPrefix = "/auth" // DefaultSessionLifetime is the absolute lifetime of a sign-in: a week, // better-auth's default. It is never extended; the person signs in again // after it, which is also when an Allow rule change or a changed provider // profile reaches the cookie. DefaultSessionLifetime = 7 * 24 * time.Hour // FlowLifetime bounds the time a person may spend at the provider, // plugin/oauth's DefaultStateTTL. FlowLifetime = oauth.DefaultStateTTL // MaxCookieSize is the largest Set-Cookie value (name, value and // attributes) the gate writes: RFC 6265 section 6.1 asks browsers to // accept at least 4096 bytes per cookie, and most stop there. A session // that would not fit (provider tokens kept with KeepTokens are the usual // cause) is refused with ErrCookieTooLarge rather than written and // silently dropped by the browser. MaxCookieSize = 4096 // MaxReturnToLength bounds the returnTo path a start accepts, so the flow // cookie that carries it stays far below MaxCookieSize. MaxReturnToLength = 1024 )
Defaults and limits.
Variables ¶
var ( // ErrNotAllowed is a sign-in or a cookie whose profile no Allow rule // accepts. ErrNotAllowed = auth.NewError("authgate.not_allowed", http.StatusForbidden, "This account is not allowed here.") // ErrCookieTooLarge is a sign-in whose sealed session would exceed // MaxCookieSize. Nothing is written; the log says how large it was. ErrCookieTooLarge = auth.NewError("authgate.cookie_too_large", http.StatusInternalServerError, "The sign-in could not be saved. Contact the site's owner.") // ErrBadReturnTo is a returnTo that is not a path on this site. ErrBadReturnTo = auth.NewError("authgate.bad_return_to", http.StatusBadRequest, "The return address must be a path on this site.") // ErrUnknownProvider is a route naming a provider the gate was not given. ErrUnknownProvider = auth.NewError("authgate.unknown_provider", http.StatusNotFound, "This sign-in method is not offered here.") // ErrCrossSite is a sign-out posted from another site. ErrCrossSite = auth.NewError("authgate.cross_site", http.StatusForbidden, "This request came from another site.") )
Errors a request can answer. The state, denial and provider failures reuse plugin/oauth's, so a client sees the same codes either way.
var ( // ErrNoProviders is New without WithProviders. ErrNoProviders = errors.New("authgate: no providers; pass WithProviders") // ErrNoRule is New without any Allow rule. Letting every account at the // provider in is a real choice for a GitHub tool and a breach for a // company dashboard, so it must be said out loud with AllowAnyone. ErrNoRule = errors.New("authgate: no Allow rule; pass Allow, AllowEmailDomain, AllowEmails or AllowAnyone") // ErrBaseURL is a missing or malformed WithBaseURL: an http or https // origin with no path, query or fragment. ErrBaseURL = errors.New("authgate: WithBaseURL must be an http or https origin such as https://tools.example.com") // ErrDuplicateProvider is two providers with one name, which would share // one callback route. ErrDuplicateProvider = errors.New("authgate: two providers share a name") // ErrBadOption is an option given an unusable value (a non-positive // lifetime, an empty domain, a path that is not one). ErrBadOption = errors.New("authgate: bad option") )
Configuration errors New returns.
Functions ¶
This section is empty.
Types ¶
type Gate ¶
type Gate struct {
// contains filtered or unexported fields
}
Gate is a configured login gate. It is safe for concurrent use and holds no state beyond its configuration: every sign-in lives in the browser's cookies, so any number of instances with the same options serve the same people.
func New ¶
New builds a gate. It refuses a configuration nobody could sign in with or that would let in more than intended: no provider, a provider oauth.ValidateProvider refuses, no secret or a short one, no base URL, and no Allow rule.
func (*Gate) CallbackURL ¶
CallbackURL is the redirect URI to register at the provider named name.
func (*Gate) Handler ¶
Handler serves the gate's routes under the prefix:
GET {prefix}/{provider}/start?returnTo=/path begin a sign-in
GET {prefix}/{provider}/callback the provider's redirect back
POST {prefix}/{provider}/callback a form-post provider's answer, relayed to GET
POST {prefix}/sign-out clear the session cookie (?returnTo=/path)
GET {prefix}/session the signed-in Profile as JSON, or 401
Mount it at the prefix with the trailing slash: mux.Handle("/auth/", g.Handler()).
func (*Gate) Middleware ¶
Middleware reads the session cookie and, when it opens, carries the current SessionVersion, has not expired and an Allow rule still accepts it, puts the Profile in the request context (ProfileFrom). A cookie that fails any check is cleared. It never refuses a request; RequireUser does.
func (*Gate) RequireUser ¶
RequireUser serves next only to a signed-in, allowed person. Anyone else gets, for a browser navigation (GET or HEAD accepting text/html), a redirect to SignInPage or, with one provider, straight to its start, with returnTo the page asked for; for anything else, 401 auth.ErrSignedOut. It runs Middleware itself, so it needs nothing in front of it.
type Option ¶
type Option func(*Gate)
Option configures a Gate.
func Allow ¶
Allow adds an access rule. A profile gets in when any rule returns true, so AllowEmailDomain("example.com") and Allow(isContractor) together let in the company and the contractor. Rules run at sign-in and again on every request that carries a cookie, so removing someone from a rule and deploying shuts them out at once, without waiting for their cookie to expire. A rule must therefore be cheap and must not call the network; it sees only what the cookie holds (the Profile, with Tokens under KeepTokens). Without any rule New fails with ErrNoRule.
func AllowAnyone ¶
func AllowAnyone() Option
AllowAnyone lets in every account the providers sign in: the explicit choice for a tool that acts on the person's own account and guards nothing of the operator's. Any other rule is then redundant.
func AllowEmailDomain ¶
AllowEmailDomain lets in a profile whose email the provider verified and whose domain is one of domains, compared without regard to case. The match is exact: "example.com" does not admit "eu.example.com" (list it too), so a subdomain someone else controls is never let in by accident. An unverified address never matches: anyone can type any address into an account at a provider that does not check it.
func AllowEmails ¶
AllowEmails lets in a profile whose email the provider verified and is one of addrs, compared without regard to case.
func ErrorHandler ¶
ErrorHandler answers every error of Handler and RequireUser's 401 instead of the library's JSON error body, for an app that renders its own page. err is one of this package's errors, auth.ErrSignedOut, or plugin/oauth's ErrStateInvalid, ErrProviderDenied or ErrProviderFailed; auth.ErrorOf(err) gives its code and status.
func KeepTokens ¶
func KeepTokens() Option
KeepTokens keeps the provider's access token, refresh token, expiry and granted scope in the sealed session cookie, for a tool that calls the provider's API as the person (Profile.Tokens). Off by default: a cookie that holds nothing but a profile leaks nothing an attacker can use at the provider. The ID token and the raw token answer are never kept. Tokens are what the provider issued at sign-in; nothing refreshes them into the cookie, so a tool whose access token expires before the session does asks the person to sign in again. Large tokens (Entra ID's can pass 2 KB) can push the cookie over MaxCookieSize, which refuses the sign-in with ErrCookieTooLarge.
func SessionLifetime ¶
SessionLifetime sets how long a sign-in lasts from the moment it happened; the default is DefaultSessionLifetime. It is absolute: activity does not extend it.
func SessionVersion ¶
SessionVersion stamps every cookie with n and accepts only cookies stamped with n. Raising it (from a deploy variable) signs everyone out at once: the gate's only revocation, because there is no store to delete one session from. The default is 0.
func SignInPage ¶
SignInPage is where RequireUser sends a browser that is not signed in, with ?returnTo= the page it asked for. Without it, a gate with exactly one provider sends the browser straight to that provider's start, and a gate with several answers 401.
func WithBaseURL ¶
WithBaseURL sets the origin the gate is served at, such as "https://tools.example.com". The callback URL registered at each provider is this origin, the prefix, then /{provider}/callback. http is accepted for local development on localhost; the cookies stay Secure, which browsers honour on http://localhost.
func WithHTTPClient ¶
WithHTTPClient sets the client used to reach the providers; the default times out after oauth.DefaultHTTPTimeout.
func WithLogger ¶
WithLogger sets where refused sign-ins and provider failures are logged; the default is slog.Default.
func WithProviders ¶
WithProviders sets the providers people may sign in with: any oauth.Provider, such as oauth.Google, oauth.GitHub, a preset or oauth.OIDC. Each is checked by oauth.ValidateProvider, the check plugin/oauth runs.
func WithSecret ¶
WithSecret sets the secret the cookie keys are derived from, by the same HKDF rule as auth.WithSecret under the label auth.KeyLabelAuthGate. It is required and at least auth.MinSecretSize bytes. Rotation: pass the new secret first and the old ones after; cookies sealed under an old one still open until they expire. Dropping a secret signs out everyone it sealed.
type Profile ¶
type Profile struct {
// Provider is the name of the provider signed in with, such as "google".
Provider string `json:"provider"`
// Subject is the provider's stable id for the person, never the email.
Subject string `json:"subject"`
// Email is the address the provider gave, "" when it gave none.
Email string `json:"email,omitempty"`
// EmailVerified is the provider's claim that the person controls Email.
// AllowEmailDomain and AllowEmails admit only a verified address.
EmailVerified bool `json:"emailVerified"`
Name string `json:"name,omitempty"`
Image string `json:"image,omitempty"`
// SignedInAt is when the sign-in happened; ExpiresAt is SignedInAt plus
// SessionLifetime, after which the cookie is refused.
SignedInAt time.Time `json:"signedInAt"`
ExpiresAt time.Time `json:"expiresAt"`
// Tokens are the provider's tokens under KeepTokens: AccessToken,
// RefreshToken, ExpiresAt, RefreshExpiresAt and Scope. Zero without
// KeepTokens. Never written to JSON; the tokens are auth.Token, which
// prints as "[redacted]" anyway.
Tokens oauth.TokenSet `json:"-"`
}
Profile is the signed-in person as the provider described them at sign-in, read back from the sealed session cookie. There is no user id of the gate's own: ID (provider and subject) is the stable key an app keys its own data by.
func CurrentProfile ¶
CurrentProfile is ProfileFrom(r.Context()).
func ProfileFrom ¶
ProfileFrom returns the signed-in Profile Middleware or RequireUser put in ctx, and false when the request carried no valid, allowed, unexpired session cookie.