email

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

github.com/ubgo/auth/plugin/email

Adds email addresses to accounts: sign-in and recovery by address, verification links, one or many addresses per user, changing the address with an undo link to the old one, a welcome mail, and optional passwordless sign-in with a six-digit code.

Install

go get github.com/ubgo/auth

The package is part of the core module, so it adds no dependency. You provide the mail transport by implementing email.Mailer (or wrapping a function in email.MailerFunc) over SMTP, an API or a queue.

Wire it

From examples/quickstart:

a, err := auth.New(
	auth.WithDB(db, sqlb.SQLiteDialect{}),
	auth.WithBaseURL(baseURL),
	auth.WithPlugins(
		email.New(email.Login(), email.Recover(), email.WithMailer(m)),
		password.New(),
	),
)

With no options, an address is unique among verified rows and can be verified, but it does not sign in or recover. Login() and Recover() are choices you make explicitly.

Email is a sign-in method only with SignInWithCode() or SignUpWithCode(). Otherwise pair it with a method such as password, magiclink or oauth, or auth.New fails with auth: no sign-in method; add a plugin such as password or oauth.

Validate makes auth.New fail with:

Error Text Cause
ErrNoMailer email: no mailer; pass email.WithMailer WithMailer missing
ErrNoBaseURL email: links need auth.WithBaseURL or email.Links neither auth.WithBaseURL nor Links set
ErrCodesNeedLogin email: SignInWithCode needs Login() code sign-in on a kind that cannot sign in
ErrSignInOnVerifyNeedsLogin email: SignInOnVerify needs Login() a verify link that signs in, on a kind that cannot sign in
ErrBadCallback email: VerifyByGET's callback must be a path of this site or a URL on a trusted origin VerifyByGET given a page TrustsReturnURL refuses (checked at Init)

Links point at pages of your app, not at API routes. Each page reads ?token= and POSTs it to the matching route; if a GET spent the token, every mail scanner that previews links would spend it first. The default pages are DefaultVerifyPage (/verify-email), DefaultChangePage (/confirm-email-change) and DefaultUndoPage (/undo-email-change), plus DefaultApprovePage (/approve-email-change) under ApproveChangeFirst(), or you can supply your own with Links. VerifyByGET is the opt-in exception for the verify link, better-auth's behaviour, with the risk spelled out below.

Options

Option What it does Default
Login() The address can sign in; auth.New then requires it unique off
Recover() The address receives recovery messages (reset links); only a verified one off
Contact() Contact only: several users may share an address, and it neither signs in nor recovers off (unique among verified)
ScopedByTenant() Addresses unique per tenant (auth.WithScope) global
RequireVerified() Refuse sign-in until the address is verified off
Multiple() Many addresses per user, one primary one address per user
SignInWithCode() Adds send-code and sign-in-code, which sign in a verified owner with a mailed code; needs Login() off
SignUpWithCode() Like SignInWithCode, and a code also creates an account for an address nobody has verified off
Welcome() Mails auth.PurposeWelcome once per user, when their first address is verified off
WithMailer(m) How mail is sent. Required none
WithNormalizer(fn) Replaces Normalize (trim + lowercase), e.g. to fold dots or +tags Normalize
AllowDomains(domains...) The allow-list: refuses every new address whose domain the list does not name, with the same ErrAddressRejected, when an address check would run. Each entry matches exactly, so acme.com allows neither eu.acme.com nor notacme.com; *.acme.com allows every subdomain and not acme.com itself, so list both for both; case-insensitive. Its check joins CheckAddresses' and runs where it stands among the options, so give it first to skip a DNS lookup for an outside address. An empty list or an entry naming no domain fails auth.New with ErrBadDomainList, naming the option and the entry none
CheckAddresses(checks...) Runs each AddressCheck on a new address (created at sign-up, add, change or an OAuth sign-up, and before a change's confirm mail; never at sign-in); a refusal is ErrAddressRejected. Built in: BlockDomains(domains...) (a domain and its subdomains, case-insensitive; ParseDomainList reads a one-per-line list) and RequireMX(resolver, timeout) (refuses a domain that does not exist, the RFC 7505 null MX, or no MX and no address record; any other lookup failure lets the address through; timeout 0 is DefaultMXTimeout, 3s, because the check runs inside the write's transaction). No domain list ships with the library none
VerifyByCode() Verification and address change work by a code the person types instead of a link: 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. The undo mail stays a link; the link routes stay registered (VerifyByCode) off: links
VerifyByGET(callback) better-auth's GET /verify-email?token&callbackURL: adds GET /email/verify-link?token=…&callbackURL=…, which verifies (and signs in under SignInOnVerify) and redirects to callbackURL, or to callbackURL?error=<code> for a bad link; without a trusted callbackURL a failure lands on auth.WithErrorURL's page, and the route answers JSON when neither is set. The verify mail then links there: BaseURL + DefaultMountPrefix (/auth) + /email/verify-link, with callback ("" for none) as its callbackURL. Every callback must pass auth.Auth.TrustsReturnURL (email.callback_invalid at the route, ErrBadCallback at auth.New). Risk: a mail scanner that fetches the link spends it, and with SignInOnVerify a GET sign-in cannot be origin-checked, so a link to an attacker's own verification signs a victim into the attacker's account; auth.WithFetchMetadata judges mutations only and does not cover this GET off: the link opens your page, which POSTs
SignInOnVerify() The verify link also starts a session for the address's user (the click proves the mailbox, as a magic link does); verify then answers signedIn, secondFactorRequired and sets the cookie. Needs Login() off
GenerateCode(fn) better-auth's generateOTP, a plain override: fn(ctx, CodeFor{Email, Kind}) returns every code this plugin mails (verify, sign-in, change, approve), compared exactly; an error or an empty code from fn fails the send. Risk: a code is only as hard to guess as fn makes it off: auth.WithCodeLength digits from crypto/rand
ReuseCodeOnResend() better-auth's resendStrategy: "reuse": a send while a code is open mails the same code and extends its expiry by the code TTL; wrong guesses already made still count, and a code with no attempts left is replaced. Needs auth.WithSecret (auth.ErrNoSecret at auth.New). The default code is derived (an HMAC of its row id under a key from the secret); a GenerateCode code is stored sealed under that key and mailed again. Risk: the database plus the secret reveal every open code, and a longer-lived code gives a guesser more time off: rotate, random digits
WithCodeTTL(d) Lifetime of a mailed sign-in code DefaultCodeTTL = 10 minutes
WithMaxAttempts(n) Wrong guesses one sign-in code survives auth.DefaultCodeMaxAttempts = 5
WithCodeSendLimit(n, window) Rate rule of send-code per address 3 per 15 minutes (codeSendLimit, codeSendWindow)
WithCodeTryLimit(n, window) Rate rule of sign-in-code per address 10 per 15 minutes (codeTryLimit, codeTryWindow)
WithVerifyTTL(d) Lifetime of verify and change links DefaultVerifyTTL = 24h
WithUndoTTL(d) How long the old address can undo a change DefaultUndoTTL = 7 days
ResendOnSignIn() SendVerification mails a fresh verify link; the core calls it when a correct password is refused because the address is unverified (RequireVerified()), so the user gets the link again without a resend off: nothing is sent at sign-in
ReplaceUnverified() 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); the old unverified rows are deleted. Once an address is verified a change is the two-step flow again. A change to another account's verified address answers the same, changes nothing and tells that owner someone tried, as a duplicate sign-up does. The change rate rule covers it off: a change always verifies first
ChangeWithoutVerification() change replaces the address at once even when it is verified (better-auth's updateEmailWithoutVerification); warned about at every start (WarnChangeWithoutVerification) off
ApproveChangeFirst() change mails an approval link to the current verified primary address first (PurposeChangeApproval, page DefaultApprovePage = /approve-email-change); the confirm link goes to the new address only after approve-change. Under VerifyByCode() the approval is a code (PurposeApproveCode), typed by the signed-in user at approve-change-code. A user with no verified current address gets the one-step flow off
Links(fn) Builds the URL for each purpose (PurposeVerifyToken, PurposeChangeToken, PurposeUndoToken) DefaultLink(baseURL, …)
Templates(fn) Replaces the English subjects and bodies DefaultTemplates
TemplatesByLocale(map) Translations picked by auth.PickLocale; falls back to Templates none

A message of another plugin's purpose (an organization invite, say) uses that plugin's auth.Message.DefaultSubject and DefaultText; without them it is "A message about your account" and the bare link. Your Templates still win.

Sign-in codes have auth.DefaultCodeLength (6) digits unless the app sets auth.WithCodeLength; they live DefaultCodeTTL (10 minutes, WithCodeTTL) and allow auth.DefaultCodeMaxAttempts (5, WithMaxAttempts) wrong guesses. Addresses longer than maxEmailLength (254) are refused.

Routes and Go API

The routes are under your mount prefix (for example /auth). The rate limits are this plugin's own rules, keyed by the normalized address.

Route Go method Requirement Rate limit
POST /email/verify Verify(ctx, token) Public none
GET /email/verify-link?token=…&callbackURL=… none: a browser follows the mailed link Public none; only with VerifyByGET
POST /email/resend Resend(ctx, address) Public 3 per hour (resendLimit, resendWindow)
GET /email/list List(ctx) SignedIn none
POST /email/add Add(ctx, address) SignedIn none
POST /email/make-primary MakePrimary(ctx, address) FreshSignIn none
POST /email/remove Remove(ctx, address) FreshSignIn none
POST /email/change Change(ctx, address) FreshSignIn 5 per hour (changeLimit, changeWindow)
POST /email/confirm-change ConfirmChange(ctx, token) Public none
POST /email/undo-change UndoChange(ctx, token) Public none
POST /email/approve-change ApproveChange(ctx, token) Public none; only with ApproveChangeFirst()
POST /email/verify-code (email, code) VerifyCode(ctx, address, code) returns VerifyResult Public 10 per 15 minutes per address (codeTryLimit, codeTryWindow); only with VerifyByCode()
POST /email/confirm-change-code (email, code) ConfirmChangeCode(ctx, address, code) returns VerifyResult SignedIn 10 per 15 minutes per address; only with VerifyByCode()
POST /email/approve-change-code (email: the new address, code) ApproveChangeCode(ctx, address, code): checks the code mailed to the current address and sends the confirm code to the new one SignedIn 10 per 15 minutes per address; only with VerifyByCode() and ApproveChangeFirst()
POST /email/check-code (kind: verify, sign_in, change or approve; email, code) CheckCode(ctx, kind, address, code) Public 10 per 15 minutes per address, shared with the guesses; only with VerifyByCode() or SignInWithCode()
(no route) SetFor(ctx, userID, address, verified): server-side set of a user's address under the plugin's rules, for an admin plugin; replaces the address (adds, with Multiple()), verified and primary at once or unverified with the verify link mailed; a verified address of another user is auth.ErrIdentityTaken (SetFor) the caller's the caller's
POST /email/send-code (email, optional metadata) SendCode(ctx, address), SendCodeWithMetadata(ctx, address, metadata) Public 3 per 15 minutes (codeSendLimit, codeSendWindow)
POST /email/sign-in-code (email, code, optional name, image and sign-up fields, top-level or in fields, for the account a code creates) SignInCode(ctx, address, code), SignInCodeAs(ctx, address, code, name), SignInCodeWith(ctx, CodeRequest) Public 10 per 15 minutes (codeTryLimit, codeTryWindow)

send-code and sign-in-code exist only with SignInWithCode() or SignUpWithCode() together with Login(). The operation ids are exported as Endpoint* constants for hooks and rate rules.

Behaviour worth knowing:

  • resend always answers OK, and sends only to an unverified address. Signed in, it considers only the caller's own addresses. A new verify link spends every earlier one of the address, so only the newest mail verifies.

  • add with an address the user already has does nothing. Without Multiple(), a second address is ErrSingleAddress.

  • change creates no row. It mails a confirm link to the new address, so a typo changes nothing.

  • confirm-change makes the new address the verified primary. Without Multiple() it removes the old ones, and it mails the old primary (if it was verified) an undo link.

  • undo-change restores the old address as primary, removes the address that replaced it, and deletes every session of the user.

  • remove of the primary address promotes the next verified one.

  • check-code says whether a code is right without spending it (CheckCode), so a form can validate the code before asking for the new password or address. A wrong peek counts an attempt like a wrong guess, so peeking cannot brute-force a code. A kind whose feature is off, an unknown address, a verified one and a change code from a signed-out caller all answer auth.code_invalid.

  • send-code and the magic-link send take an optional metadata object (at most 8 entries of 256 bytes each, CheckMetadata) that reaches the Mailer as Mail.Metadata beside the message and is never stored; over the bounds the request is refused with auth.bad_request and nothing is sent.

Tables and columns

The plugin has no table of its own.

  • Addresses: each address is a row of the core auth_identity table with kind = 'email'. When the kind is unique, the core adds one unique index for it, in migration 0000_identity_unique_email_<mode>. The mode is verified by default, or all, with a _scoped suffix under ScopedByTenant().
  • Columns on auth_user (owned by this plugin, kept in step by its data hooks):
Column Type Meaning
primary_email (ColPrimaryEmail) text, nullable the verified primary address
email_verified_at (ColEmailVerifiedAt) timestamp, nullable when it was verified

Read them with the typed columns email.PrimaryEmail and email.EmailVerifiedAt, or embed email.EmailColumns in your user struct and bind it with email.Bindings.

stability_test.go lists the auth_identity columns (id, user_id, kind, value, value_raw, verified_at, is_primary, scope_id, created_at) as stable. The two auth_user columns above are not on that list.

Errors

Error Code HTTP When
email.ErrSingleAddress email.single_address 409 add of a second address without Multiple()
email.ErrAddressRejected email.address_rejected 400 a CheckAddresses check refused a new address; one message for every reason
email.ErrCallbackInvalid email.callback_invalid 400 verify-link (VerifyByGET) with a callbackURL that is not a site path or a URL on a trusted origin; the token is not spent
auth.ErrTokenInvalid auth.token_invalid 400 a verify, change or undo link that is bad, spent, expired, or for an address removed since
auth.ErrCodeInvalid auth.code_invalid 400 a wrong, spent or expired code, a malformed address, or no verified owner (without SignUpWithCode); under VerifyByCode() also an address with no open code, one that is not the signed-in user's, or one already verified
auth.ErrIdentifierInvalid auth.identifier_invalid 400 the value is not an address
auth.ErrIdentityTaken auth.identity_taken 409 another account verified the address after this link or code was mailed; an add, change or resend for an address already verified elsewhere mails that owner the duplicate sign-up notice instead of a link, and answers the requester as for a free address
auth.ErrIdentityNotFound auth.identity_not_found 404 make-primary or remove of an address the user does not have
auth.ErrNotVerified auth.not_verified 409 make-primary of an unverified address
auth.ErrLastSignInMethod auth.last_sign_in_method 409 remove of the user's last way in
auth.ErrRateLimited auth.rate_limited 429 a limit in the table above

ErrNoMailer, ErrNoBaseURL, ErrCodesNeedLogin and ErrSignInOnVerifyNeedsLogin are configuration errors from auth.New, never returned to a user.

Security notes

Property Pinned by
resend reveals nothing: same answer for verified, unknown and malformed addresses, and mail only to an unverified one integration/TestEmail_ResendOnlyForUnverified, TestIdentEmail_ResendSignedIn
A resend revokes the earlier verify link: only the newest works integration/TestR23_EmailResendRevokesEarlierLink
A code signs in only the verified owner, works once, and is bound to its tenant integration/TestEmailCode_SignInOwnerOnlyAndSingleUse, TestIdentEmail_CodeEdgeCases
An address another account verified cannot be verified again or taken by a change integration/TestEmail_VerifiedAddressCannotBeVerifiedTwice, TestIdentEmail_ConfirmChangeToATakenAddress
A change mails an undo link to the old verified address, and undo signs every session out integration/TestEmail_ChangeAndUndo, TestIdentEmail_UndoAfterTheNewAddressWasRemoved
No undo link goes to an unverified old address integration/TestIdentEmail_ChangeWithoutAVerifiedPrimarySendsNoUndo
The last way into an account cannot be removed integration/TestEmail_MultipleAddressesAndLastSignInMethod
AllowDomains lets in exactly the listed domains and only the subdomains a *. entry asks for, case and a trailing dot aside, at sign-up, add and change; an account from before the list still signs in; the refusal body is the same for an address that has an account elsewhere and one that has none; an empty list or a bare *. fails auth.New with ErrBadDomainList; the check runs where it stands among the options TestAllowDomains, TestAllowDomains_RefusesAListThatAllowsNobody, TestAllowDomains_JoinsTheChecksInOptionOrder, integration/TestBatch1_EmailAllowDomains
The welcome goes once per user, only to a proven address integration/TestWelcome_OncePerUser
ReuseCodeOnResend() mails the same code and extends it, keeps the wrong-guess count, and replaces an exhausted code; without it a send rotates integration/TestR21_EmailReuseCodeOnResend, TestR21_EmailRotatesByDefault, TestR21_EmailReuseWithGenerator
GenerateCode's code is the one mailed and accepted; a generator error fails the send and mails nothing integration/TestR21_EmailGenerateCode, TestR21_GeneratorErrorsFailTheSend
fields on sign-in-code sets only auth.WithSignUpFields columns of a created account integration/TestR21_EmailCodeSignUpFields
Under VerifyByCode() the mail carries a code and no link; a wrong code, an unknown address and a verified one answer alike; a resend replaces the code integration/TestEmailVerifyByCode_VerifiesWithACode
A verify code is bound to its owner's row: another user's session cannot spend it; SignInOnVerify works from a code integration/TestEmailVerifyByCode_SignedInAndSignInOnVerify
A change code is bound to the user and the new address; the undo link is still sent integration/TestEmailVerifyByCode_ChangeByCode
Under ApproveChangeFirst() with VerifyByCode() the approval mail carries a code and no link; the code is checked by the signed-in user for the address it was mailed for (a wrong code, another address, another user's session and a peek of the wrong kind answer alike and send nothing), works once, and only then does the confirm code go to the new address; the link route stays registered integration/TestEmailVerifyByCode_ChangeWithApprovalFirst
SetFor replaces or adds, verifies an own address in place, refuses a taken one and an unknown user integration/TestEmail_SetFor
A peek never spends a code; a wrong peek counts an attempt; every refusal answers alike integration/TestEmail_CheckCode
Metadata reaches the Mailer with the message, a plain send carries none, and an oversized map is refused before anything is sent integration/TestMetadata_ReachesTheSender
Every database failure fails the operation, none is reported as success integration/TestIdentFaults_Email, integration/TestIdentFaults_EmailVerifyByCode
Normalization happens in Go, and the comparison is exact on every engine integration/TestPortability_UnicodeFoldsInGoAndComparesExactly

Limits:

  • Validate is a structural check only; delivery of the verify link is the real validation.
  • Mail is sent after the transaction commits, in the background. A Mailer error is logged, not returned, so an operation can report OK for mail that failed to send.
  • Normalize keeps dots and +tags, so a.b+x@example.com and ab@example.com are two addresses unless you pass WithNormalizer.

Example

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

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

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

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

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

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

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

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

View Source
const DefaultMountPrefix = "/auth"

DefaultMountPrefix is where VerifyByGET's default link assumes the auth handler is mounted, as the oauth plugin's default callback does.

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

View Source
const PluginName = "email"

PluginName is the plugin's permanent identity: route prefix and migration owner.

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

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

View Source
const WarnChangeWithoutVerification = "" /* 164-byte string literal not displayed */

WarnChangeWithoutVerification is logged by Init under ChangeWithoutVerification.

Variables

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.

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

CodeKindValues is the canonical iteration order over code kinds.

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

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

func CheckMetadata(m map[string]string) error

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(base string, purpose auth.Purpose, t auth.Token) string

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

func DefaultTemplates(m auth.Message) (subject, text string)

DefaultTemplates are short English messages. A link is on its own line so every client makes it clickable.

func Normalize

func Normalize(raw string) (string, error)

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

func ParseDomainList(r io.Reader) ([]string, error)

ParseDomainList reads one domain per line, skipping blank lines and lines starting with "#", the format of the common disposable-domain lists.

func Validate

func Validate(v string) error

Validate is a structural check only. No pattern matches the real grammar; delivery of the verify link is the real validation.

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

type AddressCheck func(ctx context.Context, address string) error

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

type AddressCodeRequest struct {
	Email string `json:"email"`
	Code  string `json:"code"`
}

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

type CodeFor struct {
	Email string
	Kind  CodeKind
}

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

type CodeGenerator func(ctx context.Context, in CodeFor) (string, error)

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

type EmailColumns struct {
	PrimaryEmail    *string
	EmailVerifiedAt *time.Time
}

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 Mailer

type Mailer interface {
	Send(ctx context.Context, m Mail) error
}

Mailer sends mail. The app implements it over SMTP, an API or a queue.

type MailerFunc

type MailerFunc func(ctx context.Context, m Mail) error

MailerFunc adapts a function to Mailer, for an app whose sender is a closure over its SMTP or API client.

func (MailerFunc) Send

func (f MailerFunc) Send(ctx context.Context, m Mail) error

Send calls f, so a closure satisfies Mailer without a named type.

type Option

type Option func(*Plugin)

Option configures the plugin; options are applied in order by New.

func AllowDomains

func AllowDomains(domains ...string) Option

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(fn func(p auth.Purpose, t auth.Token) string) Option

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

func Templates(fn func(m auth.Message) (subject, text string)) Option

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

func VerifyByGET(callback string) Option

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

func WithCodeSendLimit(n int, window time.Duration) Option

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

func WithCodeTTL(d time.Duration) Option

WithCodeTTL overrides DefaultCodeTTL, how long a mailed sign-in code works.

func WithCodeTryLimit

func WithCodeTryLimit(n int, window time.Duration) Option

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

func WithMailer(m Mailer) Option

WithMailer sets how mail is sent. Required: auth.New fails with ErrNoMailer without it, because verification is impossible without mail.

func WithMaxAttempts

func WithMaxAttempts(n int) Option

WithMaxAttempts overrides auth.DefaultCodeMaxAttempts for sign-in codes: the wrong guesses one code survives before it is spent.

func WithNormalizer

func WithNormalizer(fn func(raw string) (string, error)) Option

WithNormalizer replaces Normalize, for an app that folds dots or +tags.

func WithUndoTTL

func WithUndoTTL(d time.Duration) Option

WithUndoTTL overrides DefaultUndoTTL, how long the old address can undo a change.

func WithVerifyTTL

func WithVerifyTTL(d time.Duration) Option

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

func New(opts ...Option) *Plugin

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

func (p *Plugin) Add(ctx context.Context, address string) error

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

func (p *Plugin) ApproveChange(ctx context.Context, t auth.Token) error

ApproveChange spends an approval token (ApproveChangeFirst) and sends the confirm link to the new address.

func (*Plugin) ApproveChangeCode

func (p *Plugin) ApproveChangeCode(ctx context.Context, address, code string) error

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

func (p *Plugin) Change(ctx context.Context, address string) error

Change starts replacing the primary address by mailing a confirm link to the new one; nothing changes until ConfirmChange.

func (*Plugin) CheckCode

func (p *Plugin) CheckCode(ctx context.Context, kind CodeKind, address, code string) error

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

func (p *Plugin) ConfirmChange(ctx context.Context, t auth.Token) (VerifyResult, error)

ConfirmChange spends a change token: the new address becomes the verified primary, and the old primary is mailed an undo link.

func (*Plugin) ConfirmChangeCode

func (p *Plugin) ConfirmChangeCode(ctx context.Context, address, code string) (VerifyResult, error)

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

func (p *Plugin) DataHooks() auth.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

func (p *Plugin) Deliver(ctx context.Context, to auth.Identity, m auth.Message) error

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

func (p *Plugin) Init(a *auth.Auth) error

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) List

func (p *Plugin) List(ctx context.Context) ([]Address, error)

List returns the signed-in user's addresses, verified or not.

func (*Plugin) MakePrimary

func (p *Plugin) MakePrimary(ctx context.Context, address string) error

MakePrimary makes a verified address the primary one, the address messages go to. An unverified one is auth.ErrNotVerified.

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns PluginName, the key the core stores this plugin's migrations and route prefix under; it never changes.

func (*Plugin) Operations

func (p *Plugin) Operations() []auth.Operation

Operations lists every operation; the code ones only when SignsIn, so an app without codes exposes no route that mails them.

func (*Plugin) OptionErr

func (p *Plugin) OptionErr() error

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

func (p *Plugin) RateLimits() []auth.RateRule

RateLimits bounds the operations that send mail, per address, so none can be used to flood an inbox.

func (*Plugin) Remove

func (p *Plugin) Remove(ctx context.Context, address string) error

Remove removes an address, unless it is the last way into the account.

func (*Plugin) Resend

func (p *Plugin) Resend(ctx context.Context, address string) error

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

func (p *Plugin) SendCode(ctx context.Context, address string) error

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

func (p *Plugin) SendVerification(ctx context.Context, to auth.Identity) error

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

func (p *Plugin) SetFor(ctx context.Context, userID, address string, verified bool) error

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

func (p *Plugin) SignInCode(ctx context.Context, address, code string) (CodeSignInResult, error)

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

func (p *Plugin) SignsIn() bool

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

func (p *Plugin) UndoChange(ctx context.Context, t auth.Token) error

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

func (p *Plugin) Validate(c *auth.Config) error

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

func (p *Plugin) Verify(ctx context.Context, t auth.Token) (VerifyResult, error)

Verify spends a verify token and marks its address verified. A token for an address removed since is ErrTokenInvalid.

func (*Plugin) VerifyCode

func (p *Plugin) VerifyCode(ctx context.Context, address, code string) (VerifyResult, error)

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

type TemplateFunc = func(m auth.Message) (subject, text string)

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

type TokenRequest struct {
	Token auth.WireToken `json:"token"`
}

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.

Jump to

Keyboard shortcuts

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