Documentation
¶
Overview ¶
Package email registers the email identifier kind: normalization, the uniqueness policy, verification links, primary-address bookkeeping, change with undo, and delivery through an app-supplied Mailer.
It has no table of its own. Addresses are rows of the core auth_identity table, so the core's one uniqueness rule and one verified check cover them; the plugin adds two columns to auth_user so the primary verified address is readable without a join.
Index ¶
- Constants
- Variables
- func Bindings(c *EmailColumns) []sqlb.Binding
- func CheckMetadata(m map[string]string) error
- func DefaultLink(base string, purpose auth.Purpose, t auth.Token) string
- func DefaultTemplates(m auth.Message) (subject, text string)
- func Normalize(raw string) (string, error)
- func ParseDomainList(r io.Reader) ([]string, error)
- func Validate(v string) error
- type Address
- type AddressCheck
- type AddressCodeRequest
- type AddressRequest
- type CheckCodeRequest
- type CodeFor
- type CodeGenerator
- type CodeKind
- type CodeRequest
- type CodeSignInResult
- type EmailColumns
- type ListResult
- type MXResolver
- type Mail
- type Mailer
- type MailerFunc
- type Option
- func AllowDomains(domains ...string) Option
- func ApproveChangeFirst() Option
- func ChangeWithoutVerification() Option
- func CheckAddresses(checks ...AddressCheck) Option
- func Contact() Option
- func GenerateCode(fn CodeGenerator) Option
- func Links(fn func(p auth.Purpose, t auth.Token) string) Option
- func Login() Option
- func Multiple() Option
- func Recover() Option
- func ReplaceUnverified() Option
- func RequireVerified() Option
- func ResendOnSignIn() Option
- func ReuseCodeOnResend() Option
- func ScopedByTenant() Option
- func SignInOnVerify() Option
- func SignInWithCode() Option
- func SignUpWithCode() Option
- func Templates(fn func(m auth.Message) (subject, text string)) Option
- func TemplatesByLocale(byLocale map[string]TemplateFunc) Option
- func VerifyByCode() Option
- func VerifyByGET(callback string) Option
- func Welcome() Option
- func WithCodeSendLimit(n int, window time.Duration) Option
- func WithCodeTTL(d time.Duration) Option
- func WithCodeTryLimit(n int, window time.Duration) Option
- func WithMailer(m Mailer) Option
- func WithMaxAttempts(n int) Option
- func WithNormalizer(fn func(raw string) (string, error)) Option
- func WithUndoTTL(d time.Duration) Option
- func WithVerifyTTL(d time.Duration) Option
- type Plugin
- func (p *Plugin) Add(ctx context.Context, address string) error
- func (p *Plugin) ApproveChange(ctx context.Context, t auth.Token) error
- func (p *Plugin) ApproveChangeCode(ctx context.Context, address, code string) error
- func (p *Plugin) Change(ctx context.Context, address string) error
- func (p *Plugin) CheckCode(ctx context.Context, kind CodeKind, address, code string) error
- func (p *Plugin) Columns() []auth.ColumnExtension
- func (p *Plugin) ConfirmChange(ctx context.Context, t auth.Token) (VerifyResult, error)
- func (p *Plugin) ConfirmChangeCode(ctx context.Context, address, code string) (VerifyResult, error)
- func (p *Plugin) DataHooks() auth.DataHooks
- func (p *Plugin) Deliver(ctx context.Context, to auth.Identity, m auth.Message) error
- func (p *Plugin) Identifiers() []auth.IdentifierKind
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) List(ctx context.Context) ([]Address, error)
- func (p *Plugin) MakePrimary(ctx context.Context, address string) error
- func (p *Plugin) Name() string
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) RateLimits() []auth.RateRule
- func (p *Plugin) Remove(ctx context.Context, address string) error
- func (p *Plugin) Resend(ctx context.Context, address string) error
- func (p *Plugin) SendCode(ctx context.Context, address string) error
- func (p *Plugin) SendCodeWithMetadata(ctx context.Context, address string, metadata map[string]string) error
- func (p *Plugin) SendVerification(ctx context.Context, to auth.Identity) error
- func (p *Plugin) SetFor(ctx context.Context, userID, address string, verified bool) error
- func (p *Plugin) SignInCode(ctx context.Context, address, code string) (CodeSignInResult, error)
- func (p *Plugin) SignInCodeAs(ctx context.Context, address, code, name string) (CodeSignInResult, error)
- func (p *Plugin) SignInCodeWith(ctx context.Context, in CodeRequest) (CodeSignInResult, error)
- func (p *Plugin) SignsIn() bool
- func (p *Plugin) UndoChange(ctx context.Context, t auth.Token) error
- func (p *Plugin) Validate(c *auth.Config) error
- func (p *Plugin) Verify(ctx context.Context, t auth.Token) (VerifyResult, error)
- func (p *Plugin) VerifyCode(ctx context.Context, address, code string) (VerifyResult, error)
- type TemplateFunc
- type TokenRequest
- type VerifyLinkRequest
- type VerifyResult
Constants ¶
const ( //ds:def id=purposeverifytoken-m9zusn9c owner=@khanakia stability=stable PurposeVerifyToken auth.Purpose = "email.verify" //ds:def id=purposechangetoken-uuqg97cm owner=@khanakia stability=stable PurposeChangeToken auth.Purpose = "email.change" //ds:def id=purposeundotoken-w3f3bsxc owner=@khanakia stability=stable PurposeUndoToken auth.Purpose = "email.undo" PurposeApproveToken auth.Purpose = "email.approve" )
Verification purposes this plugin owns. What each row's Identifier holds:
- PurposeVerifyToken: the identity id being verified.
- PurposeChangeToken: the user id; Meta is the new normalized address.
- PurposeUndoToken: the user id; Meta is "<old>\n<new>".
- PurposeApproveToken: the user id; Meta is the new normalized address, sent to the current address under ApproveChangeFirst.
const ( PurposeVerifyCode auth.Purpose = "email.verify_code" PurposeChangeCode auth.Purpose = "email.change_code" PurposeApproveCode auth.Purpose = "email.approve_code" )
One-time-code purposes, used under VerifyByCode instead of the link purposes above. What each code's subject holds:
- PurposeVerifyCode: the identity id being verified.
- PurposeChangeCode: the user id and the new normalized address (changeSubject), because no row exists for the new address until the code is checked.
- PurposeApproveCode: the same subject as PurposeChangeCode, for the code ApproveChangeFirst mails the CURRENT address under VerifyByCode; a distinct purpose, so an approval code can never confirm the change.
const ( EndpointVerify auth.EndpointID = "email.verify" EndpointResend auth.EndpointID = "email.resend" EndpointList auth.EndpointID = "email.list" EndpointAdd auth.EndpointID = "email.add" EndpointMakePrimary auth.EndpointID = "email.make-primary" EndpointRemove auth.EndpointID = "email.remove" EndpointChange auth.EndpointID = "email.change" EndpointConfirmChange auth.EndpointID = "email.confirm-change" EndpointUndoChange auth.EndpointID = "email.undo-change" EndpointApproveChange auth.EndpointID = "email.approve-change" EndpointSendCode auth.EndpointID = "email.send-code" EndpointSignInCode auth.EndpointID = "email.sign-in-code" EndpointVerifyCode auth.EndpointID = "email.verify-code" EndpointConfirmCode auth.EndpointID = "email.confirm-change-code" EndpointCheckCode auth.EndpointID = "email.check-code" EndpointApproveCode auth.EndpointID = "email.approve-change-code" // EndpointVerifyLink is the GET verification VerifyByGET adds. EndpointVerifyLink auth.EndpointID = "email.verify-link" )
Operation ids, exported so another plugin's hook or a rate rule can name them. They are wire names, stable across releases.
const ( //ds:def id=colprimaryemail-wk287c2b owner=@khanakia stability=stable ColPrimaryEmail = "primary_email" //ds:def id=colemailverifiedat-vqzgpu2a owner=@khanakia stability=stable ColEmailVerifiedAt = "email_verified_at" )
Column names this plugin adds to auth_user.
const ( // DefaultVerifyTTL is how long a verify or change link works: a day, so a // user who signs up at night can verify in the morning. //ds:def id=defaultverifyttl-npdv6uys owner=@khanakia stability=stable DefaultVerifyTTL = 24 * time.Hour // DefaultUndoTTL is how long the old address can undo a change. A week, // because the owner of a stolen account may not read mail every day. //ds:def id=defaultundottl-f64c6t59 owner=@khanakia stability=stable DefaultUndoTTL = 7 * 24 * time.Hour )
Default lifetimes.
const ( //ds:def id=maxmetadataentries-6p3kr9wt owner=@khanakia stability=stable MaxMetadataEntries = 8 //ds:def id=maxmetadatalength-2n8vx4qm owner=@khanakia stability=stable MaxMetadataLength = 256 )
Bounds on client-supplied message metadata (AddressRequest.Metadata). The map comes from a public request body and travels to the app's Mailer, so its size is capped: enough for an invite id and a few flags, too small to carry a payload.
const ( //ds:def id=defaultverifypage-27eburvg owner=@khanakia stability=stable DefaultVerifyPage = "/verify-email" //ds:def id=defaultchangepage-fbv79xmc owner=@khanakia stability=stable DefaultChangePage = "/confirm-email-change" //ds:def id=defaultundopage-svuesac4 owner=@khanakia stability=stable DefaultUndoPage = "/undo-email-change" // DefaultApprovePage is where the approval link of ApproveChangeFirst // lands; the page POSTs the token to approve-change. DefaultApprovePage = "/approve-email-change" )
Default app pages the links open. Each page reads the token from the query and POSTs it to the matching operation. They are pages of the app, not of this library, because a link must never spend its token on a GET.
const ( // DefaultCodeTTL is how long a mailed sign-in code works. Mail can take a // minute or two to arrive; longer only helps whoever reads the inbox later. //ds:def id=defaultcodettl-u6wv36f8 owner=@khanakia stability=stable DefaultCodeTTL = 10 * time.Minute )
Sign-in code limits.
const DefaultMXTimeout = 3 * time.Second
DefaultMXTimeout bounds RequireMX's lookups. The check runs inside the writing transaction, so a slow resolver must not hold it open.
const DefaultMountPrefix = "/auth"
DefaultMountPrefix is where VerifyByGET's default link assumes the auth handler is mounted, as the oauth plugin's default callback does.
const KindEmail auth.IdentifierKindName = "email"
KindEmail is the identifier kind this plugin registers. It is stored in every auth_identity row this plugin owns, so it never changes.
const PluginName = "email"
PluginName is the plugin's permanent identity: route prefix and migration owner.
const PurposeChangeApproval auth.Purpose = "email.change_approval"
PurposeChangeApproval is the message purpose of the approval mail ApproveChangeFirst sends to the current address. Apps word it with Templates; the default is a generic subject and the DefaultText the plugin supplies, which names the new address.
const PurposeSignInCode auth.Purpose = "email.sign_in"
PurposeSignInCode is the one-time-code purpose of an email sign-in code. Its subject is the tenant and the normalized address; see codeSubject.
const WarnChangeWithoutVerification = "" /* 164-byte string literal not displayed */
WarnChangeWithoutVerification is logged by Init under ChangeWithoutVerification.
Variables ¶
var ( PrimaryEmail = sqlb.Col[*string](auth.TableUser, ColPrimaryEmail) EmailVerifiedAt = sqlb.Col[*time.Time](auth.TableUser, ColEmailVerifiedAt) )
Typed columns on auth_user. Both are null until the user has a verified primary address, which is the only kind that is copied here.
var ( ErrSingleAddress = auth.NewError("email.single_address", http.StatusConflict, "This account can have one email address. Change it instead.") ErrNoMailer = errors.New("email: no mailer; pass email.WithMailer") ErrNoBaseURL = errors.New("email: links need auth.WithBaseURL or email.Links") ErrCodesNeedLogin = errors.New("email: SignInWithCode needs Login()") ErrSignInOnVerifyNeedsLogin = errors.New("email: SignInOnVerify needs Login()") // ErrCallbackInvalid refuses a verify-link whose callbackURL is not a // page of this site or of a trusted origin (auth.Auth.TrustsReturnURL), // so the GET route is never an open redirect. ErrBadCallback is the same // mistake in VerifyByGET's own argument, refused by auth.New. //ds:def id=email-errcallbackinvalid-4mw9qt2k owner=@khanakia stability=stable ErrCallbackInvalid = auth.NewError("email.callback_invalid", http.StatusBadRequest, "This page cannot be opened after verifying.") ErrBadCallback = errors.New("email: VerifyByGET's callback must be a path of this site or a URL on a trusted origin") )
Errors this plugin returns. ErrSingleAddress reaches the user; the others are configuration errors auth.New returns, so a broken setup fails at start rather than at the first sign-up.
var CodeKindValues = []CodeKind{CodeKindVerify, CodeKindSignIn, CodeKindChange, CodeKindApprove}
CodeKindValues is the canonical iteration order over code kinds.
var ErrAddressRejected = auth.NewError("email.address_rejected", http.StatusBadRequest, "This email address cannot be used. Use another one.")
ErrAddressRejected refuses an address an AddressCheck turned down. Its message names no reason, so the answer is the same whether the domain is blocked or has no mail server.
var ErrBadDomainList = errors.New("email: AllowDomains needs at least one domain, and every entry must name one")
ErrBadDomainList is AllowDomains given no domain, or an entry that names none ("", "*.", "*", or one holding "@" or another "*"). auth.New refuses the plugin with it, naming the option and the entry: a list that allows nobody is a configuration mistake to stop at start-up, because at the first sign-up it would refuse everyone.
Functions ¶
func Bindings ¶
func Bindings(c *EmailColumns) []sqlb.Binding
Bindings binds EmailColumns for a query on auth_user, so an app reads the primary address in the same SELECT as the user without a join.
func CheckMetadata ¶
CheckMetadata refuses a metadata map over the bounds with auth.ErrBadRequest; nil and empty pass. Other identifier plugins that take metadata in a request body apply the same bounds through it.
func DefaultLink ¶
DefaultLink is the link the plugin sends when no Links option is given: a page of the app, named for the purpose, with the token in the query.
func DefaultTemplates ¶
DefaultTemplates are short English messages. A link is on its own line so every client makes it clickable.
func Normalize ¶
Normalize trims and lowercases an address. It does not remove dots or +tags: that blocks a common habit, and the choice belongs to the app (WithNormalizer).
func ParseDomainList ¶
ParseDomainList reads one domain per line, skipping blank lines and lines starting with "#", the format of the common disposable-domain lists.
Types ¶
type Address ¶
type Address struct {
Email string `json:"email"`
Verified bool `json:"verified"`
Primary bool `json:"primary"`
}
Address is one row of the list. Verified and Primary are separate because an unverified address is listed but can never be primary.
type AddressCheck ¶
AddressCheck decides whether a new address may be stored: at sign-up, when an address is added, and when one is changed (before the confirm mail goes out, and again when the row is written). It never runs at sign-in, so an account created before a check was switched on keeps working. Return ErrAddressRejected (or any auth.Error) to refuse the address; any other error is a failure of the check itself and fails the request.
A check runs inside the writing transaction, so it must be quick: the built-in RequireMX bounds its lookup with a timeout.
func BlockDomains ¶
func BlockDomains(domains ...string) AddressCheck
BlockDomains refuses an address whose domain is one of domains or a subdomain of one ("mailinator.com" also blocks "x.mailinator.com"). Domains are compared in lower case with any trailing dot removed. The library ships no list, because a list goes stale and its choice is the app's: load one with ParseDomainList, for example the disposable-email-domains project's blocklist that better-auth-harmony's own list is built from.
func RequireMX ¶
func RequireMX(r MXResolver, timeout time.Duration) AddressCheck
RequireMX refuses an address whose domain cannot receive mail: the domain does not exist, it publishes the null MX record (RFC 7505), or it has no MX record and no address record to fall back to (RFC 5321 §5.1). r nil is net.DefaultResolver; timeout 0 is DefaultMXTimeout.
A lookup that fails for any other reason (a timeout, a resolver that is down) lets the address through: the verify mail is the real test of an address, and an outage of the app's DNS must not stop every sign-up.
type AddressCodeRequest ¶
AddressCodeRequest names an address and the code mailed to it, for verify-code and confirm-change-code under VerifyByCode.
func (AddressCodeRequest) RateIdentifier ¶
func (r AddressCodeRequest) RateIdentifier() string
RateIdentifier counts guesses per normalized address.
type AddressRequest ¶
type AddressRequest struct {
Email string `json:"email"`
// Metadata travels to the Mailer with the message send-code mails (an
// invite id, say); other operations ignore it. At most
// MaxMetadataEntries entries of MaxMetadataLength bytes each.
Metadata map[string]string `json:"metadata,omitempty"`
}
AddressRequest names one address, as the user typed it; each operation normalizes it.
func (AddressRequest) RateIdentifier ¶
func (r AddressRequest) RateIdentifier() string
RateIdentifier counts per normalized address for ByIdentifier limits, so changing the case of an address does not reset its limit.
type CheckCodeRequest ¶
type CheckCodeRequest struct {
Kind CodeKind `json:"kind"`
Email string `json:"email"`
Code string `json:"code"`
}
CheckCodeRequest names a code to look at without spending it: the kind of code, the address it was mailed to, and the code.
func (CheckCodeRequest) RateIdentifier ¶
func (r CheckCodeRequest) RateIdentifier() string
RateIdentifier counts peeks per normalized address, in the same budget as guesses.
type CodeFor ¶
CodeFor is what a custom code generator (GenerateCode) is told about the code it makes: the normalized address it goes to and its kind, better-auth's generateOTP({email, type}).
type CodeGenerator ¶
CodeGenerator returns the code to mail instead of the core's random digits.
type CodeKind ¶
type CodeKind string
CodeKind names which code check-code looks at. The three kinds live under different subjects, so the client says which one it holds.
const ( // CodeKindVerify is a verify code (VerifyByCode): the address as typed, // or the signed-in user's own address. CodeKindVerify CodeKind = "verify" // CodeKindSignIn is a sign-in code (SignInWithCode) for the address. CodeKindSignIn CodeKind = "sign_in" // CodeKindChange is a change code (VerifyByCode) for the signed-in // user's new address; signed out it is simply not found. CodeKindChange CodeKind = "change" // CodeKindApprove is an approval code (VerifyByCode with // ApproveChangeFirst), mailed to the signed-in user's current address // and named by the new address; signed out it is simply not found. CodeKindApprove CodeKind = "approve" )
Code kinds.
type CodeRequest ¶
type CodeRequest struct {
Email string `json:"email"`
Code string `json:"code"`
// Name and Image are the display name and picture URL for the account a
// code creates (SignUpWithCode); ignored when the address already has an
// owner. They are the two profile columns every user has.
Name string `json:"name,omitempty"`
Image string `json:"image,omitempty"`
// Fields sets other auth_user columns of the account a code creates,
// better-auth's additionalFields: only the columns auth.WithSignUpFields
// names, each of its declared type, else auth.ErrFieldInvalid before the
// code is checked. The same columns may also be sent as top-level keys,
// as better-auth does; one sent both ways is auth.ErrFieldInvalid.
// Ignored when the address already has an owner.
Fields auth.Fields `json:"fields,omitempty"`
// contains filtered or unexported fields
}
CodeRequest carries an address and the six-digit code mailed to it.
func (CodeRequest) RateIdentifier ¶
func (r CodeRequest) RateIdentifier() string
RateIdentifier counts guesses per address.
func (*CodeRequest) UnmarshalJSON ¶
func (r *CodeRequest) UnmarshalJSON(data []byte) error
UnmarshalJSON keeps the top-level keys that are not the request's own, so sign-up fields may sit beside email and code; auth.Auth.MergeFields then refuses any that is not a declared column.
type CodeSignInResult ¶
type CodeSignInResult struct {
User auth.UserView `json:"user"`
Created bool `json:"created"`
SecondFactorRequired bool `json:"secondFactorRequired"`
Token auth.Token `json:"-"`
}
CodeSignInResult is the output of sign-in-code. Token is for Go callers and bearer transports, never encoded into JSON.
type EmailColumns ¶
EmailColumns is this plugin's part of a composite user. Embed it: the columns are on auth_user, so the fields are flat.
Why the name stutters: Go names an embedded field after its type, so if two plugins both exported a type called Columns, an app could not embed both. Every plugin names its column struct <Plugin>Columns for that reason.
type ListResult ¶
type ListResult struct {
Addresses []Address `json:"addresses"`
}
ListResult is the output of list, in creation order; Addresses is never nil, so it encodes as [] rather than null.
type MXResolver ¶
type MXResolver interface {
LookupMX(ctx context.Context, name string) ([]*net.MX, error)
LookupHost(ctx context.Context, host string) ([]string, error)
}
MXResolver is the part of *net.Resolver RequireMX uses, so a test or an app with its own DNS client can supply one.
type Mail ¶
type Mail struct {
To string
Subject string
Text string
// Metadata is what the sending operation attached (auth.Message.Metadata):
// an invite id, a campaign, a page the client was on. Nil for most mail.
// The Mailer may use it for its own routing or templates; the library
// never reads it.
Metadata map[string]string
}
Mail is one message for the Mailer: plain text only, because every link a message carries must survive a client that renders no HTML.
type MailerFunc ¶
MailerFunc adapts a function to Mailer, for an app whose sender is a closure over its SMTP or API client.
type Option ¶
type Option func(*Plugin)
Option configures the plugin; options are applied in order by New.
func AllowDomains ¶
AllowDomains refuses every new address whose domain the list does not name: the allow-list counterpart of BlockDomains, for an app open only to one company's or one university's people. It is an option rather than an AddressCheck so a bad list fails auth.New (ErrBadDomainList) instead of panicking or refusing every sign-up. Its check joins the ones CheckAddresses adds and runs where it stands among the options, so give it before CheckAddresses(RequireMX(…)) to refuse an outside address without a DNS lookup. Like every address check it runs on sign-up, on an added address and on a changed one, never at sign-in, so an account made before the list existed keeps working. Giving it twice requires both lists.
An entry matches exactly: "acme.com" allows "pat@acme.com" and nothing else, neither "pat@eu.acme.com" nor "pat@notacme.com". A subdomain is allowed only when asked for, with a leading "*.": "*.acme.com" allows "pat@eu.acme.com" and "pat@a.b.acme.com" but not "pat@acme.com", so list both to allow both. Exact by default because an allow-list grants access: a subdomain is often run by someone else (a student society, a customer's tenant on a shared host), and nobody should be let in by a rule they did not write. Entries and addresses are compared in lower case with any trailing dot removed; a name in Unicode must be listed in the form the email plugin stores it (punycode unless WithNormalizer changes that).
A refused address gets ErrAddressRejected, the same answer as a blocked domain or a domain with no mail server, and no row is read, so the answer tells nothing about which accounts exist. Load a long list with ParseDomainList.
func ApproveChangeFirst ¶
func ApproveChangeFirst() Option
ApproveChangeFirst makes a change of address a two-step affair: the link goes to the CURRENT verified primary address first, and only when it is used does the confirm link go to the new address. Without it the old address is told after the switch, with an undo link; with it the old address decides before. A user whose current address is unverified gets the one-step flow, because there is nobody proven to ask. Under VerifyByCode the approval is a code, typed by the signed-in user at approve-change-code: the reader is the person who just asked, in the app.
func ChangeWithoutVerification ¶
func ChangeWithoutVerification() Option
ChangeWithoutVerification is better-auth's user.changeEmail.updateEmailWithoutVerification: `change` replaces the address at once even when the current one is verified, as ReplaceUnverified does while none is. The new address is unverified and gets the verification mail; the old one is gone, with no confirm step and no undo mail. Off by default; Init logs WarnChangeWithoutVerification at every start while it is on.
The risk it opens: whoever holds a session (a laptop left open, a stolen cookie) can point the account at an address of their own, and every password-reset and magic-link mail goes there from then on, with no mail to the old address to warn its owner or undo it. The core still refuses a change that would remove the user's last way in.
func CheckAddresses ¶
func CheckAddresses(checks ...AddressCheck) Option
CheckAddresses runs every check, in order, on each new address; the first refusal wins. It is the hook better-auth-harmony and Sentinel's email validation fill: BlockDomains for a disposable-domain list, RequireMX for a domain that can receive mail, or the app's own function.
func Contact ¶
func Contact() Option
Contact makes email contact information only: several users may share an address, and it neither signs in nor recovers.
func GenerateCode ¶
func GenerateCode(fn CodeGenerator) Option
GenerateCode replaces the core's random digits with fn's code, for every code this plugin mails (verify, sign-in, change and approve), better-auth's generateOTP. It is a plain override: whatever fn returns is the code, compared exactly, and an error from fn fails the send. Off by default; the default draws auth.WithCodeLength digits from crypto/rand.
Risk: a code is only as hard to guess as fn makes it.
func Links ¶
Links builds the URL a message carries. The link must open a page of the app that POSTs the token; a GET that spent the token would be spent by every mail scanner that previews links.
func Login ¶
func Login() Option
Login allows email in sign-in. auth.New then requires the address to be unique, since a login box cannot pick between two users.
func Multiple ¶
func Multiple() Option
Multiple allows many addresses per user, one of them primary. Without it a change replaces the address, and Add refuses a second one.
func Recover ¶
func Recover() Option
Recover allows email to receive recovery messages such as password reset links. Only a verified address receives one.
func ReplaceUnverified ¶
func ReplaceUnverified() Option
ReplaceUnverified lets a user who mistyped their address at sign-up fix it without ever receiving mail: while none of the user's addresses is verified, `change` replaces the address at once and the verification goes to the new one; no confirm step, no undo mail (an unverified address proves nothing about who owns the account, so there is nobody to warn). The moment an address is verified, a change is the normal two-step affair again: the new address must be proven first, and the old one gets the undo link, because that is what keeps a stolen session from redirecting every reset mail.
The answer is the same whether or not the new address is already another account's: then nothing changes and that account's owner is told someone tried, as a duplicate sign-up does, so the route confirms no address. The rate rule of `change` (per new address) covers both paths.
func RequireVerified ¶
func RequireVerified() Option
RequireVerified refuses sign-in until the address is verified, for an app where an unproven address must not open an account.
func ResendOnSignIn ¶
func ResendOnSignIn() Option
ResendOnSignIn makes SendVerification mail a fresh verify link, so a sign-in refused because the address is unverified (RequireVerified) gets the link again without a separate resend. Off, SendVerification sends nothing: a sign-in attempt then mails nobody.
func ReuseCodeOnResend ¶
func ReuseCodeOnResend() Option
ReuseCodeOnResend makes a resend mail the same code again and extend its life by the code TTL, better-auth's resendStrategy "reuse", instead of replacing it with a new one (the default, "rotate"). Wrong guesses already made still count: a resend never refills the attempt budget, and a code with no attempts left or past its expiry is replaced as usual.
Off by default, and the risk is the secret: a code that must be mailed again is derived from its row id with a key from auth.WithSecret, or, with GenerateCode, stored sealed under that key (auth.CodeOptions.Reuse), so auth.New requires the secret. Anyone with the database AND the secret can read open codes; and a code that lives longer gives a guesser more time across its attempt budget.
func ScopedByTenant ¶
func ScopedByTenant() Option
ScopedByTenant makes addresses unique per tenant; see auth.WithScope.
func SignInOnVerify ¶
func SignInOnVerify() Option
SignInOnVerify makes a verify link also sign its user in: the click proves control of the mailbox, as a magic link does. It needs Login(). The session is a first factor only, so a user with two-factor gets a pending one.
func SignInWithCode ¶
func SignInWithCode() Option
SignInWithCode enables sign-in with a six-digit code sent to a verified address, as a passwordless method. It needs Login.
func SignUpWithCode ¶
func SignUpWithCode() Option
SignUpWithCode also lets a code create an account for an address nobody has verified, whose control the code has just proven. Implies SignInWithCode.
func Templates ¶
Templates replaces the default English subjects and bodies, for every locale TemplatesByLocale does not cover.
func TemplatesByLocale ¶
func TemplatesByLocale(byLocale map[string]TemplateFunc) Option
TemplatesByLocale adds translations: the message's Locale picks one by auth.PickLocale (exact tag, then base language), and a message whose language has none uses the templates set by Templates, or the English defaults. The library ships no translations; the words are the app's.
The choice is made at delivery, so the order of this option and Templates does not matter.
func VerifyByCode ¶
func VerifyByCode() Option
VerifyByCode makes verification and address change work by a code the person types instead of a link they click: the verify mail, a resend, the confirm mail of a change and the approval mail of ApproveChangeFirst carry a code (WithCodeTTL, WithMaxAttempts apply), checked at verify-code, confirm-change-code and approve-change-code. A code suits a mobile app, where a link would open the browser, and survives mail scanners, which open every link. The undo mail of a change stays a link: its reader may be anywhere, and it must work after the sessions it ends are gone. The link routes stay registered, so links sent before the switch still work.
func VerifyByGET ¶
VerifyByGET adds better-auth's GET verification: GET <mount>/email/verify-link?token=…&callbackURL=… spends the token, verifies the address (and signs in under SignInOnVerify), then redirects to callbackURL, or to callbackURL?error=<code> when the link is bad; without a trusted callbackURL a failure lands on auth.WithErrorURL's page, and the route answers JSON like POST verify when neither is set. The verify mail's default link points at it, BaseURL + DefaultMountPrefix + "/email/verify-link", with callback ("" for none) as its callbackURL; Links still overrides.
Off by default, and the risk is why: a GET spends the token, so a mail scanner or a link preview that fetches the URL verifies the address (and, under SignInOnVerify, opens a session nobody uses) before the person clicks, and the person then lands on "this link has expired". With SignInOnVerify a GET sign-in also cannot be origin-checked, so a link to an attacker's own verification signs a victim into the attacker's account (login CSRF), which auth.WithFetchMetadata, judging mutations only, does not cover. The default POST page has neither problem. callbackURL must pass auth.Auth.TrustsReturnURL; the POST route stays for links already sent.
func Welcome ¶
func Welcome() Option
Welcome sends a welcome mail (auth.PurposeWelcome) when a user's first address is verified: once per user, and only to an address the user has proven, so it cannot be used to mail strangers.
func WithCodeSendLimit ¶
WithCodeSendLimit replaces the default of 3 codes per address per 15 minutes. Each code is a mail to somebody, so the limit is what stops the endpoint flooding an inbox; raise it for a product whose users retry often.
func WithCodeTTL ¶
WithCodeTTL overrides DefaultCodeTTL, how long a mailed sign-in code works.
func WithCodeTryLimit ¶
WithCodeTryLimit replaces the default of 10 guesses per address per 15 minutes. The per-code attempt cap (WithMaxAttempts) is the real bound; this one stops a script cycling fresh codes.
func WithMailer ¶
WithMailer sets how mail is sent. Required: auth.New fails with ErrNoMailer without it, because verification is impossible without mail.
func WithMaxAttempts ¶
WithMaxAttempts overrides auth.DefaultCodeMaxAttempts for sign-in codes: the wrong guesses one code survives before it is spent.
func WithNormalizer ¶
WithNormalizer replaces Normalize, for an app that folds dots or +tags.
func WithUndoTTL ¶
WithUndoTTL overrides DefaultUndoTTL, how long the old address can undo a change.
func WithVerifyTTL ¶
WithVerifyTTL overrides DefaultVerifyTTL for verify and change links.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the email identifier plugin. Build it with New: the zero value has no kind, no templates and no operations.
func New ¶
New builds the plugin. With no options an address is unique among verified rows and verifiable, but neither signs in nor recovers: those are choices.
func (*Plugin) Add ¶
Add adds an unverified address and sends it a verify link. Adding one the user already has is a no-op; without Multiple a second is ErrSingleAddress.
func (*Plugin) ApproveChange ¶
ApproveChange spends an approval token (ApproveChangeFirst) and sends the confirm link to the new address.
func (*Plugin) ApproveChangeCode ¶
ApproveChangeCode checks the approval code Change mailed to the signed-in user's current address (ApproveChangeFirst with VerifyByCode) for a change to address, and sends the confirm code to that new address. A wrong code, a code for another address and no pending change are all auth.ErrCodeInvalid.
func (*Plugin) Change ¶
Change starts replacing the primary address by mailing a confirm link to the new one; nothing changes until ConfirmChange.
func (*Plugin) CheckCode ¶
CheckCode reports whether a code is right without spending it, so a form can validate the code the person typed before asking for what comes next (the new password, the new address). A wrong guess still counts an attempt, so peeking cannot be used to brute-force; every refusal is auth.ErrCodeInvalid.
func (*Plugin) Columns ¶
func (p *Plugin) Columns() []auth.ColumnExtension
Columns adds the primary address to auth_user. The data hooks keep it in step with auth_identity; nothing else may write it.
func (*Plugin) ConfirmChange ¶
ConfirmChange spends a change token: the new address becomes the verified primary, and the old primary is mailed an undo link.
func (*Plugin) ConfirmChangeCode ¶
ConfirmChangeCode checks the code Change mailed to the new address (VerifyByCode): the new address becomes the verified primary, and the old primary is mailed an undo link.
func (*Plugin) DataHooks ¶
DataHooks keep auth_user's copy of the primary address, and send a verify link for every unverified address however it was created.
func (*Plugin) Deliver ¶
Deliver renders m in its locale's template and sends it. The core calls it after the transaction commits, so mail never goes out for a rolled-back row.
func (*Plugin) Identifiers ¶
func (p *Plugin) Identifiers() []auth.IdentifierKind
Identifiers registers the email kind with the core, which from then on owns its uniqueness and verified checks.
func (*Plugin) Init ¶
Init captures the core and fills the default link builder from auth.WithBaseURL; it runs after Validate has checked one of them is set.
func (*Plugin) MakePrimary ¶
MakePrimary makes a verified address the primary one, the address messages go to. An unverified one is auth.ErrNotVerified.
func (*Plugin) Name ¶
Name returns PluginName, the key the core stores this plugin's migrations and route prefix under; it never changes.
func (*Plugin) Operations ¶
Operations lists every operation; the code ones only when SignsIn, so an app without codes exposes no route that mails them.
func (*Plugin) OptionErr ¶
OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).
func (*Plugin) RateLimits ¶
RateLimits bounds the operations that send mail, per address, so none can be used to flood an inbox.
func (*Plugin) Resend ¶
Resend sends a new verify link for an unverified address. It answers the same for every input, so it reveals no account.
func (*Plugin) SendCode ¶
SendCode mails a sign-in code when the address may receive one, and answers the same when it may not.
func (*Plugin) SendCodeWithMetadata ¶
func (p *Plugin) SendCodeWithMetadata(ctx context.Context, address string, metadata map[string]string) error
SendCodeWithMetadata is SendCode with metadata the Mailer receives beside the message; see AddressRequest.Metadata.
func (*Plugin) SendVerification ¶
SendVerification mails a fresh verify link for one of this plugin's unverified addresses, when ResendOnSignIn is set. The core calls it through its dispatcher after a correct password was refused for lack of verification; a verified or foreign identity is ignored.
func (*Plugin) SetFor ¶
SetFor gives userID the address, from server-side code with no session: an admin plugin's "set email" or "create user". It applies this plugin's own rules: the address is normalized; one another user has verified is auth.ErrIdentityTaken; one the user already holds is verified and made primary when verified is set, and left alone otherwise; without Multiple the user's other addresses are replaced, with it the address is added. verified creates the address verified and primary at once (the data hooks copy it to auth_user); otherwise it is created unverified and the data hooks mail the verify link as for any new address. An unknown user is auth.ErrUserNotFound. Everything runs in one transaction, or joins the one in ctx.
Not an operation: there is no caller to authorize here, the admin plugin has done that. From a request, use the operations.
func (*Plugin) SignInCode ¶
SignInCode checks a sign-in code and starts a session.
func (*Plugin) SignInCodeAs ¶
func (p *Plugin) SignInCodeAs(ctx context.Context, address, code, name string) (CodeSignInResult, error)
SignInCodeAs is SignInCode with the display name a new account gets (SignUpWithCode); the name is ignored when the address has an owner.
func (*Plugin) SignInCodeWith ¶
func (p *Plugin) SignInCodeWith(ctx context.Context, in CodeRequest) (CodeSignInResult, error)
SignInCodeWith is SignInCode with the full request: the profile (Name, Image) a new account gets under SignUpWithCode.
func (*Plugin) SignsIn ¶
SignsIn reports whether code sign-in is on, which is what counts as a way in for the core's "someone can sign in" check.
func (*Plugin) UndoChange ¶
UndoChange spends an undo token: the old address is primary again, the new one is removed, and every session of the user ends.
func (*Plugin) Validate ¶
Validate checks that messages can be sent and linked, so a setup that could never verify an address fails in auth.New, not at the first sign-up.
func (*Plugin) Verify ¶
Verify spends a verify token and marks its address verified. A token for an address removed since is ErrTokenInvalid.
func (*Plugin) VerifyCode ¶
VerifyCode checks a verify code (VerifyByCode) and marks its address verified: the signed-in user's own address, or signed out the address as typed. A wrong code, an unknown address and a verified one are all auth.ErrCodeInvalid.
type TemplateFunc ¶
TemplateFunc renders one message as a subject and a text body. It must include m.Link or m.Code when set, or the message is useless.
type TokenRequest ¶
TokenRequest carries a token from a link. The token is never logged: its String method redacts it.
type VerifyLinkRequest ¶
type VerifyLinkRequest struct {
Token auth.WireToken `json:"token"`
CallbackURL string `json:"callbackURL,omitempty"`
}
VerifyLinkRequest is the query of verify-link (VerifyByGET): the token and, optionally, the page to land on afterwards.
type VerifyResult ¶
type VerifyResult struct {
Email string `json:"email"`
// SignedIn is true when SignInOnVerify started a session for the
// address's user; SecondFactorRequired then says whether it is pending.
// Token is for Go callers and bearer transports, never encoded into JSON.
SignedIn bool `json:"signedIn,omitempty"`
SecondFactorRequired bool `json:"secondFactorRequired,omitempty"`
Token auth.Token `json:"-"`
}
VerifyResult is the output of verify and confirm-change: the normalized address now verified, which the page can show.