Documentation
¶
Overview ¶
Package auth is a shared authentication and authorization module for Go APIs: password hashing, sessions, OAuth sign-in, two-factor authentication, and role-based access control.
Its exported API follows semantic versioning: nothing is removed or changed until v2.
Index ¶
- Constants
- Variables
- func Can(ctx context.Context, permission rbac.Permission) bool
- func Migrate(ctx context.Context, pool *pgxpool.Pool) error
- type Actor
- type Apple
- type Auth
- func (a *Auth) ActorOf(r *http.Request) Actor
- func (a *Auth) Authenticated(h http.Handler) http.Handler
- func (a *Auth) Cleanup(ctx context.Context) error
- func (a *Auth) Close(ctx context.Context) error
- func (a *Auth) CreateOrganization(ctx context.Context, name string, ownerID uuid.UUID, by Actor) (Organization, error)
- func (a *Auth) Events(ctx context.Context, userID uuid.UUID, cursor string) (EventPage, error)
- func (a *Auth) Protect(mux *http.ServeMux) http.Handler
- func (a *Auth) Public(h http.Handler) http.Handler
- func (a *Auth) Register(mux *http.ServeMux)
- func (a *Auth) Require(permission rbac.Permission, h http.Handler) http.Handler
- func (a *Auth) RequireOrg(permission rbac.Permission, h http.Handler) http.Handler
- func (a *Auth) ResetTwoFactor(ctx context.Context, userID uuid.UUID, by Actor) error
- func (a *Auth) SetAppRole(ctx context.Context, userID uuid.UUID, role string, by Actor) error
- func (a *Auth) SetTwoFactorRequired(ctx context.Context, userID uuid.UUID, required bool, by Actor) error
- type Config
- type EmailData
- type Emails
- type Event
- type EventPage
- type GitHub
- type Google
- type Hooks
- type OAuth
- type Org
- type Organization
- type Session
- type User
Constants ¶
const ( // EventLoginSucceeded is a session started. Metadata "method" is // "password", "verification_link", "two_factor", "google", "github", // or "apple". EventLoginSucceeded = "login_succeeded" // EventLoginFailed is a wrong password or 2FA code at login. Metadata // "method" is "password" or "two_factor". UserID is nil for an email // without an account. EventLoginFailed = "login_failed" EventLogout = "logout" EventLogoutEverywhere = "logout_everywhere" // EventSessionEnded is a user ending one of their other sessions. EventSessionEnded = "session_ended" EventPasswordChanged = "password_changed" EventPasswordReset = "password_reset" EventTwoFactorEnabled = "two_factor_enabled" EventTwoFactorDisabled = "two_factor_disabled" EventTwoFactorReset = "two_factor_reset" EventRecoveryCodeUsed = "recovery_code_used" EventRecoveryCodesRegenerated = "recovery_codes_regenerated" // EventOAuthLinked and EventOAuthUnlinked have metadata "provider". EventOAuthLinked = "oauth_linked" EventOAuthUnlinked = "oauth_unlinked" EventEmailChanged = "email_changed" // EventAppRoleChanged has metadata "role" ("" when removed). EventAppRoleChanged = "app_role_changed" // EventOrgRoleChanged and EventMemberAdded have metadata // "organization_id" and "role"; EventMemberRemoved has // "organization_id". ActorID is who made the change. EventOrgRoleChanged = "org_role_changed" EventMemberAdded = "member_added" EventMemberRemoved = "member_removed" // EventTwoFactorRequiredChanged has metadata "required" ("true" or // "false"). EventTwoFactorRequiredChanged = "two_factor_required_changed" )
Event types in the security event log. More may be added; existing ones never change.
const ( PermOrganizationUpdate rbac.Permission = "organization:update" PermMembersRead rbac.Permission = "members:read" PermMembersManage rbac.Permission = "members:manage" )
Permissions the module's own organization endpoints use. Projects give them to roles like any other permission; owners always have them.
const InvitationLifetime = 7 * 24 * time.Hour
InvitationLifetime is how long an invitation link works.
const OrgHeader = "X-Organization-ID"
OrgHeader names the organization a RequireOrg request is for.
const OwnerRole = "owner"
OwnerRole is the organization role that always has every permission. Config.OrgRoles must define it, and every organization keeps at least one owner.
const Schema = migrate.Schema
Schema is the Postgres schema that holds every table the module owns.
Variables ¶
var ErrBadCursor = errors.New("auth: bad cursor")
ErrBadCursor means a cursor passed to Events wasn't one it returned.
var ErrInvalidOrgName = errors.New("auth: organization name must be 1 to 200 characters")
ErrInvalidOrgName is returned by CreateOrganization for a blank name, or one over 200 characters.
var ErrSignUpNotAllowed = errors.New("auth: sign-up not allowed")
ErrSignUpNotAllowed is returned by Hooks.OnUserCreated to refuse a sign-up, such as one from an address that isn't on the project's allowlist. The sign-up is rolled back and logged at Info, not as an error.
With email and password in verify first mode (the default), the response is exactly the one for an accepted sign-up, and no email is sent, so the endpoint doesn't reveal which addresses are allowed. With AllowUnverifiedLogin an accepted sign-up logs the user in, which can't be imitated, so a refused one gets a 403 sign_up_not_allowed. A sign-up with a provider (OAuth) also gets a 403 sign_up_not_allowed: the provider has already shown that the user owns the address.
Functions ¶
func Can ¶
func Can(ctx context.Context, permission rbac.Permission) bool
Can reports whether the current user has permission: against their organization role under RequireOrg, or their app role under Require. It's for finer checks inside a handler, such as whether to offer a delete button. It's false anywhere else.
Types ¶
type Actor ¶
type Actor struct {
// UserID is the user making the change.
UserID *uuid.UUID
// IP and UserAgent are the client they made it from, if known.
IP *netip.Addr
UserAgent string
}
Actor is who makes a change through one of Auth's admin methods (SetAppRole, SetTwoFactorRequired, ResetTwoFactor, and CreateOrganization), and from where, for the security event log. Admin tooling handling a request gets one with ActorOf. The zero Actor records no one, for seed scripts and changes the system makes itself.
type Apple ¶
type Apple struct {
// BundleIDs are the iOS apps' bundle IDs, such as "com.example.app".
// Identity tokens issued to any other app are refused.
BundleIDs []string
}
Apple is native Sign in with Apple, for the iOS app.
type Auth ¶
type Auth struct {
// contains filtered or unexported fields
}
Auth serves the module's endpoints and protects the project's own. Create it with New.
func New ¶
New validates cfg and returns a ready Auth. It doesn't touch the database; call Migrate first.
func (*Auth) ActorOf ¶
ActorOf returns the logged-in user making r, with their IP and user agent read the same way as for the module's own endpoints. Without a logged-in user, UserID is nil.
func (*Auth) Authenticated ¶
Authenticated wraps h so it only runs for a request with a valid session token in its Authorization: Bearer header. Otherwise it responds 401 with the standard error format. Inside h, UserFrom and SessionFrom return the logged-in user and their session.
A user who must have 2FA but hasn't set it up gets 403 two_factor_setup_required, here and under Require and RequireOrg.
func (*Auth) Cleanup ¶
Cleanup deletes what has expired: sessions, email links, 2FA challenges and old wrong-code records, OAuth flows, used Apple sign-in nonces, ended rate-limit windows and login delays, and security events over a year old. Apart from the events, they're already refused, so this only frees space.
A background job runs it every hour, unless Config.DisableCleanup is set; then call it on your own schedule. Running it from several API instances at once is harmless.
func (*Auth) Close ¶
Close stops the cleanup job and waits for emails that are still being prepared or sent, until ctx is done. Call it when the server shuts down, after it stops accepting requests.
func (*Auth) CreateOrganization ¶
func (a *Auth) CreateOrganization(ctx context.Context, name string, ownerID uuid.UUID, by Actor) (Organization, error)
CreateOrganization creates an organization with ownerID as its owner. It's for apps that create organizations themselves, e.g. when a client signs up, typically with Config.DisableOrganizationCreation set. by is who's creating it, for the security event log.
func (*Auth) Events ¶
Events returns a page of a user's security events, newest first, 50 at a time. Pass "" for the first page, then each page's NextCursor. It's for admin tooling and support; there's no endpoint for it.
func (*Auth) Protect ¶
Protect wraps the project's whole router and enforces deny by default: a request whose route wasn't wrapped by Public, Authenticated, Require, or RequireOrg gets a 500 and an error in the log, and the route's handler never runs. A forgotten permission makes a route unusable instead of open to everyone.
The access rule must be the outermost wrapper on each route: a.RequireOrg("x:y", logging(h)), not logging(a.RequireOrg("x:y", h)). Middleware for every route goes around Protect instead.
func (*Auth) Public ¶
Public marks h as intentionally open to logged-out users. It doesn't change how h behaves; it makes the choice explicit where routes are registered, which Protect requires.
func (*Auth) Register ¶
Register adds the module's endpoints to mux. Defining a route the module already owns makes mux panic at startup, which surfaces the clash immediately.
func (*Auth) Require ¶
Require wraps h so it only runs for a logged-in user whose app role (Config.AppRoles) grants permission. Otherwise it responds 401 or 403 forbidden.
It panics if no app role lists permission, which catches a misspelled permission when the route is registered rather than denying it silently.
func (*Auth) RequireOrg ¶
RequireOrg wraps h so it only runs for a logged-in member of the organization named in the X-Organization-ID header, whose role there (Config.OrgRoles) grants permission. It responds 400 if the header is missing or malformed, 404 if the organization doesn't exist or the user isn't a member (so outsiders can't tell which), and 403 forbidden if the role lacks the permission. Inside h, OrgFrom returns the organization.
It panics if Config.OrgRoles isn't set, or if no role lists permission.
func (*Auth) ResetTwoFactor ¶
ResetTwoFactor turns off a user's 2FA and ends all their sessions. It's for admin tooling and support, for a user who has lost both their authenticator and their recovery codes, after confirming who they are some other way. There's no endpoint for it. The user is emailed that 2FA was turned off. It does nothing for a user without 2FA. by is who's resetting it, for the security event log.
func (*Auth) SetAppRole ¶
SetAppRole gives a user an app role from Config.AppRoles, replacing any role they had. An empty role removes it. It's for seed scripts and admin tooling; there's no endpoint for it. by is who's making the change, for the security event log.
func (*Auth) SetTwoFactorRequired ¶
func (a *Auth) SetTwoFactorRequired(ctx context.Context, userID uuid.UUID, required bool, by Actor) error
SetTwoFactorRequired sets whether a user must have 2FA even without one of Config.TwoFactorRoles. It's for admin tooling, for individual users an admin marks. Until they set 2FA up, they can only set it up, read GET /v1/users/me, and log out. It takes effect on their next request. by is who's making the change, for the security event log.
type Config ¶
type Config struct {
// DB is the project's Postgres pool. Run Migrate on it first.
DB *pgxpool.Pool
// TrustedProxies are the networks allowed to set X-Forwarded-For and
// X-Forwarded-User-Agent, normally the Next.js server on Railway's
// private network. Requests from anywhere else use the connection's
// own address and User-Agent.
TrustedProxies []netip.Prefix
Hooks Hooks
// Logger receives the module's logs. Default: slog.Default().
Logger *slog.Logger
// Now returns the current time. Default: time.Now. Set it in tests to
// control session expiry.
Now func() time.Time
// Mailer sends the module's emails. Required. Use mail/resend in
// production.
Mailer mail.Mailer
// AppURL is the web app's base URL, for links in emails, e.g.
// "https://app.example.com". Required. It must use https, except on
// localhost.
AppURL string
// AppName appears in email subjects and text, e.g. "Acme", and as the
// issuer in authenticator apps. Required.
AppName string
// EncryptionKey encrypts 2FA secrets: 32 random bytes, base64-encoded,
// from an environment variable such as AUTH_ENCRYPTION_KEY. Generate one
// with `openssl rand -base64 32`. Required. Keep a backup: if it's lost,
// every user has to set up 2FA again.
EncryptionKey string
// AllowUnverifiedLogin lets users log in before verifying their email,
// and makes sign-up log them in immediately. Off by default: new users
// must verify their email, with their password, before they can log
// in. See docs/api.md.
AllowUnverifiedLogin bool
// Emails overrides the default email templates.
Emails Emails
// DisableBreachCheck turns off checking new passwords against Have I
// Been Pwned. The check is on by default; it sends only the first 5
// characters of the password's SHA-1 hash, and is skipped if the API
// doesn't answer within 2 seconds.
DisableBreachCheck bool
// OrgRoles are the roles a user can have in an organization, checked
// by RequireOrg. They must include "owner", which always has every
// permission. Leave nil if the app has no organizations.
OrgRoles rbac.Roles
// DisableOrganizationCreation removes POST /v1/organizations, for apps
// that create organizations themselves with CreateOrganization.
DisableOrganizationCreation bool
// AppRoles are roles that apply across the whole app, such as staff
// roles, checked by Require. Users have no app role until SetAppRole
// gives them one. Leave nil if the app doesn't need them.
AppRoles rbac.Roles
// TwoFactorRoles are the privileged roles (the auth standard §6): staff
// roles such as "support" always, and "owner" and "admin" when
// organizations are shared by teams. Apps where each user gets their
// own workspace may leave owner and admin out. A user with one of them,
// in any organization, must have 2FA, and their sessions last at most
// 12 hours. Until they set 2FA up, they can only set it up, read
// GET /v1/users/me, and log out. Each name must be defined in OrgRoles
// or AppRoles.
TwoFactorRoles []string
// OAuth turns on signing in with Google and GitHub.
OAuth OAuth
// DisableCleanup stops the module deleting expired data every hour in
// the background. Call Auth.Cleanup on your own schedule instead.
DisableCleanup bool
// DisableNewDeviceEmails stops emailing users when they log in from a
// browser or app and network they haven't used in the last 90 days.
DisableNewDeviceEmails bool
}
Config configures the module. DB, Mailer, AppURL, AppName, and EncryptionKey are required.
type EmailData ¶
type EmailData struct {
// AppName is Config.AppName.
AppName string
// AppURL is Config.AppURL, for links that don't need a token, such as
// the page to request a password reset.
AppURL string
// Link is the email's action link, with its token.
Link string
// ExpiresIn is how long Link works.
ExpiresIn time.Duration
// OrgName is the organization's name, for invitations.
OrgName string
// RecoveryCodesLeft is how many unused recovery codes the user has,
// for RecoveryCodeUsed.
RecoveryCodesLeft int
// Provider is the sign-in provider's name, such as "Google", for
// OAuthLinked and OAuthUnlinked.
Provider string
// Device is the browser or app's user agent, as it sent it, for
// NewDeviceLogin. It's empty if it sent none. It comes from the client,
// so treat it as untrusted text.
Device string
// LoggedInAt is when the login happened, in UTC, for NewDeviceLogin.
LoggedInAt time.Time
}
EmailData is what an email template can use.
type Emails ¶
type Emails struct {
// VerifyEmail asks a new user to confirm their address.
VerifyEmail func(EmailData) (subject, text, html string)
// SignUpAttempt tells the owner of an existing account that someone
// tried to sign up with their address. Link is a password reset link.
SignUpAttempt func(EmailData) (subject, text, html string)
// PasswordReset sends a password reset link.
PasswordReset func(EmailData) (subject, text, html string)
// PasswordChanged tells a user their password was changed or reset.
// Link is empty.
PasswordChanged func(EmailData) (subject, text, html string)
// EmailChange asks a user to confirm a new address. It's sent to the
// new address.
EmailChange func(EmailData) (subject, text, html string)
// EmailChanged tells the old address that the account's email was
// changed. Link is empty.
EmailChanged func(EmailData) (subject, text, html string)
// Invitation invites someone to join an organization. OrgName is set.
Invitation func(EmailData) (subject, text, html string)
// TwoFactorEnabled, TwoFactorDisabled, and RecoveryCodesRegenerated
// tell a user about changes to their 2FA. Link is empty.
TwoFactorEnabled func(EmailData) (subject, text, html string)
TwoFactorDisabled func(EmailData) (subject, text, html string)
RecoveryCodesRegenerated func(EmailData) (subject, text, html string)
// RecoveryCodeUsed tells a user a recovery code was used to log in or
// to reset their password.
// RecoveryCodesLeft is set. Link is empty.
RecoveryCodeUsed func(EmailData) (subject, text, html string)
// TwoFactorGuessing warns a user that someone got past their password
// (or into their email, for a password reset) but then entered too
// many wrong 2FA codes. Link is empty.
TwoFactorGuessing func(EmailData) (subject, text, html string)
// OAuthLinked and OAuthUnlinked tell a user a sign-in provider was
// added to or removed from their account. Provider is set.
OAuthLinked func(EmailData) (subject, text, html string)
OAuthUnlinked func(EmailData) (subject, text, html string)
// NewDeviceLogin tells a user their account was logged in to from a
// browser or app and network it hasn't been used from in the last 90
// days. Device and LoggedInAt are set. Link is empty.
NewDeviceLogin func(EmailData) (subject, text, html string)
}
Emails overrides the module's default emails. Each field returns an email's subject, plain-text body, and optional HTML body. A nil field uses the default: short, plain text, and without branding.
type Event ¶
type Event struct {
ID uuid.UUID
// UserID is who it happened to. It's nil for a failed login with an
// unknown email, and once the user is deleted.
UserID *uuid.UUID
// ActorID is who did it, when that's someone else.
ActorID *uuid.UUID
// Type is one of the Event constants.
Type string
IP *netip.Addr
UserAgent string
Metadata map[string]string
CreatedAt time.Time
}
Event is one entry in the security event log. It never contains passwords, tokens, codes, or email addresses.
type EventPage ¶
type EventPage struct {
Events []Event
// NextCursor fetches the next page with Events; it's empty on the
// last page.
NextCursor string
}
EventPage is a page of a user's events, newest first.
type GitHub ¶
type GitHub struct {
ClientID string
// ClientSecret comes from an environment variable, such as
// GITHUB_CLIENT_SECRET.
ClientSecret string
}
GitHub is a GitHub OAuth app.
type Google ¶
type Google struct {
ClientID string
// ClientSecret comes from an environment variable, such as
// GOOGLE_CLIENT_SECRET.
ClientSecret string
}
Google is a Google OAuth client, of type "Web application".
type Hooks ¶
type Hooks struct {
// OnUserCreated runs inside the sign-up transaction. Returning an error
// cancels the sign-up, so a project's own rows (e.g. a profile) are
// created together with the user or not at all.
//
// The hook must write through tx, not the project's own pool: writes
// through the pool aren't part of the transaction and wouldn't be
// undone. Keep it quick, since the transaction stays open while it
// runs.
//
// To refuse a sign-up, for example because the address isn't on an
// allowlist, return ErrSignUpNotAllowed (or an error wrapping it). Any
// other error is treated as a failure: logged as an error and, with
// AllowUnverifiedLogin or a provider, answered with a 500.
//
// In verify first mode (the default), an email and password sign-up
// answers before creating the user, so the response takes the same
// time whether or not the address has an account. The hook then runs
// just after the response, with the request's context values but not
// its cancellation, and a failure can't change the 202 already sent.
OnUserCreated func(ctx context.Context, tx pgx.Tx, u User) error
// OnEvent runs after each security event is recorded, for projects
// that also want auth events in their own tools, such as alerting or
// Sentry. It runs in the request, so keep it quick and hand slow work
// off. See the Event constants.
OnEvent func(ctx context.Context, e Event)
}
Hooks let a project react to auth events.
type OAuth ¶
type OAuth struct {
Google *Google
GitHub *GitHub
// Apple turns on native Sign in with Apple in the iOS app. Apple isn't
// offered on the web or Android.
Apple *Apple
}
OAuth configures signing in with other providers. A nil provider is off.
Register these redirect URIs with each provider: {AppURL}/oauth/callback for the web app and {AppURL}/oauth/callback/mobile for the mobile app. The web app serves both pages; see docs/api.md §9.
type Organization ¶
Organization is an organization. Projects keep their own data about it in their own tables, referencing auth.organizations(id).
type Session ¶
type Session struct {
ID uuid.UUID
// Client is "web" or "mobile".
Client string
ExpiresAt time.Time
CreatedAt time.Time
}
Session is a login session. It never contains the session token.
Source Files
¶
- apple.go
- auth.go
- cleanup.go
- device_tokens.go
- doc.go
- email_change.go
- emails.go
- events.go
- handlers.go
- invitations.go
- login_failures.go
- middleware.go
- migrate.go
- new_device.go
- oauth_accounts.go
- oauth_login.go
- organizations.go
- passwords.go
- rate_limits.go
- required_two_factor.go
- roles.go
- two_factor.go
- two_factor_login.go
- verification.go
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
api
command
Command api is an example Go API built on the auth module.
|
Command api is an example Go API built on the auth module. |
|
internal
|
|
|
background
Package background runs work after an endpoint has responded.
|
Package background runs work after an endpoint has responded. |
|
emailtoken
Package emailtoken creates and checks the single-use tokens sent in emails: verification, password reset, and email change.
|
Package emailtoken creates and checks the single-use tokens sent in emails: verification, password reset, and email change. |
|
httpapi
Package httpapi holds the request and response plumbing shared by the module's HTTP endpoints, following the API design standard: the standard error format, strict JSON decoding, request IDs, and finding the real client behind a trusted proxy.
|
Package httpapi holds the request and response plumbing shared by the module's HTTP endpoints, following the API design standard: the standard error format, strict JSON decoding, request IDs, and finding the real client behind a trusted proxy. |
|
migrate
Package migrate applies the module's embedded migrations.
|
Package migrate applies the module's embedded migrations. |
|
migrations
Package migrations holds the module's SQL migrations, embedded into the binary.
|
Package migrations holds the module's SQL migrations, embedded into the binary. |
|
oauth
Package oauth runs the provider side of signing in with Google and GitHub: building the authorization URL with PKCE and state, and swapping the returned code for the user's identity.
|
Package oauth runs the provider side of signing in with Google and GitHub: building the authorization URL with PKCE and state, and swapping the returned code for the user's identity. |
|
oauth/oauthtest
Package oauthtest provides fake Google and GitHub servers for tests.
|
Package oauthtest provides fake Google and GitHub servers for tests. |
|
password
Package password hashes and verifies passwords with argon2id, and checks new passwords against the length policy in the auth standard.
|
Package password hashes and verifies passwords with argon2id, and checks new passwords against the length policy in the auth standard. |
|
pwned
Package pwned checks passwords against Have I Been Pwned's list of passwords seen in data breaches, using its range API (https://haveibeenpwned.com/API/v3#PwnedPasswords).
|
Package pwned checks passwords against Have I Been Pwned's list of passwords seen in data breaches, using its range API (https://haveibeenpwned.com/API/v3#PwnedPasswords). |
|
ratelimit
Package ratelimit counts requests in fixed time windows, stored in Postgres so every API instance shares the counts.
|
Package ratelimit counts requests in fixed time windows, stored in Postgres so every API instance shares the counts. |
|
recoverycode
Package recoverycode makes and checks 2FA recovery codes: single-use codes a user keeps for when they lose their authenticator.
|
Package recoverycode makes and checks 2FA recovery codes: single-use codes a user keeps for when they lose their authenticator. |
|
secretbox
Package secretbox encrypts small secrets for storage, such as TOTP secrets, with AES-256-GCM.
|
Package secretbox encrypts small secrets for storage, such as TOTP secrets, with AES-256-GCM. |
|
sender
Package sender sends email in the background, so endpoints respond without waiting for the email provider.
|
Package sender sends email in the background, so endpoints respond without waiting for the email provider. |
|
session
Package session creates, validates, extends, and revokes login sessions.
|
Package session creates, validates, extends, and revokes login sessions. |
|
testdb
Package testdb gives tests a fresh, empty Postgres database.
|
Package testdb gives tests a fresh, empty Postgres database. |
|
token
Package token generates and hashes the random secrets used for sessions, email verification, and password resets.
|
Package token generates and hashes the random secrets used for sessions, email verification, and password resets. |
|
totp
Package totp implements time-based one-time passwords (RFC 6238), the six-digit codes from authenticator apps.
|
Package totp implements time-based one-time passwords (RFC 6238), the six-digit codes from authenticator apps. |
|
Package mail defines how the auth module sends email.
|
Package mail defines how the auth module sends email. |
|
mailtest
Package mailtest provides a fake Mailer for tests.
|
Package mailtest provides a fake Mailer for tests. |
|
resend
Package resend sends the auth module's email through Resend (https://resend.com), the standard transactional email provider.
|
Package resend sends the auth module's email through Resend (https://resend.com), the standard transactional email provider. |
|
Package rbac defines roles and permissions for the auth module.
|
Package rbac defines roles and permissions for the auth module. |