accounts

package
v1.10.0 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 37 Imported by: 0

Documentation

Overview

Package accounts signs users up and in, with passwords or passkeys, and keeps their accounts: email verification, password resets, and the devices they're signed in on, any of which they can sign out.

It stores users and sessions in your database (SQLite or PostgreSQL), in tables whose names start with tern_. Install it as a plugin, after creating its tables:

acc, err := accounts.New(db, accounts.Options{Send: accounts.SendWith(mailer, queue)})
err = acc.Migrate(ctx)
app, err := ternApp(kit.WithPlugins(acc))

In loads and actions, accounts.Current(ctx) is the signed-in user, and acc.SignIn, acc.SignUp and the rest change who that is.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidCredentials = errors.New("accounts: wrong email or password")
	ErrWrongPassword      = errors.New("accounts: the current password is wrong")
	ErrEmailTaken         = errors.New("accounts: an account with this email already exists")
	ErrInvalidToken       = errors.New("accounts: the link is invalid or has expired")
	ErrSignedOut          = errors.New("accounts: not signed in")
	ErrNoEmail            = errors.New("accounts: Options.Send is not set, so no email can be sent")
	ErrPasskey            = errors.New("accounts: the passkey didn't work")
	// ErrTwoStepRequired means the password was right and the account asks
	// for a code too: send the user to a page that calls CompleteSignIn.
	ErrTwoStepRequired = errors.New("accounts: enter the code from your authenticator app")
	ErrInvalidCode     = errors.New("accounts: the code is wrong or has expired")
)

The errors that tell users what went wrong. Show them as form errors.

View Source
var (
	// ErrIdentityTaken is a provider account already linked to another user.
	ErrIdentityTaken = errors.New("accounts: this account is already linked to another user")
	// ErrIdentityNoEmail is a first sign-in from a provider that gave no
	// email address, which an account needs.
	ErrIdentityNoEmail = errors.New("accounts: the provider didn't share an email address")
	// ErrEmailUnverified is a first sign-in with the address of an existing
	// account, which the provider didn't verify: sign in another way and
	// link the provider from there.
	ErrEmailUnverified = errors.New("accounts: an account with this email exists; sign in to it to link this provider")
	// ErrLastSignIn is unlinking the only way an account can sign in.
	ErrLastSignIn = errors.New("accounts: add a password or a passkey before removing the last way to sign in")
)
View Source
var ErrTooManyAttempts = errors.New("accounts: too many attempts, try again later")

ErrTooManyAttempts is returned when an address or a client has tried too often: guessing passwords or codes, or sending emails.

Functions

func CheckPassword

func CheckPassword(hash, password string) (ok, rehash bool)

CheckPassword reports whether password matches hash, and whether the hash should be replaced with a fresh HashPassword.

func FormError

func FormError(c *kit.Ctx, err error) error

FormError turns the errors users cause into form errors, in their language, for an action to return: a wrong password, a taken email, an expired link. Other errors stay as they are.

if _, err := acc.SignIn(c, in.Email, in.Password, in.Remember); err != nil {
	return struct{}{}, accounts.FormError(c, err)
}

func HashPassword

func HashPassword(password string) (string, error)

HashPassword hashes a password for storing.

func SendWith

func SendWith(mailer *mail.Mailer, q *jobs.Queue) func(ctx context.Context, e Email) error

SendWith sends the emails with mailer, in the background through q, so they're retried and requests don't wait for them.

Types

type Accounts

type Accounts struct {
	// contains filtered or unexported fields
}

Accounts is the accounts plugin. Create it with New.

func New

func New(db *sql.DB, opts Options) (*Accounts, error)

New returns the accounts plugin.

func (*Accounts) ActAs added in v1.10.0

func (a *Accounts) ActAs(c *kit.Ctx, u *User)

ActAs makes u the signed-in user for the rest of c's request, without a session: Current returns u and Require lets it through. For tests (see kit/kittest), and for support tools that act for a user once their own checks have passed. c must have run the plugin's hook, as a Ctx from kit.App.NewCtx does.

func (*Accounts) AwaitingCode added in v1.7.0

func (a *Accounts) AwaitingCode(c *kit.Ctx) bool

AwaitingCode reports whether this browser has signed in with a password and still has to enter its code, for the code page's load: without one, send the user back to sign in.

func (*Accounts) ChangePassword

func (a *Accounts) ChangePassword(c *kit.Ctx, current, password string) error

ChangePassword sets a new password after checking the current one, and signs out the user's other devices. An account without a password gets one with current "".

func (*Accounts) CompleteSignIn added in v1.7.0

func (a *Accounts) CompleteSignIn(c *kit.Ctx, code string) (*User, error)

CompleteSignIn finishes a sign-in that returned ErrTwoStepRequired, with a code from the app or a recovery code.

func (*Accounts) ConfirmTwoStep added in v1.7.0

func (a *Accounts) ConfirmTwoStep(c *kit.Ctx, code string) ([]string, error)

ConfirmTwoStep turns two-step sign-in on with a code from the app, and returns the recovery codes, to show the user once.

func (*Accounts) DeleteAccount

func (a *Accounts) DeleteAccount(c *kit.Ctx) error

DeleteAccount deletes the signed-in user, their sessions and passkeys.

func (*Accounts) Identities added in v1.7.0

func (a *Accounts) Identities(c *kit.Ctx) ([]LinkedIdentity, error)

Identities lists the providers linked to the signed-in user.

func (*Accounts) Migrate

func (a *Accounts) Migrate(ctx context.Context) error

Migrate creates the tables, if they don't exist yet.

func (*Accounts) Name

func (*Accounts) Name() string

Name implements kit.Plugin.

func (*Accounts) Passkeys

func (a *Accounts) Passkeys(c *kit.Ctx) ([]Passkey, error)

Passkeys lists the signed-in user's passkeys.

func (*Accounts) PendingTwoStepSetup added in v1.7.0

func (a *Accounts) PendingTwoStepSetup(c *kit.Ctx) (*TwoStepSetup, error)

PendingTwoStepSetup is the setup SetUpTwoStep started and that isn't confirmed yet, or nil, for showing it again after a reload.

func (*Accounts) RecoveryCodes added in v1.7.0

func (a *Accounts) RecoveryCodes(c *kit.Ctx) ([]string, error)

RecoveryCodes replaces the signed-in user's recovery codes with new ones, to show once.

func (*Accounts) RecoveryCodesLeft added in v1.7.0

func (a *Accounts) RecoveryCodesLeft(c *kit.Ctx) (int, error)

RecoveryCodesLeft is how many unused recovery codes the signed-in user has.

func (*Accounts) RemovePasskey

func (a *Accounts) RemovePasskey(c *kit.Ctx, id string) error

RemovePasskey deletes one of the signed-in user's passkeys.

func (*Accounts) RequestEmailChange added in v1.7.0

func (a *Accounts) RequestEmailChange(c *kit.Ctx, newEmail string) error

RequestEmailChange emails a link to newEmail that makes it the signed-in user's address. The address only changes once the link is opened, at VerifyPath, so a typo can't lock anyone out. The old address is told.

func (*Accounts) RequestPasswordReset

func (a *Accounts) RequestPasswordReset(c *kit.Ctx, email string) error

RequestPasswordReset emails a reset link if an account has this email. It returns nil either way, so the form doesn't reveal who has one.

func (*Accounts) Require

func (a *Accounts) Require(c *kit.Ctx) (*User, error)

Require is the signed-in user; signed out, it returns a redirect to the sign-in page that comes back here, to return from a load or action.

func (*Accounts) ResetPassword

func (a *Accounts) ResetPassword(c *kit.Ctx, token, password string) (*User, error)

ResetPassword sets a new password with a reset link's token. It signs the user out everywhere and in here, and verifies their email: the link reached it. With two-step sign-in on, it returns ErrTwoStepRequired instead of signing in, as SignIn does.

func (*Accounts) Revoke

func (a *Accounts) Revoke(c *kit.Ctx, sessionID string) error

Revoke signs out one of the user's sessions, by its Session.ID.

func (*Accounts) SendVerification

func (a *Accounts) SendVerification(c *kit.Ctx) error

SendVerification emails the signed-in user a link that verifies their email address.

func (*Accounts) Sessions

func (a *Accounts) Sessions(c *kit.Ctx) ([]Session, error)

Sessions lists the devices the signed-in user is signed in on, the current one first, then the most recently used.

func (*Accounts) SetName

func (a *Accounts) SetName(c *kit.Ctx, name string) error

SetName changes the signed-in user's name.

func (*Accounts) SetUpTwoStep added in v1.7.0

func (a *Accounts) SetUpTwoStep(c *kit.Ctx) (*TwoStepSetup, error)

SetUpTwoStep starts turning on two-step sign-in for the signed-in user: show the result, then call ConfirmTwoStep with a code from the app. Calling it again starts over with a new secret, until it's confirmed.

func (*Accounts) Setup

func (a *Accounts) Setup(s *kit.Setup) error

Setup implements kit.Plugin: each request gets its user from the session cookie.

func (*Accounts) SignIn

func (a *Accounts) SignIn(c *kit.Ctx, email, password string, remember bool) (*User, error)

SignIn checks a password and signs the user in. remember keeps the session for Options.SessionLength instead of a day.

func (*Accounts) SignInWith added in v1.7.0

func (a *Accounts) SignInWith(c *kit.Ctx, id Identity) (*User, error)

SignInWith signs in with another provider's identity:

  • signed in already, it links the identity to the current user;
  • an identity linked before signs in its user;
  • otherwise it joins the account with the same verified email, or creates an account.

With two-step sign-in on, it returns ErrTwoStepRequired as SignIn does: the provider counts as the first step.

func (*Accounts) SignOut

func (a *Accounts) SignOut(c *kit.Ctx) error

SignOut ends the request's session.

func (*Accounts) SignOutOthers

func (a *Accounts) SignOutOthers(c *kit.Ctx) error

SignOutOthers signs out every session of the user but this one.

func (*Accounts) SignUp

func (a *Accounts) SignUp(c *kit.Ctx, email, name, password string) (*User, error)

SignUp creates an account with a password and signs it in. password may be "" for an account that signs in with passkeys only.

func (*Accounts) TurnOffTwoStep added in v1.7.0

func (a *Accounts) TurnOffTwoStep(c *kit.Ctx, code string) error

TurnOffTwoStep turns two-step sign-in off, with a current code or a recovery code.

func (*Accounts) TwoStepPath added in v1.7.0

func (a *Accounts) TwoStepPath() string

TwoStepPath is Options.TwoStepPath, for redirects to the code page.

func (a *Accounts) Unlink(c *kit.Ctx, provider string) error

Unlink removes a provider from the signed-in user, unless it's the only way left to sign in.

func (*Accounts) UserByEmail

func (a *Accounts) UserByEmail(ctx context.Context, email string) (*User, error)

UserByEmail returns a user, or nil if there's none.

func (*Accounts) UserByID

func (a *Accounts) UserByID(ctx context.Context, id int64) (*User, error)

UserByID returns a user, or nil if there's none.

func (*Accounts) VerifyEmail

func (a *Accounts) VerifyEmail(c *kit.Ctx, token string) (*User, error)

VerifyEmail verifies the address a verification link was sent to, or confirms a new address from RequestEmailChange's link.

type Email

type Email struct {
	// Kind is "verify", "reset", "change" (confirm a new address),
	// "changed" (to the old one) or "new_device".
	Kind    string `json:"kind"`
	To      string `json:"to"`
	Name    string `json:"name"`
	Link    string `json:"link"`
	Subject string `json:"subject"`
	Text    string `json:"text"`
	HTML    string `json:"html"`
}

Email is a verification or password reset email for Options.Send. Its texts are Tern's own (tern.accounts.*), translated like any other into the request's locale.

type Identity added in v1.7.0

type Identity struct {
	// Provider is its name, such as "github", and Subject its id for the
	// user, which never changes.
	Provider, Subject string
	Email             string
	// EmailVerified is whether the provider checked the address. Only then
	// does a first sign-in join an existing account with that address.
	EmailVerified bool
	Name          string
}

Identity is a user as another provider knows them.

type LinkedIdentity added in v1.7.0

type LinkedIdentity struct {
	Provider  string    `json:"provider"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"createdAt"`
}

LinkedIdentity is a provider linked to the signed-in user.

type NewDevice added in v1.9.0

type NewDevice struct {
	User *User
	// Device describes it, such as "Firefox on macOS".
	Device string
	IP     string
	Time   time.Time
}

NewDevice is a sign-in from a device the account hadn't been signed in on before, for Options.OnNewDevice.

type Options

type Options struct {
	// Send delivers the verification and password reset emails. SendWith
	// sends them with a mail.Mailer through a jobs queue. Without it,
	// those features fail with ErrNoEmail.
	Send func(ctx context.Context, e Email) error
	// BaseURL is the app's address, such as "https://example.com", for
	// links in emails. The default is the request's own.
	BaseURL string
	// ResetPath and VerifyPath are the pages the emails link to, with the
	// token in ?token=. The defaults are /reset-password and /verify-email.
	ResetPath, VerifyPath string
	// SignInPath is where Require sends signed-out users. The default is
	// /login.
	SignInPath string
	// Cookie names the session cookie. The default is "tern_session".
	Cookie string
	// SessionLength is how long a "keep me signed in" session lasts
	// without use. The default is 30 days; other sessions last a day.
	SessionLength time.Duration
	// PasskeyPath is where the passkey endpoints are mounted, for the
	// browser's passkeys helpers. The default is /_tern/passkeys.
	PasskeyPath string
	// RPName is the app's name the browser shows when creating a passkey.
	// The default is the host name.
	RPName string
	// TwoStepPath is the page that asks for the two-step code after a
	// password sign-in, for apps to redirect to on ErrTwoStepRequired. The
	// default is /login/code.
	TwoStepPath string
	// Issuer names the app in authenticator apps. The default is RPName,
	// then the host name.
	Issuer string
	// NoRateLimits turns off the built-in limits on attempts (see
	// ErrTooManyAttempts), for tests or a limiter of your own.
	NoRateLimits bool

	// NotifyNewDevices emails users when their account is signed in to
	// from a device it hadn't been signed in on before. OnNewDevice is
	// called then, too. Both need plugins/signals installed, which gives
	// each browser a device ID; see NewDevice.
	NotifyNewDevices bool
	OnNewDevice      func(c *kit.Ctx, d NewDevice)
}

Options configure accounts.

type Passkey

type Passkey struct {
	ID        string     `json:"id"`
	Name      string     `json:"name"`
	CreatedAt time.Time  `json:"createdAt"`
	LastUsed  *time.Time `json:"lastUsed"`
}

Passkey is one of a user's passkeys.

type Session

type Session struct {
	// ID identifies it for Revoke. It isn't the cookie's token.
	ID        string    `json:"id"`
	Device    string    `json:"device"` // such as "Firefox on macOS"
	IP        string    `json:"ip"`
	CreatedAt time.Time `json:"createdAt"`
	LastSeen  time.Time `json:"lastSeen"`
	// Current is the session of this request.
	Current bool `json:"current"`
}

Session is a device the user is signed in on.

type TwoStepSetup added in v1.7.0

type TwoStepSetup struct {
	// Secret is the key in base32, grouped by four for typing.
	Secret string `json:"secret"`
	// URI is the otpauth:// address the QR code holds; on a phone, a link
	// to it opens the authenticator app.
	URI string `json:"uri"`
	// QR is the QR code as a data: URL of a PNG image.
	QR string `json:"qr"`
}

TwoStepSetup is what a user needs to add the account to their authenticator app: a QR code to scan, or the secret to type.

type User

type User struct {
	ID            int64     `json:"id"`
	Email         string    `json:"email"`
	Name          string    `json:"name"`
	EmailVerified bool      `json:"emailVerified"`
	CreatedAt     time.Time `json:"createdAt"`
	// HasPassword is false for accounts made with a passkey only.
	HasPassword bool `json:"hasPassword"`
	// TwoStep is whether signing in with a password also asks for a code
	// from an authenticator app.
	TwoStep bool `json:"twoStep"`
}

User is an account.

func Current

func Current(c *kit.Ctx) *User

Current is the signed-in user, or nil.

Jump to

Keyboard shortcuts

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