Documentation
¶
Overview ¶
Package magiclink signs users in with a single-use link sent to one of their identifiers, usually an email address.
It knows no mail and no SMS: it asks the core to deliver to whichever plugin owns the identifier's kind, so it works with email, phone, or a kind a third plugin adds. A link signs in only the verified owner of the identifier; with SignUp it also creates an account for an identifier nobody has verified, whose control the click has just proven.
Index ¶
- Constants
- Variables
- type Option
- func GenerateToken(fn TokenGenerator) Option
- func Kinds(kinds ...auth.IdentifierKindName) Option
- func Links(fn func(t auth.Token) string) Option
- func SignUp() Option
- func VerifyByGET() Option
- func WithIPLimit(n int, window time.Duration) Option
- func WithSendLimit(n int, window time.Duration) Option
- func WithTTL(d time.Duration) Option
- type Plugin
- func (p *Plugin) Init(a *auth.Auth) 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) Send(ctx context.Context, identifier string) error
- func (p *Plugin) SendAs(ctx context.Context, identifier, name string) error
- func (p *Plugin) SendWith(ctx context.Context, in SendRequest) error
- func (p *Plugin) SendWithMetadata(ctx context.Context, identifier string, metadata map[string]string) error
- func (p *Plugin) SignIn(ctx context.Context, t auth.Token) (SignInResult, error)
- func (p *Plugin) SignsIn() bool
- func (p *Plugin) Validate(c *auth.Config) error
- type SendRequest
- type SignInRequest
- type SignInResult
- type TokenGenerator
- type VerifyRequest
Constants ¶
const ( EndpointSend auth.EndpointID = "magiclink.send" EndpointSignIn auth.EndpointID = "magiclink.sign-in" // EndpointVerify is the GET sign-in VerifyByGET adds. EndpointVerify auth.EndpointID = "magiclink.verify" )
Operation ids, exported so a hook or a rate rule can name them. They are wire names, stable across releases.
const ( // DefaultTTL is short: a sign-in link is used within minutes or not at // all, and a long one is only useful to whoever reads the mailbox later. //ds:def id=defaultttl-z8p2fp62 owner=@khanakia stability=stable DefaultTTL = 15 * time.Minute // DefaultPage is the app page a link opens; it POSTs the token to // magiclink.sign-in. A GET that signed in would be spent by every mail // scanner that previews links. //ds:def id=defaultpage-uke973qb owner=@khanakia stability=stable DefaultPage = "/magic-link" )
Defaults and limits.
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 PluginName = "magiclink"
PluginName is the plugin's permanent identity.
const PurposeSignIn auth.Purpose = "magiclink.sign-in"
PurposeSignIn is the verification purpose of a link. Its Identifier is "<kind>:<normalized value>": the link is bound to the identifier it was sent to, not to an account, because the account may not exist yet. Its Meta is the tenant (auth.WithScope) the link was sent in, or "", followed by metaSeparator and the new user's name when the request gave one.
Variables ¶
var ( ErrNoKinds = errors.New("magiclink: no identifier kind can receive a link; add a Verifiable kind whose plugin delivers, such as email") ErrNoBase = errors.New("magiclink: links need auth.WithBaseURL or magiclink.Links") ErrBadKinds = errors.New("magiclink: a kind given to Kinds must be registered, sign-in capable, verifiable and owned by a plugin that delivers") // ErrCallbackInvalid refuses a callbackURL, newUserCallbackURL or // errorCallbackURL that is not a page of this site or of a trusted // origin (auth.Auth.TrustsReturnURL), at send and at verify, so the GET // route is never an open redirect. //ds:def id=magiclink-errcallbackinvalid-7tq2mw5x owner=@khanakia stability=stable ErrCallbackInvalid = auth.NewError("magiclink.callback_invalid", http.StatusBadRequest, "This page cannot be opened after signing in.") )
Configuration errors auth.New returns, so a setup that could never send a link fails at start rather than at the first sign-in.
Functions ¶
This section is empty.
Types ¶
type Option ¶
type Option func(*Plugin)
Option configures the plugin; options are applied in order by New.
func GenerateToken ¶
func GenerateToken(fn TokenGenerator) Option
GenerateToken replaces the core's random 256-bit token with fn's, better-auth's generateToken. It is a plain override: whatever fn returns is the token, and an error from fn fails the send. Off by default.
Risk: a link token is found by its hash alone, with no attempt counter, so a guessable fn signs in whoever guesses it.
func Kinds ¶
func Kinds(kinds ...auth.IdentifierKindName) Option
Kinds limits links to these identifier kinds. By default every registered kind that can sign in and has a verification flow accepts links.
func Links ¶
Links builds the URL a message carries, for an app whose sign-in page is not DefaultPage. The page must POST the token; a GET must not spend it, unless the app chose VerifyByGET and its risk.
func SignUp ¶
func SignUp() Option
SignUp lets a link create an account for an identifier nobody has verified. Without it, a link to an unknown identifier is not sent at all, and the answer is the same either way.
func VerifyByGET ¶
func VerifyByGET() Option
VerifyByGET adds better-auth's GET sign-in: the link in the message is BaseURL + DefaultMountPrefix + "/magiclink/verify?token=…" with the send request's callbackURL, newUserCallbackURL and errorCallbackURL, and that GET spends the token, signs in and redirects: to newUserCallbackURL when the link created the account (SignUp), else to callbackURL, and on a bad link to errorCallbackURL (or callbackURL) with ?error=<code>. Without a callbackURL it answers JSON like POST sign-in. Links still overrides the link.
Off by default, and the risk is why: a GET spends the token, so a mail scanner or a chat preview that fetches the URL signs in itself and the person then gets "this link has expired"; and a GET sign-in cannot be origin-checked, so a link to the attacker's own account signs a victim into it (login CSRF), which auth.WithFetchMetadata, judging mutations only, does not cover. The default page, which POSTs the token, has neither problem. Every callback must pass auth.Auth.TrustsReturnURL.
func WithIPLimit ¶
WithIPLimit replaces the default of 20 links per IP per hour, which bounds one client cycling through many identifiers; raise it behind a shared NAT.
func WithSendLimit ¶
WithSendLimit replaces the default of 3 links per identifier per 15 minutes. Every link is a mail or a text to somebody, so this is what keeps the route from flooding an inbox.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the magic-link sign-in method. Build it with New: the zero value has no TTL and no operations.
func New ¶
New builds the plugin. With no options, every sign-in kind that can be verified and delivered to accepts links, and a link never creates an account.
func (*Plugin) Init ¶
Init fills the default kinds and link, and refuses a kind whose plugin cannot deliver. Validate cannot see that: the configuration lists kinds, not the plugins behind them.
func (*Plugin) Operations ¶
Operations lists send and sign-in; both are public, since the link is the proof of identity.
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 links per identifier and per IP, so the send endpoint cannot flood an inbox or be used to spam many.
func (*Plugin) Send ¶
Send delivers a sign-in link when the identifier may receive one, and answers the same when it may not.
func (*Plugin) SendAs ¶
SendAs is Send with the display name a new account gets (SignUp); the name is ignored when the identifier already has an owner.
func (*Plugin) SendWith ¶
func (p *Plugin) SendWith(ctx context.Context, in SendRequest) error
SendWith is Send with the whole request: the display name, metadata and the GET sign-in's pages (VerifyByGET).
func (*Plugin) SendWithMetadata ¶
func (p *Plugin) SendWithMetadata(ctx context.Context, identifier string, metadata map[string]string) error
SendWithMetadata is Send with metadata the sender receives beside the link; see SendRequest.Metadata.
func (*Plugin) SignIn ¶
SignIn spends a link's token and starts a session for the identifier's verified owner, or creates one with SignUp.
type SendRequest ¶
type SendRequest struct {
Identifier string `json:"identifier"`
// Name is the display name for the account the link creates (SignUp);
// it travels in the token and is ignored when the identifier has an owner.
Name string `json:"name,omitempty"`
// Metadata travels to the delivering plugin's sender with the link (an
// invite id, a page to return to) and is not stored. At most
// email.MaxMetadataEntries entries of email.MaxMetadataLength bytes.
Metadata map[string]string `json:"metadata,omitempty"`
// CallbackURL, NewUserCallbackURL and ErrorCallbackURL are where the GET
// sign-in (VerifyByGET) lands: after a sign-in, after an account was
// created, after a failure. They travel in the link, are refused with
// ErrCallbackInvalid unless auth.Auth.TrustsReturnURL accepts them, and
// are ignored without VerifyByGET or with Links.
CallbackURL string `json:"callbackURL,omitempty"`
NewUserCallbackURL string `json:"newUserCallbackURL,omitempty"`
ErrorCallbackURL string `json:"errorCallbackURL,omitempty"`
}
SendRequest names where the link goes: any sign-in identifier, as typed; its kind is detected.
func (SendRequest) RateIdentifier ¶
func (r SendRequest) RateIdentifier() string
RateIdentifier counts links per identifier, folded like the email normalizer so case changes do not reset the limit.
type SignInRequest ¶
SignInRequest carries the token from the link, which the page POSTs.
type SignInResult ¶
type SignInResult struct {
User auth.UserView `json:"user"`
// Created is true when the link created the account (SignUp).
Created bool `json:"created"`
SecondFactorRequired bool `json:"secondFactorRequired"`
Token auth.Token `json:"-"`
}
SignInResult is the output of sign-in. Token is for Go callers and bearer transports, never encoded into JSON.
type TokenGenerator ¶
TokenGenerator returns the token a link carries instead of the core's random one, given the normalized identifier the link goes to; better-auth's generateToken(email).
type VerifyRequest ¶
type VerifyRequest struct {
Token auth.WireToken `json:"token"`
CallbackURL string `json:"callbackURL,omitempty"`
NewUserCallbackURL string `json:"newUserCallbackURL,omitempty"`
ErrorCallbackURL string `json:"errorCallbackURL,omitempty"`
}
VerifyRequest is the query of the GET sign-in (VerifyByGET): the token and the pages SendRequest named.