Documentation
¶
Overview ¶
Package phone registers the phone identifier kind (E.164 numbers), verifies numbers with SMS codes, and can sign users in — and up — with a code alone.
A number is a row of the core auth_identity table, so one uniqueness policy and one verified check cover it with every other kind. The codes are the core's one-time codes (auth.IssueCode, auth.CheckCode), shared with the email plugin, so the attempt counter and single use live in one place.
Index ¶
- Constants
- Variables
- func DefaultTemplate(m auth.Message) string
- func Normalize(raw string) (string, error)
- func Of(a *auth.Auth, userID string, dst *Data) (sqlb.Query, error)
- func UserBindings(dst *Data) []sqlb.Binding
- func UserJoin() sqlb.Join
- func Validate(v string) error
- type ChangeResult
- type CodeFor
- type CodeGenerator
- type CodeRequest
- type Data
- type ListResult
- type MetadataSender
- type Number
- type NumberRequest
- type Option
- func AllowPrefixes(prefixes ...string) Option
- func Contact() Option
- func GenerateCode(fn CodeGenerator) Option
- func LimitRange(digits, limit int, window time.Duration) Option
- func Login() Option
- func Multiple() Option
- func Recover() Option
- func RequireVerified() Option
- func ResendOnSignIn() Option
- func ScopedByTenant() Option
- func SignInWithCode() Option
- func Templates(fn func(m auth.Message) string) Option
- func TemplatesByLocale(byLocale map[string]func(m auth.Message) string) Option
- func WithCodeTTL(d time.Duration) Option
- func WithMaxAttempts(n int) Option
- func WithNormalizer(fn func(raw string) (string, error)) Option
- func WithSMS(s SMSSender) Option
- func WithValidator(fn func(normalized string) error) Option
- func WithVerifier(v Verifier) Option
- type Plugin
- func (p *Plugin) Add(ctx context.Context, number string) error
- func (p *Plugin) Change(ctx context.Context, number string) error
- func (p *Plugin) ConfirmChange(ctx context.Context, number, code string) (ChangeResult, 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) ([]Number, error)
- func (p *Plugin) MakePrimary(ctx context.Context, number 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, number string) error
- func (p *Plugin) Resend(ctx context.Context, number string) error
- func (p *Plugin) SendCode(ctx context.Context, number string) error
- func (p *Plugin) SendVerification(ctx context.Context, to auth.Identity) error
- func (p *Plugin) SignIn(ctx context.Context, number, code string) (SignInResult, error)
- func (p *Plugin) SignInWith(ctx context.Context, in SignInRequest) (SignInResult, error)
- func (p *Plugin) SignsIn() bool
- func (p *Plugin) Tables() []sqlb.Table
- func (p *Plugin) Validate(*auth.Config) error
- func (p *Plugin) Verify(ctx context.Context, number, code string) error
- type SMSFunc
- type SMSSender
- type SignInRequest
- type SignInResult
- type Verifier
Constants ¶
const ( EndpointAdd auth.EndpointID = "phone.add" EndpointVerify auth.EndpointID = "phone.verify" EndpointResend auth.EndpointID = "phone.resend" EndpointRemove auth.EndpointID = "phone.remove" EndpointSendCode auth.EndpointID = "phone.send-code" EndpointSignIn auth.EndpointID = "phone.sign-in" EndpointChange auth.EndpointID = "phone.change" EndpointConfirmChange auth.EndpointID = "phone.confirm-change" // EndpointList and EndpointMakePrimary are registered only under // Multiple: with one number per account there is nothing to list or // choose between. EndpointList auth.EndpointID = "phone.list" EndpointMakePrimary auth.EndpointID = "phone.make-primary" )
Operation ids, exported so a hook or a rate rule can name them. They are wire names, stable across releases.
const ( PurposeVerifyCode auth.Purpose = "phone.verify" PurposeSignInCode auth.Purpose = "phone.sign_in" PurposeChangeCode auth.Purpose = "phone.change" )
Code purposes. The subject of a verify code is the identity id; of a sign-in code, the normalized number, because the account may not exist; of a change code, the user id and the new number (changeSubject), because no row exists for the new number until the code is checked.
const ( //ds:def id=defaultcodettl-38ny9rrh owner=@khanakia stability=stable DefaultCodeTTL = 10 * time.Minute //ds:def id=defaultmaxattempts-9thd5u9b owner=@khanakia stability=stable DefaultMaxAttempts = 5 )
Code defaults. The TTL is short because an SMS arrives in seconds; the attempt cap exists because a six-digit code is a million guesses, nothing to a script and everything to a counter.
const ( //ds:def id=mindigits-s68ve98t owner=@khanakia stability=stable MinDigits = 7 //ds:def id=maxdigits-9yc3z2gc owner=@khanakia stability=stable MaxDigits = 15 )
E.164 bounds: the shortest plausible international number and the maximum.
const KindPhone auth.IdentifierKindName = "phone"
KindPhone is the identifier kind this plugin registers. It is stored in every auth_identity row this plugin owns, so it never changes.
const PluginName = "phone"
PluginName is the plugin's permanent identity.
const TableAliasIdentity = "phi"
TableAliasIdentity is this plugin's alias for the shared core identity table; a composite user query joins that table once per identifier kind, and each occurrence needs its own alias.
Variables ¶
var ( IdentityUserID = sqlb.Col[string](TableIdentity, auth.ColIdentityUserID) IdentityKind = sqlb.Col[auth.IdentifierKindName](TableIdentity, auth.ColIdentityKind) IdentityValue = sqlb.Col[string](TableIdentity, auth.ColIdentityValue) IdentityValueRaw = sqlb.Col[string](TableIdentity, auth.ColIdentityValueRaw) IdentityVerifiedAt = sqlb.Col[*time.Time](TableIdentity, auth.ColIdentityVerifiedAt) IdentityIsPrimary = sqlb.Col[bool](TableIdentity, auth.ColIdentityIsPrimary) )
Typed columns on the identity table under this plugin's alias, for UserJoin and Of; the same columns as the core's, qualified by "phi".
var ( // ErrCodeInvalid is the core's one-time-code error, re-exported so a // caller of this package need not import the core to match it. ErrCodeInvalid = auth.ErrCodeInvalid ErrHasNumber = auth.NewError("phone.has_number", http.StatusConflict, "This account already has a phone number. Remove it first.") ErrNoSMS = errors.New("phone: no SMS sender; pass phone.WithSMS") ErrCodeNoLogin = errors.New("phone: SignInWithCode needs Login()") )
Errors. ErrCodeInvalid and ErrHasNumber reach the user; ErrNoSMS and ErrCodeNoLogin are configuration errors auth.New returns, so a setup that could never text a code fails at start.
var ErrGenerateWithVerifier = errors.New("phone: GenerateCode has no effect with WithVerifier, whose provider makes the code")
ErrGenerateWithVerifier is GenerateCode together with WithVerifier: the provider makes and checks the code, so a generator would never be used.
var TableIdentity = sqlb.Table{Name: auth.TableNameIdentity, Alias: TableAliasIdentity}
TableIdentity is the core identity table under this plugin's alias.
Functions ¶
func DefaultTemplate ¶
DefaultTemplate is the default English text for codes that live DefaultCodeTTL. The plugin's own default states its configured TTL instead, so WithCodeTTL never leaves a text promising the wrong lifetime; an app that passes DefaultTemplate to Templates with another TTL should write its own.
func Normalize ¶
Normalize reduces a number to "+" and digits. Without it "+1 (555) 0100" and "+15550100" would be two rows and the unique index would prevent nothing. A number must be typed with its country code: guessing a country is a product decision, made by WithNormalizer-style wrapping in the app.
func Of ¶
Of builds the standalone lookup for one user's primary phone data, for an app that reads it outside a composite user query.
func UserBindings ¶
UserBindings binds this plugin's columns to dst. Every column of a LEFT join must survive NULL, so strings and the flag are coalesced and VerifiedAt is a pointer, where nil is the meaningful answer.
Types ¶
type ChangeResult ¶
type ChangeResult struct {
Phone string `json:"phone"`
}
ChangeResult is the output of confirm-change: the normalized number now verified and primary.
type CodeFor ¶
CodeFor is what a custom code generator (GenerateCode) is told about the code it makes: the normalized number it goes to and the code's purpose, PurposeVerifyCode, PurposeChangeCode or PurposeSignInCode.
type CodeGenerator ¶
CodeGenerator returns the code to text instead of the core's random digits.
type CodeRequest ¶
CodeRequest carries a number and the six-digit code texted to it.
func (CodeRequest) RateIdentifier ¶
func (r CodeRequest) RateIdentifier() string
RateIdentifier counts guesses per normalized number; see NumberRequest.RateIdentifier for the malformed case.
type Data ¶
Data is the phone plugin's contribution to a user, shaped as a nested edge: a number lives in auth_identity, not auth_user, so it reads as u.Phone.Number (ADR-0001). Every field is empty when the user has no number.
type ListResult ¶
type ListResult struct {
Numbers []Number `json:"numbers"`
}
ListResult is the output of list, in creation order; Numbers is never nil, so it encodes as [] rather than null.
type MetadataSender ¶
type MetadataSender interface {
SMSSender
SendWithMetadata(ctx context.Context, to, text string, metadata map[string]string) error
}
MetadataSender is an SMSSender that also wants the metadata a sending operation attached to the message (auth.Message.Metadata), such as an invite id a magic link was requested with. Optional: a sender without it gets the text alone, and the library calls SendWithMetadata only when the message carries metadata.
type Number ¶
type Number struct {
Phone string `json:"phone"`
Verified bool `json:"verified"`
Primary bool `json:"primary"`
}
Number is one row of the list. Verified and Primary are separate because an unverified number is listed but can never be primary.
type NumberRequest ¶
type NumberRequest struct {
Phone string `json:"phone"`
}
NumberRequest names a number as the user typed it; each operation normalizes it.
func (NumberRequest) RateIdentifier ¶
func (r NumberRequest) RateIdentifier() string
RateIdentifier counts per normalized number. A malformed number keys as "" and is refused by the operation anyway, so sharing that key costs nothing.
type Option ¶
type Option func(*Plugin)
Option configures the plugin; options are applied in order by New.
func AllowPrefixes ¶
AllowPrefixes accepts only numbers that start with one of prefixes, such as "+1" and "+44". Every other number is refused on every path — sign-up, add, code sign-in — as an invalid identifier. It is the first defence against SMS pumping, where bots request codes for expensive numbers abroad.
func Contact ¶
func Contact() Option
Contact makes numbers contact information only: several users may share one, 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 texts (verify, change and sign-in), as email.GenerateCode does for mail: a fixed code for an app-store review account, a code in another alphabet, one from the app's own generator. Whatever fn returns is the code, compared exactly; an error from fn fails the send; the core refuses an empty code or one longer than auth.MaxGeneratedCodeLength. Off by default; the default draws auth.WithCodeLength digits (4 to 10) from crypto/rand. auth.New refuses it beside WithVerifier.
Risk: a code is only as hard to guess as fn makes it, and the attempt and rate limits are tuned for random digits of the configured length.
func LimitRange ¶
LimitRange counts code requests per number range: the first digits of the number, "+" included. LimitRange(6, 20, time.Hour) allows 20 codes an hour to all numbers starting "+15550", however many numbers and IPs a bot uses. It adds to the per-number and per-IP limits, never replaces them.
func Login ¶
func Login() Option
Login allows numbers in sign-in. auth.New then requires them unique, since a login box cannot pick between two users.
func Multiple ¶
func Multiple() Option
Multiple allows several numbers per user, one of them primary: the one texts go to and the one a composite user query shows. Add accepts a second number instead of ErrHasNumber, a confirmed change adds the new number as primary and keeps the others, verifying a number makes it primary only when the user has no verified primary yet, and the list and make-primary routes are registered. Removing the primary promotes another verified number, so texts keep a destination.
func Recover ¶
func Recover() Option
Recover allows numbers to receive recovery messages. Only a verified number receives one.
func RequireVerified ¶
func RequireVerified() Option
RequireVerified refuses sign-in through an unverified number, which could be anybody's claim.
func ResendOnSignIn ¶
func ResendOnSignIn() Option
ResendOnSignIn makes SendVerification text a fresh verify code, so a password sign-in refused because the number is unverified (RequireVerified) gets a code again without a separate resend. Off, nothing is sent.
func ScopedByTenant ¶
func ScopedByTenant() Option
ScopedByTenant makes numbers unique per tenant, for an app where one person has a separate account in each; see auth.WithScope.
func SignInWithCode ¶
func SignInWithCode() Option
SignInWithCode enables sign-in, and sign-up, with an SMS code alone.
func Templates ¶
Templates replaces the default English texts, for every locale TemplatesByLocale does not cover. It must include m.Code or m.Link.
func TemplatesByLocale ¶
TemplatesByLocale adds translations, picked by auth.PickLocale; a message in a language with none uses the templates set by Templates, or the English defaults. The library ships no translations.
The choice is made at delivery, so the order of this option and Templates does not matter.
func WithCodeTTL ¶
WithCodeTTL overrides DefaultCodeTTL. The default text states the TTL set here.
func WithMaxAttempts ¶
WithMaxAttempts overrides DefaultMaxAttempts, the wrong guesses one code survives before it is spent.
func WithNormalizer ¶
WithNormalizer replaces Normalize, for an app whose users type numbers without the country code; contrib/phonenumbers has one built on libphonenumber. The result must be E.164 ("+" and digits): it is what is stored, compared and texted.
func WithSMS ¶
WithSMS sets how text messages are sent. Required: auth.New fails with ErrNoSMS without it, because no number could ever be verified.
func WithValidator ¶
WithValidator replaces Validate, such as a country-aware check that a number can exist. AllowPrefixes still applies on top of it.
func WithVerifier ¶
WithVerifier delegates making, texting and checking codes to a provider's verification service; see Verifier. WithSMS is still required for the messages that are not codes.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the phone 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 a number is unique among verified rows and verifiable, but neither signs in nor recovers.
func (*Plugin) Change ¶
Change starts replacing the signed-in user's number by texting a code to the new one; nothing changes until ConfirmChange, so the account keeps its old number in between. It also sets a first number in one step for a user who has none.
func (*Plugin) ConfirmChange ¶
ConfirmChange checks the code texted by Change: the new number becomes the verified primary and the old number is removed, in one transaction.
func (*Plugin) DataHooks ¶
DataHooks send a code to every new unverified number, however it was created.
func (*Plugin) Deliver ¶
Deliver renders m in its locale's template and texts it. The core calls it after the transaction commits, so no text goes out for a rolled-back row.
With a Verifier, a code message with no code is one the provider makes: the plugin asked for a verification (issueCode) and the provider texts its own code here, through the same background path every delivery takes.
func (*Plugin) Identifiers ¶
func (p *Plugin) Identifiers() []auth.IdentifierKind
Identifiers registers the phone kind with the core, which from then on owns its uniqueness and verified checks.
func (*Plugin) List ¶
List returns the signed-in user's numbers, verified or not. Registered only under Multiple; without it the call is auth.ErrOperationNotWired.
func (*Plugin) MakePrimary ¶
MakePrimary makes one of the signed-in user's verified numbers the primary one, the number texts go to. An unverified one is auth.ErrNotVerified, a number the user does not have auth.ErrIdentityNotFound. Registered only under Multiple, and it needs a fresh sign-in, as change does.
func (*Plugin) Operations ¶
Operations lists the operations; code sign-in only when enabled, so an app without it exposes no route that texts codes to strangers.
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 codes sent and guessed, per number, per IP, and per number range when LimitRange is set, because every text costs money.
func (*Plugin) Resend ¶
Resend texts a new verify code to the signed-in user's unverified number. It answers the same when the number is already verified or not theirs.
func (*Plugin) SendCode ¶
SendCode texts a sign-in code to any valid number, so the answer never reveals whether it has an account.
func (*Plugin) SendVerification ¶
SendVerification texts a fresh verify code for one of this plugin's unverified numbers, 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) SignIn ¶
SignIn checks a sign-in code and starts a session, creating the account when the number has no verified owner.
func (*Plugin) SignInWith ¶
func (p *Plugin) SignInWith(ctx context.Context, in SignInRequest) (SignInResult, error)
SignInWith is SignIn with the whole request, for a Go caller that sets Fields on the account the code creates.
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) Tables ¶
Tables declares the aliased identity table, so auth.New can reject another plugin claiming the same alias.
type SMSFunc ¶
SMSFunc adapts a function to SMSSender, for an app whose sender is a closure over its provider's client.
type SMSSender ¶
SMSSender delivers a text message. The app implements it over its provider, so the library depends on nobody's SDK.
type SignInRequest ¶
type SignInRequest struct {
Phone string `json:"phone"`
Code string `json:"code"`
// Fields sets the auth.WithSignUpFields columns of the account a code
// creates, 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 number already has an owner.
Fields auth.Fields `json:"fields,omitempty"`
// contains filtered or unexported fields
}
SignInRequest is the input of sign-in: a number, its code, and the auth_user columns of the account the code creates, better-auth's additional fields in the phone verify body.
func (SignInRequest) RateIdentifier ¶
func (r SignInRequest) RateIdentifier() string
RateIdentifier counts guesses per normalized number, as CodeRequest does.
func (*SignInRequest) UnmarshalJSON ¶
func (r *SignInRequest) UnmarshalJSON(data []byte) error
UnmarshalJSON keeps the top-level keys that are not the request's own, so sign-up fields may sit beside phone and code; auth.Auth.MergeFields then refuses any that is not a declared column.
type SignInResult ¶
type SignInResult struct {
User auth.UserView `json:"user"`
// Created is true when the code created the account.
Created bool `json:"created"`
// SecondFactorRequired is true when the session is pending a second
// factor.
SecondFactorRequired bool `json:"secondFactorRequired"`
Token auth.Token `json:"-"`
}
SignInResult is the output of sign-in. Token is for Go callers and bearer transports and is never encoded into JSON.
type Verifier ¶
type Verifier interface {
Send(ctx context.Context, number string) error
Check(ctx context.Context, number, code string) error
}
Verifier is a provider's own verification service, such as Twilio Verify: it makes the code, texts it and checks it, so the library never sees the code. Send starts a verification for the number; Check reports a wrong, expired or exhausted code as ErrCodeInvalid and any other failure as its own error, which fails the operation rather than refusing the user.
With a Verifier (WithVerifier) the plugin issues no code of its own for verify, sign-in and change: the provider's attempt counter and lifetime apply instead of WithMaxAttempts and WithCodeTTL. The rate limits on sending still apply, because every Send costs money. Other messages (a reset link, a sign-in link) still go through the SMSSender.